WSL LinuxをWindowsとなじませる設定

Windowsとの親和性という面ではWSL2よりもMSYS2に軍配が上がる。Linuxを日常使いしているほど感じやすいのだが、ちょっとした部分でWSL2はイラッとすることがある。ただし、インタラクティブシェルとしてPowerShell 7を使っているのであれば、設定である程度WSL2の親和性を引き上げることができる。

前回はその方法として次の設定を紹介した。

#========================================================================
# 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(' ') }
}

具体的には、この設定を$PROFILEで設定されているパスのファイルへ書き込む。パスはWindows Terminalなどのターミナルアプリケーションで「echo $PROFILE」を実行すれば確認できるほか、「notepad $PROFILE」のようにして直接編集してもよい。

$PROFILEにこの設定を書き込んだら新しいPowerShel 7を起動し、「linuxcmds」を実行する。これでWSLのUbuntuに含まれているコマンドがwslコマンドの指定なしでもWindows側から実行できるようになる。毎回実行するのが面倒な場合は、$PROFILEの設定の後に、次の設定を追加しておけばよい。

linuxcmds

これで、WSLで動作するLinuxのコマンドをMSYS2のようにネイティブっぽいコマンドとしてWindowsからも利用できるようになる。

何を行っているのか

先ほどの「linuxcmds」を実行すると、次のようになる。

linuxcmdsの実行サンプル

この関数ではまず、WSL Linuxで定義されている環境変数PATHの値を取得し、そのパスに収められているコマンドの一覧を取得している。そして、そのコマンドと同じ名前の関数を文字列として作成し、作成した文字列状態の関数を一旦ファイルに書き出して、それを現在のセッションのPowerShellに読み込ませて本来の関数として機能させている。

また、WindowsのパスとWSL Linuxのパスは記述方法やトップディレクトリが異なるので、そのパスを自動変換する関数も作成し、先ほど作成したコマンド名の関数のなかで利用している。こうすることで、実際にはWSL Linuxのコマンドが実行されているのではなく、PowerShellで同名の関数が実行され、その中で「wsl コマンド」のようにしてWSL側のコマンドが実行される、という仕組みを実現している。ほかにも細々としたことを行っているが、大枠としてはこのようなところだ。

例えば、先ほどのスクリーンショットでは、「screenfetch」をPowerShell 7で実行している。そんなコマンドは存在しないので失敗しているわけだが、linuxcmdsを実行した後は、次のように「screenfetch」という処理が機能していることがわかる。

linuxcmds実行後はWSL Linuxのコマンドがwslなしで実行できている

上記のように、PowerShellで実行する「screenfetch」も「wsl screenfech」も同じ処理になっている。このようにラッパー関数というべきものをたくさん作ることで、あたかもWindowsでWSL Linuxのコマンドがネイティブに動作しているかのように振る舞わせている。

冒頭に掲載したlinuxcmdsでは、ほかにも動作しないページャーを動作する体にしている仕組みや、いわゆるLinuxにおけるエイリアスと同じような短縮名コマンドの実装などが行われている。PowerShell 7からWSLを活用する場合には参考にしていただきたい。

完璧ではないが必要十分

今回紹介した方法は完璧というわけではないのだが、WSLをWindowsからもネイティブ風に使いたいという場合にはそれなりに使える設定だと思う。WSLで動作するLinuxは、何といっても動作が高速だ。こういったギミックで十分利用に耐えるのであれば、かなり効果的な方法ではないかと思う。

また、ここで行っている方法はあくまでもPowerShell 7に限定した方法だ。Windowsでほかのシェルを使っているのであれば、そのシェルの書き方でここで説明したのと同じような方法を実現する必要がある。