前回までに作成した$PROFILEで、WSLで動作するLinuxのコマンドの大半をWindowsのPowerShellからシームレスに利用できるようになった。これはなかなか便利で、PowerShellを使いつつも、まるでネイティブコマンドのようにLinuxコマンドの大半を使用できる。普段LinuxやmacOSでターミナルを使っている方なら、この設定はかなり手になじむはずだ。

しかし、まだいくつか不満点が残る。基本的には前回までの設定でよいのだが、筆者としては、まだ多少の調整が必要だと思う。それぞれはささいな操作なのだが、これが放置されていると使うたびに実にイラッとするからだ。こうしたちょっとした部分を調整していくことで、PowerShellはさらに使いやすいものになる。では早速、調整の例を見ていこう。

「man」で「Get-Help」が呼び出される

LinuxやmacOSでは、manコマンドはオンラインマニュアルを表示させるコマンドだ。PowerShellにおける「Get-Help」に対応している。当然、「man man」のように実行したら、manのオンラインマニュアルが表示されることを期待する。

man manを実行する

しかし、実行結果は次のようになる。表示されるのはmanのオンラインマニュアルではなく、Get-Helpのヘルプだ。

「man man」でGet-Helpのヘルプが表示される

これは、以前lsコマンドを取り上げたときに説明したのと同じ理由だ。次のように、manはhelpへのエイリアスになっている。したがって、manを実行しても、実際に実行されるのはhelpであり、つまりGet-Helpが実行されるのだ。

manはhelpへのエイリアスになっている

manではLinuxのmanコマンドが実行されてほしい。この場合、manのエイリアスを削除すれば、Linux側のmanコマンドが実行されるようになる。lsのときと同じ要領で、manへのエイリアスを削除する設定を$PROFILEへ追加してみよう。

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

これにより、manを実行するとLinuxのmanコマンドが実行されるようになる。

manでLinuxのmanコマンドが実行されている

これでhelpでWindows側のヘルプ、manでLinuxのオンラインマニュアルが表示されるようになる。

「| less」でまともに操作できない

これまでに$PROFILEに追加してきた設定で、Linuxのlessコマンドは使用できるようになっているはずだ。次のように引数にファイルを渡しせば、動作を確認することができる。

lessコマンドの実行サンプル

ファイルの閲覧とページングができる

だが、「| less」のようにパイプラインを経由しようとすると途端に機能しなくなる。

ファイルの中身をパイプライン経由でlessへ流し込む

パイプライン経由でlessにテキストデータを流し込むと、ページャとしては機能しない。次のスクリーンショットのようにlessの操作キーは全く使用できない。ただ1ページ分が表示されただけで処理が止まってしまう。

1ページ表示した後、操作不能になる

ここで実行できる操作は「Ctrl」+「C」だけなので、「Ctrl」+「C」で処理を止めることしかできない。

「Ctrl」+「C」で処理を停止することはできる

これは、パイプラインを接続したことでlessコマンドが操作用のキー入力すらパイプライン経由で取るようになってしまったことに原因がある。Linuxで直接lessコマンドを実行している場合には、こういったことは起こらない。操作用のキーはターミナルから取得するためだ。今のところ筆者が試した範囲では、wslコマンド経由でlessコマンドを実行するとこの部分を解決することができない。

「| less」でテキストデータやファイルの中身をページングしながら閲覧するのは結構頻度の高い作業なので、これができないとなるとそのストレスは非常に大きい。最適な解決方法がわからなかったので、本稿ではひとまず、その部分の処理をOut-Hostコマンドレットに置き換えることで対処したい。機能はlessよりも劣るが、使えないよりはよほどましという判断だ。

Linuxコマンドを関数に置き換えて定義した後で、次のようにless関数を定義する。引数としてファイルパスがある場合には「wsl less」を実行し、パイプライン経由の場合には「Out-Host -Paging」の処理を実行するようにしている。

function less {
    if ($Input.Length) {
        $Input.Reset()
        $Input | Out-Host -Paging
    }
    else {
        wsl less $(_path_to_linux $Args)
    }
}

次のようにファイルの中身を「| less」に流し込んでみる。

ファイルの中身を「| less」に流し込む

「Out-Host -Paging」が機能して、ページング処理が使用できることがわかる。

「| less」でページングが機能するようになったサンプル

提供される機能はlessではなく「Out-Host -Paging」であるため、検索はできないし、使い勝手もそれほど良くはない。しかし、全く使えないよりはましだろう。最初から完璧な解決策は求めず、そのときできる範囲でさっと改善を繰り返す。これが$、PROFILEを育ててPowerShell環境を快適なものにしていくポイントだ。

今回の変更だけでも、かなりのストレス源を減らすことができるはずだ。”イラつきポイント”は設定でサクッと回避し、インタラクティブシェルとしてのPowerShellを便利なものにしていこう。

付録: $PROFILE

本連載時点での$PROFILEを次に掲載しておく。

# Copyright (c) 2020 Daichi GOTO <daichi@ongs.co.jp>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
# THE POSSIBILITY OF SUCH DAMAGE.

# author: Daichi GOTO (daichi@ongs.co.jp)
# first edition: Mon Jun 22 18:20:36 JST 2020

#========================================================================
# 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)
            }
            else {
                wsl $n `$(_path_to_linux `$Args)
            }
        }"
}
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()
        $Input | Out-Host -Paging
    }
    else {
        wsl less $(_path_to_linux $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 }
# man
Get-Alias man *> $null && Remove-Item alias:man

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