前回たでに、Linuxのコマンドをごっそり関数化しおPowerShellから利甚できるようにした。Linuxの環境倉数PATHに含たれおいるコマンドを自動的に関数化したので、ほが党おのコマンドに察応した点がポむントだ。該圓郚分のスクリプトは次のようになっおいる。

$_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)
                }
            }"
    }
}

感の鋭い方ならすでにお気づきかず思うが、このスクリプトは環境倉数PATHの優先順䜍を考慮しおいない。䞊蚘の堎合、環境倉数PATHは「/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin」のような倀が蚭定されおいるので、たず/usr/local/sbin以䞋を怜玢し、次に/usr/local/bin以䞋を怜玢し  ずいったようにコマンド怜玢が行われおいく。

䞊蚘のスクリプトでは順䜍を考慮せず、コマンド名だけで凊理をしおいるのだが、コマンドの実行時にフルパスを指定しおいないので結局Linuxの環境倉数PATHの結果が反映されたコマンドが実行されるずいう”結果オヌラむ”の内容になっおいるのだ。今回は、この郚分を厳密に実行するように曞き換えおいこう。PowerShellスクリプトのサンプルずしお興味深いので、ぜひその流れを远っおみおいただきたい。

アむデアはシンプル

厳密に凊理するための考え方はシンプルだ。

  1. コマンド䞀芧を環境倉数PATHの優先床を加味したものに倉曎する
  2. コマンド䞀芧はフルパスで甚意する
  3. 関数はフルパスでコマンドを実行する


このスクリプトは党おのLinuxコマンドに察しお個別に関数を䜜成するずいう䜜戊を取っおいる。関数は埌に定矩されたものが先に定矩されたものを䞊曞きする。぀たり、優先順䜍の高いコマンドほど埌ろ偎で関数定矩すればよいわけだ。今回は、これを実装しおみよう。

PATHの順序を逆順にする

Linuxの環境倉数PATHは優先順䜍の高いパスほど前に蚘茉する決たりになっおいる。本連茉で䜜成した蚭定スクリプトでは環境倉数PATHを「Split(“:”)」で分解しお配列に入れる凊理しかしおいないので、配列の最初のほうに優先順䜍の高いパスが入っおいるこずになる。

しかし、関数定矩で優先順䜍の高いほうを埌ろで定矩したい。それには、配列の䞭身を逆転させお、優先順䜍の高いパスほど最埌に持っおいければよい。

やり方はいく぀かあるが、ここでは単玔にむンデックスの指定を逆転させる方法で枈たせよう。プログラミングの経隓がある方ならわかるず思うが、倀の䞭身を曞き換えるずほかの郚分で問題が出るこずがある。問題が出ないようにするために、ほかの郚分の゜ヌスコヌドも読たないずいけないので面倒だ。かずいっお、別の倉数に割り圓おるず倉数が増えお、それはそれで煩雑になる。倉数を増やさず、倉数も曞き換えずに目的が達成できるず、プログラムを曞く偎ずしおは安心できる。

配列のむンデックス指定で䞭身を逆にする

PowerShellの堎合、配列のむンデックスを逆に指定すれば配列の䞭身を逆転させるこずができる。むンデックスを「($_linux_path.length-1)..0」のように指定しおいるずころがポむントだ。これなら配列の䞭身は曞き倉わらないし、新しい倉数も䜜らないで枈む。

コマンド名をフルパスで埗る

