新しい設定に移る前に、前回の処理をもう少し改善しよう。前回、Linuxコマンドの大半をそのままWindows 10から実行できるようにしたわけだが、実はそのアプローチには少し問題がある。例えば、次のように定義したLinuxコマンド対応関数の数を調べようとしてみよう。期待としては、wcコマンドによる行数が表示されてほしいところなのだが、実際にはそうはならない。

wcコマンドが行数を表示してくれない

「Ctrl」+「C」で停止すると戻ってくる

パイプラインで「wc -l」に接続しているので、標準出力の行数がカウントされて行数が出力されることを期待するのだが、実際には上記スクリーンショットのようにwcコマンドは停止しているように見える。

これは前回定義した関数がパイプラインを考慮していないことに原因がある。例えば、1つだけ別に定義したgrepで同じことをすると、次のように期待通りに動作することを確認できる。

grepはパイプラインに対して期待通りに動作している

これはgrepに対応する関数ではパイプラインを考慮した処理が記述されているためだ。grepの該当する部分は次のようになっていた。

function grep {

...略...

    $Input | wsl grep $Args
}

ポイントは「wsl grep」の処理に対して「$Input |」というパイプが接続されている点にある。この処理によって、前の処理のデータがパイプラインを介してgrepコマンドへ接続されている。

同じようにwslでLinuxのコマンドを実行している前回定義した関数部分を見ると、次のようになっている。

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

wslはそれ単体で実行されており、「$Input |」は接続されていない。これが原因だ。つまり、wcコマンドの動作が停止したように見えたのは、wcコマンドがその場でパイプラインからの入力ではなく、標準入力からのデータ入力を待っている状態になっているためということになる。

この部分をパイプとの接続を考慮するように変更すると、問題を解決することができる。

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

なお、この部分は一旦テキストファイル(.ps拡張子の一時ファイル)に書き出してから読み込むという処理をするため、「$(ドル記号)」をそのままドル記号として保持するためにバッククォートでエスケープ処理されている点に注意しておこう。「$Input |」だと「$Input」が変数として解釈され、「|」だけが使われてしまう。バッククォートでエスケープすることで「$Input |」をそのままテキストファイルへ書き出している。詳しい説明は前回をされたい。

最初に実行したコマンドをこの新しい定義でもう1回実行すると、次のようにwcコマンドが期待通りの動作をすることが確認できる。

改良後の動作

上記スクリーンショットからわかるように、大体2770個くらいのコマンドが定義されていることになるのだが、もしこれを全部関数として事前に用意しておいた場合、「$PROFILE」の中身は1万行くらい増え、今回の書き換えも2770回くらい行う必要があったことになる。$PROFILE読み込み時にダイナミックに関数を生成して利用できるようにしたことで、1カ所変更するだけで全ての関数に処理を反映することができたのだ。

Linuxのlsコマンドを使う

前述した処理でlsコマンドに関してもLinuxのlsコマンドが関数として定義されているのだが、「ls」を実行すると次のようにGet-ChildItemコマンドレットの実行結果が出力される。コマンドプロンプトでのdirコマンドの出力に似ている。

PowerShell 7では、「ls」はGet-ChildItemコマンドレットへのエイリアス

なぜこのようなことが起こるかと言うと、PowerShell 7ではlsはGet-ChildItemコマンドレットへのエイリアスになっているためだ。関数として「ls」が定義されていても、エイリアスで定義されているとエイリアスのほうが優先されるので、結果的にGet-ChildItemコマンドレットが実行される。

ただし、次のように処理すれば定義されているエイリアスは削除することができる。

Remove-Item alias:ls

ここでもうひと工夫しておこう。$PROFILEの編集を行っている場合、「. $PROFILE」のようにすることで書き換えた$PROFILEの内容を現在のPowerShellへ適用することができる。この方法で書き換えと動作確認を行っていった場合、上記の書き方だと2回目以降はエラーが発生する。それは1回目の処理で「ls」のエイリアスを削除してしまったので、2回目には「そのようなエイリアスはない」というエラーが出てしまうのだ。

いくつかやり方があるが、順当に考えると「lsがエイリアスとして定義されている場合には、lsエイリアスを削除」といった処理にすればよい。エイリアスが定義されているかどうかは「Get-Alias ls」を実行すれば調べることができる。ただし、こちらでもエイリアスが存在していないと赤いエラーメッセージが出力されてしまう。ここでリダイレクトを使い、エラーメッセージもろとも「$null」へ流し込んでやるようにする。こうすれば、Get-Aliasがエラーメッセージを発生させても出力としては表示されない。つまり、次のように記述しておけばよいことになる。

Get-Alias ls *> $null && Remove-Item alias:ls

Linuxのエイリアスコマンドは最近では「ls —color=auto」へのエイリアス(Linuxのインタラクティブシェルのエイリアス)になっていることが多い。PowerShellでも同じように動作させるなら、次のようにPowerShellの関数として定義し、そこで「—color=auto」を指定すればよい。

function ls { wsl ls --color=auto $Args }

連載では、これまで「ll」と「la」を次のようにGet-ChildItemコマンドレットとして設定していた。

function ll { Get-ChildItem -Force }
function la { Get-ChildItem -Force }

「ls」でLinuxコマンドの「ls」が使われるように書き換えたので、この部分もLinuxシェルにおけるエイリアスのように、次のように書き換えてみる。これで動作がLinuxへの設定に近づく。

function ll { ls -l }
function la { ls -a }

この状態で「ls」を実行すると次のようになる。「Get-ChildItem」ではなくLinuxのlsコマンドが実行されていることがわかる。

lsでLinuxのlsコマンドが実行される

「ll」では「ls -l」が実行され、やはりLinuxと同じ出力を得ることができる。

「ll」でもLinuxコマンドのlsコマンドが実行される

普段LinuxやmacOSといったUNIX系の環境を使っている場合、PowerShellのGet-ChildItemコマンドレットよりもlsコマンドの出力のほうがなじみがあるだろう。最初からエイリアスが設定されているため一旦エイリアスを解除する必要があるが、今回のように設定しておけば、エイリアスを解除して新しい設定を適用することができる。Linux側のコマンドに慣れている場合、今回取り上げた方法でカスタマイズしていただきたい。

付録: $PROFILE

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

#========================================================================
# Linux commands 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 functions
ForEach($n in $_linux_command_names) {
    if ($n -ne "") {
        $_linux_functions += "
            function $n {
                `$Input | 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 definitions
#========================================================================
# grep
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
}

# ls
Get-Alias ls *> $null && Remove-Item alias:ls
function ls { wsl ls --color=auto $Args }
function ll { ls -l }
function la { ls -a }

#========================================================================
# 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"