実は動かなくなったLinuxコマンド

前回までに本連載で解説してきた設定で、WindowsからLinuxのコマンドのほとんどをネイティブコマンドのように呼び出すことができるようになった(ように見せかけていた)のだが、実はそれらの変更によって逆に動作しなくなったコマンドがある。例えば、次のようにLinuxで動作するエディタをWindowsから実行してみよう。

WindowsからLinuxのエディタを実行

一見、次のように問題なく動作するように見える。しかし、このエディタは全く反応してくれない。

反応しないエディタ

仕方ないので、Linux側から動作しているプロセスを特定してkillコマンドで終了する。

Linux側からkillコマンドでエディタを強制終了

Linux側から強制終了すると制御が戻ってくる。すると、次のようにターミナルにエディタを操作しようとして入力したキーが出力されることを確認できる。

エディタの強制終了後にキー入力が表示される

これは、これまでの設定変更によってエディタにパイプラインがつながれたことに原因がある。エディタはユーザーの入力を標準入力から得るのだが、「$Input」に接続しているのでパイプラインからの入力を待つようになってしまった。これが原因で、ユーザーの操作がエディタに届かなくなったのだ。

今回はこの問題を解決して、パイプラインを使う場合も使わない場合も、問題なく動作する方法を紹介する。

$Inputでパイプライン接続の有無を判断する

いくつかのやり方が考えられるが、ここでは簡単に自動変数$Inputを使って判断することにする。パイプラインが接続された場合、自動変数$Inputには複数のプロパティが設定される。例えば、Lengthプロパティだ。このプロパティが存在する場合にはパイプラインが接続されており、このプロパティが存在していなければパイプラインは接続されていない、と判断する。

PowerShellでインタラクティブに動作を確認してみると、この仕組みで十分使えそうなことがわかる。

$Input.Lengthの有無でパイプラインを判断できそう

前回まで、Linuxコマンドを実際に呼び出すための関数を定義していた部分は次のようになっていた。

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

この部分に、次のようにパイプラインの接続の有無で切り分け処理を追加してあげることにする。

                if (`$Input.Length) {
                    パイプラインからデータが入ってくる場合
                }
                else {
                    パイプラインがつながっていない場合
                }

ただし、注意が必要だ。$Inputは1回使ってしまうともう使えない。サイド$Inputを使うには、「$Input.Reset()」のようにして一旦リセットする必要がある。つまり、処理としては次のようになる。

                if (`$Input.Length) {
                    `$Input.Reset()
                    パイプラインからデータが入ってくる場合
                }
                else {
                    パイプラインがつながっていない場合
                }

これを加味すると、WindowsからLinuxコマンドを呼び出すための関数定義部分は次のように変更すればよいことになる。

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

これで今回の問題は解決できそうだ。

動作確認

今回の設定を反映させた$PROFILEで動作を確認してみよう($PROFILEは稿末に掲載している)。まず、次のように書き換えた関数定義の内容を確認するとともに、wcコマンドにパイプラインで接続してパイプライン経由でLinuxのコマンドが動作しているかどうかを確認する。

パイプライン経由でLinuxコマンドの動作を確認

問題なく動作していることがわかる。関数の定義も問題なさそうだ。

次に、エディタを起動してパイプラインが接続されていないケースの動作を確認する。

Linuxのエディタを起動する

エディタの操作ができることを確認

エディタも問題なく動作することがわかる。

エディタ終了後も問題なし

エディタを終了しても、終了後に操作用のキー入力が現れることはない。これで、今回の問題は解決だ。

問題を解決しながら$PROFILEを育てる

これまで使いたい機能を追加し、問題があればその部分を修正するといった流れで$PROFILEの内容を育ててきた。ただし、現在の$PROFILEがパーフェクトかと言えば、実はそうでもなく、まだまだ荒削りで問題点も多い。結構シームレスにLinuxのコマンドが利用できるようになってきているとは言え、まだいくつか課題は残っている。

こうした設定ファイルを書いていく作業では、ともかく一気に全部やろうとしないというのが一つのポイントだ。実用に際して問題を感じないのであれば、とりあえずそのままでよいと思う。改善や修正にかける時間がもったいないからだ。時間が十分にあるならよいが、そうでないなら必要に迫られたときに都度、改善する方法をお薦めしたい。そのほうが、必要な設定だけをスッキリとまとめたファイルにできるだろう。

必要なときに必要な変更や改善を加えるというのは、ソフトウェア開発のアプローチの方法としては古くからある考え方のひとつでもある。特に小さいツールや設定ファイルの場合には有効なアプローチだと思うので、ぜひ試してもらえればと思う。

付録: $PROFILE

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

本連載時点での$PROFILE

#========================================================================
# Definition of Linux commands used via wsl
#========================================================================
$_linux_path = (wsl echo '$PATH').Split(":") -NotMatch "/mnt"
$_linux_command_names = wsl ls $_linux_path

# Generate Linux command functions
ForEach($n in $_linux_command_names) {
    if ($n -ne "") {
        $_linux_functions += "
            function $n {
                if (`$Input.Length) {
                    `$Input.Reset()
                    `$Input | wsl $n `$(_path_to_linux `$Args)
                }
                else {
                    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"