前回までに説明してきたように、MSYS2とWSL2はいずれも「LinuxのコマンドをWindowsで実行する」ことができるが、両者は採用している仕組みが全く異なり、その性能にも大きな違いがある。今回は、この2つの技術の違いとして、Windowsとの親和性について説明する。

MSYS2は基本的にLinuxのコマンドをWindowsでネイティブに実行している。次のようにMSYS2にインストールしたneofetchコマンドを実行してみよう。出力としては「Windows 11」が表示されている。あくまでも動いているのはWindowsカーネルの上であって、Linuxカーネルではないのだ。

neofetch / MSYS2

同じコマンドをWSL2で動作しているUbuntuで実行すると次のようになる。

neofetch / WSL2 Ubuntu

WSL2はHyper-V仮想環境でLinuxカーネルを動作させる技術だ。Linuxコマンドは当然ながらLinuxカーネルで実行される。実行結果は上記のようにLinuxが出力される。

WSL2はwslコマンドでWindows側となじむ

WSL2は、仮想環境のなかでLinuxを実行する技術だ。Visual StudioやVisual Studio Codeから使う場合は拡張機能でうまく補えるが、Windowsのネイティブな環境、例えばPowerShellからWSL2を利用しようとすると、どうしても一旦WSL2の環境へ入る必要がある。

しかし、WSL2は「wsl」というコマンドを介すことでLinuxのコマンドをWindows側からまるでネイティブなコマンドのように実行することができる。次のような感じだ。

wslコマンドを使い、WindowsからUbuntu neofetchを直接実行

インタラクティブシェルとしてPowerShell 7を使っているのであれば、この機能を使うことで、あたかも直接Linuxのコマンドを実行しているかのように見せかけることができる。Linuxコマンドと同名のPowerShell関数を作成し、その関数で「wsl コマンド」といったようにWSL2側のコマンドを呼び出す仕組みにすればよいのだ。

さらにいくつか細かい調整をすると、WSL2でもかなりMSYS2的な動きをさせることができる。今回は説明しないが、次のような関数をPowerShellに噛ませて、「linuxcmds」という関数を実行すればWSL2側のLinuxコマンドをWindowsネイティブなコマンド風に使うことができるようになる。

