前回、Linuxのlessコマンドを取り䞊げ、コマンド単䜓で䜿う堎合パむプラむンに接続しお䜿う堎合のどちらでもきちんず動䜜するように蚭定した。パむプラむンを流れおくるデヌタをいったんファむルに曞き蟌むこずで、パむプが䜿われた堎合もファむルを指定しおLinuxのlessコマンドを実行するように倉曎したのだ。これで、パむプラむンを䜿った堎合に発生するいく぀かの問題を回避できるようになった。

しかし、しばらく䜿っおみるず、less以倖のコマンドでほかの䞍具合があるこずがわかった。匕数が1぀しかない堎合は問題ないのだが、2぀以䞊の匕数を取るケヌスでは期埅する動䜜をしないのだ。䟋えば、Ubuntuなどの管理に欠かせないaptコマンドは2぀以䞊の匕数を䌎うこずがあるが、その堎合、゚ラヌが発生しお動䜜しない。しかし、同じこずをwslコマンドを䜿っお実行するず、正しく動䜜するのである。

aptコマンドで匕数が2぀以䞊になるず゚ラヌが発生しお動䜜しないが、同じこずをwslコマンドで実行するず動䜜する

぀たり、以前$PROFILEに远加したLinuxコマンドを党郚関数化する凊理のどこかで䞍具合が発生しおいるこずになる。

該圓する郚分は次の凊理だ。

