$PROFILEを改善

前回は関数を使ってLinuxのコマンドをWindowsのコマンドのようにして使う方法を紹介した。前回取り上げた設定($PROFILE)はシンプルなものだ。今回は前回のファイルをもう少しアップデートした以下の設定ファイルを取り上げる。

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
}

function less {
    wsl less $(_path_to_linux $Args)
}
function lv {
    wsl lv $(_path_to_linux $Args)
}
function vi {
    wsl vi $(_path_to_linux $Args)
}
function vim {
    wsl vim $(_path_to_linux $Args)
}
function nvim {
    wsl nvim $(_path_to_linux $Args)
}
function tree {
    wsl tree $(_path_to_linux $Args)
}
function git {
    wsl git $(_path_to_linux $Args)
}
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
}

Set-Alias -Name open -Value explorer

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

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"

関数として定義するコマンドをいくつか追加してあるほか、前回「path_to_linux」として用意したパス変換用のユーザー定義関数を「_path_to_linux」と名称変更し、中身を拡張してある。また、grep関数については個別に引数をパースする処理に変更した。以降では、変更した内容について具体的に説明していこう。

_path_to_linux関数で引数をそれとなく分けて処理

path_to_linux関数の目的は、引数として渡されるWindowsのパスをLinuxで利用できるパスに変換するというものだ。前回、ちょっとした拡張として、複数の引数を取れるようにしていた。

macOSやLinuxで使われるコマンドの引数には絶対的な決まりというものがない。「慣例的にこういった感じになっていることが多い」とは言えるのだが、そこに強制力はない。そのため、全てのコマンドに対して適切なパースを行って処理するような変換関数を書くことはできない。厳密にやろうとすれば、全てのコマンドに対して個別にパーサを書くか、全てのコマンドに対して個別にパーサ向けのルールを書く必要がある。インタラクティブシェルの入力補完機能などはこれに該当しており、コマンドごとに個別に補完ルールを書いたファイルが用意されていたりする。

しかし、それを自分で実装するのは手間だ。よく使うコマンドに対してのみ、それなりに動作するものを作っておくくらいでよいだろう。そこで、前回linux_to_path関数として作成したものを、次のように拡張し、_path_to_linux関数とした。

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
}

主な変更内容は次の通りだ。

  • 関数名称をpath_to_linuxから_path_to_linuxへ変更。この関数はユーザーが直接使用するタイプの関数ではなく、ほかの関数がパス変換用のツールとして利用するため、名称の前にアンダーバーを付けて区別する
  • 変換するドライブ名が「C:」に固定されていたので、これを「A」から「Z」まで使えるように変更
  • 引数が「-」または「+」から始まっていた場合にはパス変換の対象ではないものとして変換を行わないように変更
  • 引数が数値だった場合は自動的に数値型に変換されてしまい文字列変換ができなくなるので、文字列へ明示的にキャストしてから処理するように変更


WindowsではCドライブにシステム関係のファイルがデプロイされる仕組みになっている。Cドライブ以外のストレージに関してはそれぞれ状況に応じてDドライブ、Eドライブ、Fドライブ……といった具合にドライブ名が当てられるようになっている。前回の実装はCドライブ固定になっていたので、この部分をAからZまで使用できるように変更した。

該当する処理部分は以下の通りだ。まず引数がドライブ名で始まっているかどうかを正規表現で判定し、後は文字列変換を行ってLinuxで使用できるパスへ変換している。

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

macOSやLinuxのコマンドでは「-」から始まる引数はオプションとして指定されていることが多く、ここにパスが記載されることはまずない。オプションは「-」ではなく「+」で始まっていることもある。パスに関しても、パスそのものが「-」や「+」から始まっていることもあまりない(作れないわけではなく、そういった名前を付けることが少ない)。こうしたことから、「-」または「+」から始まっている文字列はそもそも変換の対象としないように以下の処理を追加した。

        # Option is not converted
        elseif ($winpath -match '^[-+]') {
            $linuxpath += $winpath
        }

コマンドによっては引数に数値を取るものがあるのだが、PowerShellではこの手のデータは自動的に数値型に変換されることになる。このため、.Replace()による文字列置換ができなくなる。そのため、_path_to_linux関数では次のように明示的に文字列にキャストしてから置換処理を行うように調整した。

        # Other argument is converted
        else {
            $linuxpath += ([String]$winpath).Replace('\','/')
        }

macOSやLinuxのコマンドでは「—」も特別な意味を持っていることが多い。「—」が指定された以降は、それが「-」や「+」から始まっていてもオプションとしては処理せず引数として処理するというものだ。これを関数でも処理してやりたいところだが、「$Args」を使う場合うまく処理できない。

これは、「—」がPowerShellにとって意味のあるものとして処理されるためだ。「—」を引数に渡しても$Argsに整理される段階で取り除かれてしまう。「—」も含めて処理しようとすると、$Argsへ分関する段階の処理に関しても自前で実装しなければならない。面倒なので、そこまでしなくてもよいと思う。

こうして新しく整理した_path_to_linux関数を試しに実行してみると、次のようになる。

_path_to_linux関数の使用例

引数に正規表現をとるタイプのコマンドだと問題が発生することがあるのだが、それ以外のコマンドであれば引数を_path_to_linux関数でまるごと処理させることでそこそこ使える感じにはなる。

grep関数では独自にオプション解析パーサを実装

grepコマンドの引数は次のような感じで指定できるようになっている。_path_to_linux関数でも処理できるケースであれば、誤ってパターンに対してパス変換をかけてしまうことがあり得る。

◆grepコマンドの指定例 - macOS (BSD General Commands Manual)

     grep [-abcdDEFGHhIiJLlmnOopqRSsUVvwxZ] [-A num] [-B num] [-C[num]]
          [-e pattern] [-f file] [--binary-files=value] [--color[=when]]
          [--colour[=when]] [--context[=num]] [--label] [--line-buffered]
          [--null] [pattern] [file ...]

あまり厳密にこの辺りを実装しても、労力の割にはそれほど利益がない。だが、grepコマンドは頻用するコマンドの1つだしパターンがパス変換されるとすごく面倒なので、grepに対しては専用に引数のパース処理をしながらパスだけをピンポイントに変換するよう、次のような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
}

今回はgrep関数について詳しい説明は行わないが、要するにgrepコマンドの引数に合わせ、パスだけを見つけてピンポイントで_path_to_linux関数でパス変換している。どういったgrepコマンドが実行されることになるか出力するように、ちょっと変更した状態の実行例を次に記載する。

grep関数の動作の様子

grep関数の処理はもうちょっと汎用的に広げて整理していけば、”abcdDEFGHhIiJLlmnOopqRSsUVvwxZ” “ABCef” “f” (引数なしオプション、引数ありオプション、引数ありオプションでパス変換するオプション)のような引数を与えて自動的にパースして変換するような処理を実装することもできる。そこまでする必要はないと思うが、PowerShell関数を組む課題としては実用的で面白いかもしれない。

必要に応じて徐々にカスタマイズ

インタラクティブシェルとしてPowerShellを使っていく上では$PROFILEをいかにクリーンでかつ役に立つ状態にキープするかが大切になってくる。あまりこだわりすぎると肥大化してメンテナンスできなくなるので、ほどよいところで留めておくほうがよいと思う。

そしてこれも繰り返し書いているが、やりたいことに合わせて細かく調整していくことがポイントだ。不便だと思ったら、$PROFILEをカスタマイズする。そうした作業を繰り返していくことで自分だけの使いやすい環境が出来上がっていく。ストレスフリーな環境というのは、毎日作業をしていく上でとても大切である。