前回、Linuxコマンドを汎用的にWindows 10から利用できるようにする方法を取り上げた。grepに関してはコマンド別カスタマイズのサンプルとして個別に対応させたが、それ以外に関しては大雑把に引数に渡ってきたパスをWindowsパスからLinuxパスに変換してwslコマンド経由で実行させた。

これにより、利用したいLinuxコマンドが増えたときには配列に利用したいLinuxコマンドを追加すれば良いようになった。しかし、これでもまだ煩雑さは残る。例えば、「apt」で新しいコマンドをインストールした場合、これをWindows 10から利用しようとしたら対象コマンドを配列に追加しなければならない。人間とはどんどん堕落していくもので、こういったコマンドも含めて自動的に利用できるようになってくれないかな、と思うようになる。

そこで今回は、そうした思いをかなえる方法を取り上げよう。この設定で、かなり多くのLinuxコマンドをWindows 10から読み込めるようになる。

$PROFILEの変更内容

仕組みの発想はこうだ。Linux環境の環境変数PATHにはコマンドがデプロイされているパスが記載されている。このパスに存在しているコマンドを自動的にWindows 10から使えるようにしよう、というわけだ。例えば、Ubuntu 20.04 LTSであれば/bin、/sbin、/usr/bin、/usr/sbin、/usr/local/bin、/usr/local/sbinあたりにコマンドがインストールされる。まず、これを$RPOFILEで次のように配列に入れておく。

$_linux_path = @('/usr/local/sbin', '/usr/local/bin', '/usr/sbin',
                 '/usr/bin', '/sbin', '/bin')

前回は、変数$_linux_command_namesに利用したいコマンド名を格納しておいた。つまり、この部分を次のように変更すれば、大雑把にではあるが先ほどのパスに収められているコマンド名が得られることになる。

$_linux_command_names = wsl ls $_linux_path

この方法だと空行も配列に収められてしまうので、配列から関数を作成する部分を次のように書き換える。

ForEach($n in $_linux_command_names) {
    if ($n -ne "") {
        $_linux_functions += "
            function $n {
                wsl $n `$(_path_to_linux `$Args)
            }"
    }
}

これで/bin、/sbin、/usr/bin、/usr/sbin、/usr/local/bin、/usr/local/sbinに収められているコマンドは自動的に関数として作成されることになる。大雑把な方法ではあるが効果的な書き換えだ。

$PROFILEの中身を今回の変更を適用したものにアップデートしてからPowerShellを再起動する。「$_linux_command_names」に変換対象となったコマンド名が収められているので、表示させると次のようになる。

対象となったコマンド

同じように「$_linux_functions」には定義した関数が収められており、表示すると次のようになる。

作成された関数

書き換えた部分はわずかだが、これで網羅的に多くのLinuxコマンドがWindows 10から利用できるようになる。

もしこれを前回の方法でやろうとすれば、配列に数千個のコマンドを書いておかないといけない。しかも、「apt」で新しいコマンドをインストールしたら、それも手動で追加する必要がある。しかし、今回の方法であれば、PowerShell起動時に自動的に設定できる。特にこれまでLinuxやmacOSのターミナルでコマンドを多用するような使い方をしてきた方にとって、今回の設定は役に立つだろう。

少しずつ$PROFILEを育てていこう

インタラクティブシェルとして$PROFILEの内容を書いていく場合、小さく小さく$PROFILEの内容を変更していくほうがよいとこれまでにも何度か説明してきた。$PROFILEの内容は必要に応じて変更していったほうが要望に合致したものになりやすいし、実際に書き換えたものをしばらく使ってみないと次の改善点が見えてこないのだ。

$PROFILEの変更はPowerShellの学習という面でも利点がある。PowerShellは便利なシェルであり、プログラミング言語ではあるのだが、便利な機能も必要性がないとなかなか覚えないものだ。「$PROFILEを書いて日々の作業を楽にしていこう」というモチベーションがあれば、それは直接PowerShellの学習につながっていく。現時点の$PROFILEでも変数、配列、関数、繰り返し処理、ヒアドキュメント、比較処理、コマンドレット、ネイティブコマンド、エイリアスなど、さまざまなPowerShellの機能を使っている。実用的なサンプルとして自分だけの$PROFILEを育てていくというのは、有効な学習方法だ。ぜひとも取り組んでいただきたい。

付録: $PROFILE

本稿執筆時点での$PROFILEを次に掲載しておく。参考にしてもらえれば幸いだ。

#========================================================================
# Linux command definition used via wsl
#========================================================================
$_linux_path = @('/usr/local/sbin', '/usr/local/bin', '/usr/sbin',
                 '/usr/bin', '/sbin', '/bin')
$_linux_command_names = wsl ls $_linux_path

# Generate Linux command function
ForEach($n in $_linux_command_names) {
    if ($n -ne "") {
        $_linux_functions += "
            function $n {
                wsl $n `$(_path_to_linux `$Args)
            }"
    }
}
$_linux_functions += @'
    function _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
    }
'@

# 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
. $_temp_ps1
Remove-Item $_temp_ps1

# Delete unnecessary variables
Remove-Variable _temp
Remove-Variable _temp_ps1
#Remove-Variable _linux_path
#Remove-Variable _linux_command_names
#Remove-Variable _linux_functions

# Individual Linux command function definition
function 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
}

#========================================================================
# Alias definition
#========================================================================
Set-Alias -Name open -Value explorer
Set-Alias -Name edge -Value "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
Set-Alias -Name chrome -Value "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"

#========================================================================
# Short name definition
#========================================================================
function ll { Get-ChildItem -Force }
function la { Get-ChildItem -Force }