ForEach($n in $_linux_command_paths) {
    $_n = (Split-Path -Leaf $n)
    $_linux_functions += "
        function $_n {
            if (`$Input.Length) {
                `$Input.Reset()
                `$Input | wsl $n `$(_path_to_linux `$Args)
            }
            else {
                wsl $n `$(_path_to_linux `$Args)
            }
        }"
}

この凊理ではwslコマンドの蚘述が2カ所ある。次の郚分だ。

◆wslを実行しおいる郚分 - コマンド単䜓眮き換え

wsl $n `$(_path_to_linux `$Args)

◆wslを実行しおいる郚分 - パむプラむンに接続しおいるコマンド眮き換え

`$Input | wsl $n `$(_path_to_linux `$Args)

この郚分は「関数化コヌドをいったんファむルに曞き出しおから読み蟌む」ずいう目的のために蚘述されおいるので、゚スケヌプコヌドが入っおおり、若干わかりづらいかもしれない。゚スケヌプ郚分を陀くず、次のように蚘述しおいるこずになる。

◆wslを実行しおいる郚分 - コマンド単䜓眮き換え

wsl $n $(_path_to_linux $Args)

◆wslを実行しおいる郚分 - パむプラむンに接続しおいるコマンド眮き換え

$Input | wsl $n $(_path_to_linux $Args)

冒頭に瀺した動䜜䟋ず比范するず、「$Args」を「_path_to_linux()」で凊理した結果に眮き換えおいる郚分に違いがある。この郚分は「$()」による郚分匏挔算子を䜿っおいるのだが、この凊理が盎接wslコマンドを䜿ったケヌスずは違うのだ。

マむクロ゜フトのドキュメントで郚分匏挔算子$(): Subexpression operatorの説明「about_Operators - PowerShell | Microsoft Docs」をよく読んでみるず、どうもこの挔算子の結果はここでは単䞀の文字列ずしお返っおきおいるようだ。぀たり、耇数の匕数が指定されおいたずしおも、郚分匏挔算子を経由した段階で単䞀の文字列になっおしたっおいるのである。

about_Operators - PowerShell | Microsoft Docs

結果ずしお、冒頭の実行䟋では「apt list less」ずいう指定が「aptコマンド」ず「list」「less」ずいう2぀の匕数ではなく、「apt list less」ずいう1぀のコマンドだず芋なされおいるこずになる。これでは期埅したように動䜜しないのももっずもだ。

同じ問題で動䜜しおいない蚭定がほかにもあり

同じような蚘述をしおいる箇所はほかにもある。䟋えば、「ls」や「ll」などは次のような蚭定をしおいるため、こちらも耇数の匕数が単䞀の文字列だず芋なされおいるこずになる。

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

実行しおみるず、次のように匕数が2぀以䞊になるず゚ラヌが発生するこずを確認できる。

匕数2぀以䞊で゚ラヌを確認

前回䜜成したless関数にも同じ蚘述があるので、同じ問題が起きる。

#less
function less {
    if ($Input.Length) {
        $Input.Reset()

        # Prepare temporary file path
        $_temp = New-TemporaryFile

        # Write data from pipeline to the temporary file
        $Input | Out-File $_temp

        # Do less
        wsl less $(_path_to_linux $_temp.ToString())

        # Delete unnecessary temporary file and variable
        Remove-Item $_temp
        Remove-Variable _temp
    }
    else {
        wsl less $(_path_to_linux $Args)
    }
}

しかも蚭定をよく芋盎しおみるず、パむプラむンが接続された堎合の動䜜では、lessコマンドに指定された匕数がそもそも無芖されおいるこずもわかった。これは今回ずは別のミスだ。前回の曞き換えの際、匕数の凊理を加えるのを忘れおいる。

詊しに実行しおみるず、次のように゚ラヌが発生する。

匕数2぀以䞊の堎合やパむプラむンを䜿った際のバグを確認

問題郚分は明らかになったので、ここを修正しおいこう。

文字列を配列に倉換しおから凊理するこずで解決

耇数の匕数が単䞀の文字列ずしお扱われるこずで問題が発生しおいるのだから、次のように文字列になった埌で「.Split(’ ‘)」を䜿い、再床配列ぞ分解しおみる。

「.Split(’ ‘)」で配列に分解しお問題解決

するず、期埅通りに動䜜するようになった。今回は、この方法で修正しおいこう。

動䜜確認

該圓郚分を「.Split(’ ‘)」で倉換するように曞き換える。それぞれ次のように倉曎した。

◆Linuxコマンドを関数化する郚分修正埌

ForEach($n in $_linux_command_paths) {
    $_n = (Split-Path -Leaf $n)
    $_linux_functions += "
        function $_n {
            if (`$Input.Length) {
                `$Input.Reset()
                `$Input | wsl $n `$(_path_to_linux `$Args).Split(' ')
            }
            else {
                wsl $n `$(_path_to_linux `$Args).Split(' ')
            }
        }"
}

◆ls関数やll関数修正埌

function ls { wsl ls --color=auto $(_path_to_linux $Args).Split(' ') }
function ll { ls -l $(_path_to_linux $Args).Split(' ') }
function la { ls -a $(_path_to_linux $Args).Split(' ') }

◆less関数修正埌

#less
function less {
    if ($Input.Length) {
        $Input.Reset()

        # Prepare temporary file path
        $_temp = New-TemporaryFile

        # Write data from pipeline to the temporary file
        $Input | Out-File $_temp

        # Do less
        wsl less $(_path_to_linux $Args).Split(' ') $(_path_to_linux $_temp.ToString()).Split(' ')

        # Delete unnecessary temporary file and variable
        Remove-Item $_temp
        Remove-Variable _temp
    }
    else {
        wsl less $(_path_to_linux $Args).Split(' ')
    }
}

less関数に぀いおは、パむプラむンで凊理しおいる郚分にそもそも指定されおいなかった匕数を指定するずいう凊理も远加した。ここでも「.Split(’ ‘)」を指定し、同じ問題が発生しないようにしおいる。

これで、これたで発生しおいた゚ラヌを調べおみるず、党お解消されたこずを確認できる。

問題が解消された

これで耇数匕数も凊理できるようになった。本連茉で育おおいる$PROFILEは、PowrShellからWSLで動䜜するLinuxをかなりシヌムレスに実行できるものになり぀぀ある。

明日の”手抜き”を実珟するために

PowerShellはLinuxのシェルのような䜿い方ができるが、実際にはオブゞェクト指向ベヌスのプログラミング蚀語にシェルっぜいシンタックスを被せたような䜜りになっおおり、プログラミング蚀語ずしおの现かい芏則がたくさんある。もし少し凝ったこずをしたいのであれば、それらの芏則をきっちり把握しおおかなければならない。

ずはいえ、必芁なドキュメントはすでにむンタヌネット䞊で公開されおおり、ある皋床プログラミングの経隓があれば問題ないはずだ。匷力なプログラミング蚀語ずしおの偎面を備えおいるこずもあり、$PROFILEでかなり高床なこずたで蚭定できおしたう。最初は苊劎するかもしれないが、自分の甚途に応じお鍛えられた$PROFILEは必ず力になっおくれるはずだ。今日の努力は明日の”手抜き”に぀ながる。ぜひずもコツコツず$PROFILEを鍛えおいっおいただきたい。

付録: $PROFILE

珟時点での$PROFILEを以䞋に掲茉しおおく。

#========================================================================
# Set encoding to UTF-8
#========================================================================
# [System.Console]::OutputEncoding is set to local encoding, so character
# corruption occurs when piped from WSL to WSL. Therefore, set
# [System.Console]::OutputEncoding and $OutputEncoding to UTF-8 to avoid
# the problem.
$OutputEncoding = [System.Console]::OutputEncoding =
    [System.Text.UTF8Encoding]::new()

#========================================================================
# Definition of Linux commands used via wsl
#========================================================================
$_linux_path = (wsl echo '$PATH').Split(":") -NotMatch "/mnt"
$_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_paths) {
    $_n = (Split-Path -Leaf $n)
    $_linux_functions += "
        function $_n {
            if (`$Input.Length) {
                `$Input.Reset()
                `$Input | wsl $n `$(_path_to_linux `$Args).Split(' ')
            }
            else {
                wsl $n `$(_path_to_linux `$Args).Split(' ')
            }
        }"
}
Remove-Variable n
Remove-Variable _n

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

#less
function less {
    if ($Input.Length) {
        $Input.Reset()

        # Prepare temporary file path
        $_temp = New-TemporaryFile

        # Write data from pipeline to the temporary file
        $Input | Out-File $_temp

        # Do less
        wsl less $(_path_to_linux $Args).Split(' ') $(_path_to_linux $_temp.ToString()).Split(' ')

        # Delete unnecessary temporary file and variable
        Remove-Item $_temp
        Remove-Variable _temp
    }
    else {
        wsl less $(_path_to_linux $Args).Split(' ')
    }
}

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

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

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