Linuxにおいてページャは頻用するコマンドの一つだ。システムに最初からインストールされているページャは、「less」であることが多い。そのため、多くの方はlessコマンドが手に馴染んでいるのではないだろうか。Linux登場以前からUNIX系OSに慣れているならmoreコマンドのほうが馴染みがあるかもしれない。

また、日本語化や多言語化が進む時期にLinuxなどを使っていた場合、文字コード周りで問題が出にくい「lv」というページャを愛用していた方も多いはずだ。当然、lvは今も存在しており、次のようにaptコマンド経由でインストールすることができる。

lvをインストール

lvを使うと、無難に中身を閲覧することができる。

しかし、wsl経由でページャを使うのは一筋縄ではいかないことをこれまでに何度か取り上げてきた。そのため、本連載ではless専用に関数(less関数)を作成し、パイプがある場合とない場合を切り分け、パイプがある場合には一時ファイルを作成するなどしてページャが動作するようにしてきた。

lvのようなほかのページャを使う場合、lessを使えるようにしたときと全く同じことをすればよい。しかし、それでは使用するページャが増えるごとにless関数と同じサイズの関数を$PROFILEに追加しなければならない。これだと$PROFILEがどんどん大きくなってしまう。それは避けたいところだ。そこで今回は、現在のless関数を汎用的なものに変更し、less関数やlv関数からは汎用関数を呼び出すようにして小さい$PROFILEを維持していこう。

less関数を汎用関数へ書き換え

まず、前回までに整えた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関数のなかに「wsl less」という処理がある。この部分がページャをlessに固定している部分だ。ここを「wsl $pager」のように抽象化して、$pagerの中身をlessやlvにすれば、汎用的な関数として使用できるはずである。ということで、さっそく書き換えた関数(linuxpager)を次に示す。

function _linux_pager {
    # The first argument is the pager command name
    $pager = $Args[0]

    # Remove pager command from arguments to pass arguments to pager
    $Args = $Args[1..($Args.Length - 1)]

    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 $pager $(_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 $pager $(_path_to_linux $Args).Split(' ')
    }
}

まず、関数名をlessから「_linux_pager」へと変更した。ユーザーが直接実行するタイプの関数ではないので、名前の最初にアンダーバーを付けてある。

function _linux_pager {

この関数は「_linux_pager ページャコマンド名 ページャコマンドに渡す引数…」のように使おうと思う。このため、まず引数の1つ目をページャコマンド名として次のように$pagerへ代入しておく。

    $pager = $Args[0]

第2引数以降がページャコマンドに渡す引数なので、第1引数を削除しておきたい。この場合は次のように配列の1つ目を抜かした配列を作成して上書きすればよい。

    $Args = $Args[1..($Args.Length - 1)]

ここまで準備したら、あとはlessコマンドを直接実行していた部分を、次のように$pagerを使って実行するように書き換える。

◆wsl lessをwsl $pagerへ書き換え(その1)

        wsl $pager $(_path_to_linux $Args).Split(' ') $(_path_to_linux $_temp.ToString()).Split(' ')

◆wsl lessをwsl $pagerへ書き換え(その2)

        wsl $pager $(_path_to_linux $Args).Split(' ')

これで_linux_pager関数は完了だ。次に、この関数を利用する新しいless関数とlv関数を用意する。

function less { _linux_pager less $Args.Split(' ') }
function lv   { _linux_pager lv   $Args.Split(' ') }

less関数とlv関数のポイントは、「$Args」を「$Args.Split(’ ‘)」の形式で引数として渡している点にある。こうしないと思ったように機能しない。この辺りは、これまでのless関数を作成してきたテクニックそのままだ。実は上記関数だけでは問題があるのだが、とりあえず動作するものとしてはこんなところでいいだろう。

使ってみる

書き換えたlessコマンドを実行すると次のようになる。想定していたように動作していることを確認できる。

lessの実行前

lessを実行

同じようにlvコマンドを実行すると次のようになる。

lvの実行前

lvを実行

lessもlvも実態としては_linux_pager関数が動作している。これなら新しいページャを使いたくなったら、less関数やlv関数だけをコピーすればよい。この部分はもう少し整理することもできるので、それに関しては次回以降で取り上げようと思う。

毎回の書き換えはちょっとした内容だが、積もり積もるとPowerShellとWSLをシームレスに統合しているような状態になってくる。最初に作成した$PROFILEと比較すると、今の$PROFILEはかなりいい感じに仕上がってきていて、PowerShellでLinuxコマンドを実行することに違和感がなくなりつつある。かなり便利になった感じだ。

付録: $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
}

# pager
function _linux_pager {
    # The first argument is the pager command name
    $pager = $Args[0]

    # Remove pager command from arguments to pass arguments to pager
    $Args = $Args[1..($Args.Length - 1)]

    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 $pager $(_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 $pager $(_path_to_linux $Args).Split(' ')
    }
}
function less { _linux_pager less $Args.Split(' ') }
function lv   { _linux_pager lv   $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