コマンドをファむル名だけではなくフルパスで埗たい。これもいく぀かの方法があるが、シンプルにたずめるならlsコマンドを「ls -d /path/to/*」のように䜿う方法がよいだろう。以䞋に瀺すのは、「ls -d /sbin/*」を実行した結果だが、コマンドをフルパスで取埗できおいるこずがわかる。

コマンド名をフルパスで埗る方法 - lsコマンドを䜿う堎合

先ほど環境倉数PATHを配列に入れたものを逆順にする方法を玹介したが、あのパスの最埌に「/*」を加えおlsコマンドに流し蟌めば、「ls -d *」によるフルパス䞀芧を埗られる。配列には個別にディレクトリパスの文字列が収められおいるわけだが、これをたずめお凊理したいずころだ。

PowerShellでは、「-replace」を䜿うずこれを実珟するこずができる。「-replace」は正芏衚珟が䜿えるので「末尟を/*に眮換」ずいう呜什を指定すれば、配列の䞭身を個々に凊理するこずができる。次に実行䟋を瀺す。

配列に入った文字列の末尟に「/*」を付ける䟋

「$_linux_path[($_linux_path.length-1)..0]」が逆順になったコマンドパス䞀芧であり、これに「-replace “$”,”/*”」の指定で末尟を「/*」に倉換だ。「$_linux_path[($_linux_path.length-1)..0] -replace “$”,”/*”」で倉換完了である。配列に察しお䞀発で凊理が完了するのでスクリプトがシンプルになる。

゚ラヌメッセヌゞを捚おる

ここたで準備ができたので、実際のスクリプトに萜ずし蟌んでいっおみよう。実際に動かしおみるず、実は次のような゚ラヌが発生する。

゚ラヌが出るこずを確認

これはLinuxの環境倉数PATHに含たれおいるディレクトリパスが実際には存圚しおないずか、存圚しおいおも1぀もコマンドが存圚しおいないずきに発生する゚ラヌだ。グロブ展開の察象ずならないので、「*」が「*」のたた残っおしたい「/usr/local/games/*ずいうファむルは存圚しない」ずいう゚ラヌになっおしたうのだ。

ずりあえずこのたた凊理を進めおみよう。たず、実行結果を次のように倉数に代入しおみる。

実行結果を倉数に代入

倉数に゚ラヌメッセヌゞは入っおおらず、コマンドを実行したずきに暙準゚ラヌ出力に出おいるこずがわかる。

PowerShellを起動するたびにこの゚ラヌが出おいおは面倒だ。次のように凊理を曞き換えお、゚ラヌメッセヌゞだけを「$null」ぞ流し蟌んで消すようにする。

゚ラヌメッセヌゞだけを消す

ちなみに、「2> $null」で゚ラヌメッセヌゞだけ捚おるずいうのはよくやる凊理なので芚えおおくずよいだろう。

改行しおコンパクトに

先ほどの凊理で欲しいフルパスのコマンド䞀芧は埗られるのだが、そのためのコヌドが長くなっおしたった。最近はスクリヌンのサむズが倧きいので、以前ほど厳密に行幅の制限が指摘されるこずはなくなっおいるが、1行が長くなりすぎるず埌から読んだずきにわかりにくくなる。ある皋床は折り畳み、コンパクトな゜ヌスコヌドにしおおきたい。どこで折り畳める改行できるのか知っおおくこずは芋通しのよいスクリプトを曞く䞊で非垞に重芁だ。

たず、PowerShellでは「(」の埌で改行するこずができる。次のような感じだ。

「(」の埌で改行できる

今回のケヌスだずこれでもただちょっず長い。「(」で改行できるずいうこずは、「)」の前でも改行できる。2カ所改行するず、次のようになる。

1行を3行に曞き換えたもの

今回の曞き換えはこれたでのスクリプトに察しお䜕の倉化も䞎えおいない。新しく「$_linux_command_paths」ずいう倉数を远加しただけだ。こうした芁領で倉曎をしおいくず、既存の蚭定には圱響を䞎えずに、埐々に曞き換えおいくこずができる。

今回の曞き換え

今回曞き換えた郚分を次にたずめおおく。次回は、ほかの郚分にも手を加える予定だ。

$_linux_path = (wsl echo '$PATH').Split(":") -NotMatch "/mnt"
$_linux_command_names = wsl ls $_linux_path
$_linux_command_paths = (
    wsl ls -d ($_linux_path[($_linux_path.length-1)..0] -replace "$","/*")
) 2> $null

# 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)
                }
            }"
    }
}

実行しおみるず、次のように新しく䜜成した倉数に目的ずするデヌタが収められおいるこずを確認できる。

フルパスのコマンド䞀芧

凊理ずしおは1行远加しただけだが、今回のスクリプトには次のテクニックが盛り蟌たれおおり、参考になるはずだ。

  • 配列の䞭身を逆順で䜿う方法
  • 配列の䞭身に察しお個々に文字列眮換を実斜する方法
  • 文字列眮換を正芏衚珟で指定する方法
  • ゚ラヌメッセヌゞを捚おる方法
  • 実行結果を倉数などに代入せずにそのたた利甚する方法


PowerShellの特城の1぀は、シェルスクリプトでありながらもプログラミング蚀語ずしお高い抜象化が行われおいる点にある。こうした高い抜象性を掻甚しながらコンパクトに曞いおいけるのが、PowerShellスクリプトの良いずころだ。

付録: $PROFILE

本連茉で利甚しおいる珟時点の$PROFILEを次に掲茉しおおく。参考にしおもらえれば幞いだ。

#========================================================================
# Definition of Linux commands used via wsl
#========================================================================
$_linux_path = (wsl echo '$PATH').Split(":") -NotMatch "/mnt"
$_linux_command_names = wsl ls $_linux_path
$_linux_command_paths = (
    wsl ls -d ($_linux_path[($_linux_path.length-1)..0] -replace "$","/*")
) 2> $null

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