#========================================================================
# Linux commands integration mode
#========================================================================
function linuxcmds {

    #----------------------------------------------------------------
    # Definition of Linux commands used via wsl
    #----------------------------------------------------------------
    # Linux pagers
    $_linux_pagers = @("less", "lv")

    # Linux PATH and commands
    Write-Host "checking linux commands:"
    $_linux_path = (wsl echo '$PATH').Split(":") -NotMatch "/mnt"
    $_linux_command_paths = (
        wsl ls -d ($_linux_path[($_linux_path.Length - 1)..0] -replace
            "$","/*")
    ) 2> $null

    # Generate Linux commands functions
    ForEach($n in $_linux_command_paths) {
        $_n = (Split-Path -Leaf $n)
        $_linux_functions += "
            function global:$_n {
                if (`$Input.Length) {
                    `$Input.Reset()
                    `$Input | wsl $n ([String]`$(_path_to_linux
                        `$Args)).Split(' ')
                }
                else {
                    wsl $n ([String]`$(_path_to_linux `$Args)).Split(' ')
                }
            }
            Write-Host -NoNewline .
        "
        Write-Host -NoNewline '_'
    }

    # Generate Linux pagers functions
    ForEach($_n in $_linux_pagers) {
        $_linux_functions += "
            function global:$_n {
                if (`$Input.Length) {
                    `$Input.Reset();

                    # Prepare temporary file path
                    `$_temp = New-TemporaryFile

                    # Write data from pipeline to the temporary file
                    `$Input | Out-File `$_temp

                    # Do $_n
                    wsl $_n `$(_path_to_linux `$Args).Split(' ') ``
                        `$(_path_to_linux `$_temp.ToString()).Split(' ')

                    # Delete unnecessary temporary file and variable
                    Remove-Item `$_temp
                    Remove-Variable _temp
                }
                else {
                    wsl $_n `$(_path_to_linux `$Args).Split(' ')
                }
            }
            Write-Host -NoNewline .
        "
        Write-Host -NoNewline '_'
    }

    # Function that converts Windows paths to Linux paths
    $_linux_functions += @'
        function global:_path_to_linux {
            $linuxpath = @()

            # Convert arguments to Linux path style
            ForEach($winpath in $Args) {
                if ($winpath -eq $null) {
                    Break
                }

                # Change drive path to mount path
                if ($winpath -match '^[A-Z]:') {
                    $drive = $winpath.Substring(0,1).ToLower()
                    $linuxpath += "/mnt/" + $drive +
                $winpath.Substring(2).Replace('\','/')
                }
                # Option is not converted
                elseif ($winpath -match '^[-+]') {
                    $linuxpath += $winpath
                }
                # Other argument is converted
                else {
                    $linuxpath += ([String]$winpath).Replace('\','/')
                }
            }

            $linuxpath
        }
        Write-Host .
'@
    Write-Host -NoNewline '_'

    # Prepare temporary file path with extension .ps1
    $_temp = New-TemporaryFile
    $_temp_ps1 = $_temp.FullName + ".ps1"
    Remove-Item $_temp

    # Write function definition to temporary .ps1 file and parse
    $_linux_functions | Out-File $_temp_ps1
    Write-Host
    Write-Host "functionizing linux commands:"
    . $_temp_ps1
    Remove-Item $_temp_ps1

    # Delete unnecessary variables
    Remove-Variable n
    Remove-Variable _n
    Remove-Variable _temp
    Remove-Variable _temp_ps1
    Remove-Variable _linux_pagers
    Remove-Variable _linux_path
    Remove-Variable _linux_command_paths
    Remove-Variable _linux_functions

    #----------------------------------------------------------------
    # Individual Linux command function definitions
    #----------------------------------------------------------------
    # grep
    function global:grep {
        $pattern_exists = $False
        $path_exists = $False
        $skip = $False
        $i = 0

        ForEach($a in $Args) {
            if ($skip) {
                $skip = $False
                $i++
                continue
            }

            # Options without argumetn
            if ($a -cmatch '^-[abcdDEFGHhIiJLlmnOopqRSsUVvwxZ]') {
            }
            # Options with argument
            elseif ($a -cmatch '^-[ABC]') {
                $skip = $True
            }
            # Pattern file specification option
            elseif ($a -ceq '-f') {
                $skip = $True
                $pattern_exists = $True
                $Args[$i+1] = _path_to_linux $Args[$i+1]
            }
            # Pattern specification option
            elseif ($a -ceq '-e') {
                $skip = $True
                $pattern_exists = $True
            }
            # Pattern or file path
            elseif ($a -cnotmatch '^-') {
                if ($pattern_exists) {
                    $path_exists = $True
                }
                else {
                    $pattern_exists = $True
                }
            }

            $i++
        }

        # Change file path
        if ($path_exists) {
            $Args[-1] = _path_to_linux $Args[-1]
        }

        $Input | wsl grep $Args
    }

    # ls
    Get-Alias ls *> $null && Remove-Item alias:ls
    function global:ls { wsl ls --color=auto $(_path_to_linux
        $Args).Split(' ') }
    function global:ll { ls -l $(_path_to_linux $Args).Split(' ') }
    function global:la { ls -a $(_path_to_linux $Args).Split(' ') }
}

上記設定はなかなかよくできていると思うのだが、効果があるのはPowerShellだけだ。コマンドプロンプトでは使えないし、それ以外のシェルでも使えない。それぞれのシェルで似たようなギミックを実行する必要がある。

wslコマンドでうまくいかないケースもある

先ほどの方法でかなりのコマンドがWindowsネイティブ風に使えるようになるのだが、ターミナルから直接キー入力を取るようなタイプのコマンド、具体的にはページャがこの方法ではうまく扱えない。ちょっと見てみよう。

catでファイルの中身を確認

上記のコマンドは問題なく動作する。今度はこのコマンドをパイプでlessへつなげてみる。次のような感じだ。

catの出力をlessへつなぐ (MSYS2)

MSYS2で実行すると、次のようにlessも機能すると、カーソルキーやショートカットキーで操作することもできる。

MSYS2だとlessにつないでもちゃんと動く

今度はこれをWSL2のlessへつなげてみよう。

catの出力をlessへつなぐ(WSL2)

一見すると動作するように見えるのだが、カーソルキーもショートカットキーも効かない。「Ctrl」+「C」でプログラムを終了しなければならない。

「wsl less」は制御できない

こんな感じでWSL2はあくまでも仮想環境で動作しているLinuxであり、ページャのようなコマンドをWindows側で使うといった操作がうまくできないのだ。

親和性のよさはMSYS2

やはり、Windowsとの親和性の高さという点ではWSL2よりもMSYS2のほうが高いように思える。Visual StudioやVisual Studio Codeのようにすでに用途が定まっており、拡張機能も整っている環境ではWSL2のほうが便利だが、LinuxコマンドをWindowsで使いたい場合はMSYS2のほうが無難なケースが多い。

これまで見てきて、メモリの消費量とパフォーマンスという点でMSYS2とWSL2には大きな違いがあることがわかったと思う。これら2つの技術は、競合するというよりは、苦手な部分をお互いに補い合う補完関係にあると捉えるほうが建設的だ。特性がわかれば使いこなし方も見えてくる。都合が良いように組み合わせて利用することで、より効果的な操作が可能だ。