UNIX系のインタラクティブシェルでは、エイリアスでコマンドのショートカット利用のような設定を行う。よく使うコマンド、特にワンライナーで行うような処理に別名を付けておいて、その別名で一連のコマンドを実行させるわけだ。PowerShellにもエイリアスの機能はあるが、UNIX系インタラクティブシェルのエイリアスとは機能が異なるため、同じような使い方をすることはできない。PowerShellで同様の処理を行いたい場合は、関数で実装できる。今回はそうした使い方のサンプルを紹介する。

特定の場所に移動する

最もよく使われるであろう設定の1つがカレントディレクトリの移動だ。ディレクトリの移動は「cd」で行う。設定を行わなくてもcdにパスを指定すれば移動できるわけだが、これをショートカット設定しておくことで特定のディレクトリへの移動を簡素化する。

例えば、次の設定を見てみよう。これはWindowsユーザーにデフォルトで作成されるディレクトリへ移動するための関数だ。

function doc { cd ~/Documents/ }
function dwm { cd ~/Downloads/ }
function drv { cd ~/OneDrive/ }
function pic { cd ~/Pictures/ }
function vid { cd ~/Videos/ }

実行すると次のようになる。

カレントディレクトリを変更する関数を実行したサンプル

移動したら「dir」で内容を確認することが多いので、次のように移動後にdirを実行するようにしておいてもよいと思う。

function doc { cd ~/Documents/; dir }
function dwm { cd ~/Downloads/; dir }
function drv { cd ~/OneDrive/; dir }
function pic { cd ~/Pictures/; dir }
function vid { cd ~/Videos/; dir }

実行すると次のようになる。

カレントディレクトリを変更した後、dirを実行する関数を実行したサンプル

上記サンプルではWindowsで用意される”よくあるディレクトリ”へ移動しているが、実際には移動する場所を仕事や作業でよく使うディレクトリに設定するだろう。多くの場合、作業ディレクトリは、プロジェクトや業務に応じて固定化していることが多い。そのディレクトリへ2~3文字の関数の実行で移動できるようにすると便利だ。

よく使うコマンドをまとめる

こうした機能は、よく使うコマンドの入力を短縮する目的でもよく使われる。例えば、Gitでソースコードや設定ファイル、仕事で使うファイルの取得またはアップロードなどを実施しているというのであれば、gitコマンドの操作に別名を付けておくという方法もある。例えば、次のような感じだ。

gitコマンドを関数にまとめた関数

function g_pull { git pull }
function g_push { git commit -m 'updated'; git push origin master }
function g_add { git add }

実行すると次のようになる。

gitコマンドをまとめた関数の実行サンプル

どういったまとめ方をするかは使い方次弟だ。繰り返し同じコマンドを入力していることに気がついたら、その処理を関数にまとめて入力を効率化するのが時短につながる。

UNIX系のインタラクティブシェルは履歴検索機能や入力補完機能が強力なので、こうした短縮関数を定義しなくても履歴や補完機能経由で短縮入力が可能なことがある。しかし、PowerShellはその辺りのインタラクティブな操作がそれほど便利にできるようになっていない。関数で明示的に書いておいたほうが良いのではないかと思う。

なお、この手の設定は業務が変わったりすると全く使わなくなったりもするので、定期的にチェックして不要になったら削除するなどの手入れを行うのがよい。

よく使わないコマンドをまとめる

逆に、たまにしか使わないコマンドをメモしておく目的で関数に定義してしまう、というのもアリだ。例えば、最近ではSNSやオンラインショッピング、各種オンラインサービスで何かとアカウントの新規作成を求められることが多い。当然パスワードは強度の高いものを使う必要があるわけだが、多数のアカウントを持っている場合、毎回新しいパスワードを考えるのは骨が折れる。

opensslコマンドを使うと、パスワードのランダム生成などは比較的簡単にできる。「openssl rand -base64 20」のように実行すると、ランダムな20バイト分のデータがBASE64でエンコードされたデータが表示される。これは、結構パスワードに使いやすい。

しかし、時々しか使わないコマンドは忘れるものだ。こういった類のコマンドは関数として定義して別名にしておくとよい。サンプルとして、パスワードを生成する関数「getpass」を作ってみよう。

function getpass { openssl rand -base64 20 }

実行すると次のようになる。

実行するとエラーが発生

実行すると、上記スクリーンショットのようにエラーになる。実は本連載で作ってきたWindowsとLinuxのコマンドをシームレスに組み合わせるための設定に問題があるのだ。具体的には、次の部分でエラーが発生している。

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

「$(_path_to_linux $Args)」の結果がInt32として解釈されたことが問題になっている。今回のケースでは最後が20になるので、この部分で数値と判断されたようだ。数字に対して.Split()を実行しようとしているのでエラーになっているのである。

ということで、この部分を次のように変更する。

    $_linux_functions += "
        function $_n {
            if (`$Input.Length) {
                `$Input.Reset()
                `$Input | wsl $n ([String]`$(_path_to_linux `$Args)).Split(' ')
            }
            else {
                wsl $n ([String]`$(_path_to_linux `$Args)).Split(' ')
            }
        }"

「([String]$(_path_to_linux $Args)).Split(’ ‘)」のように、結果を文字列へ変換してから.Split()を実行するように変更した。これで、これまで通りに動作するはずだ。実行すると、次のようになる。

getpass関数の動作を確認

こんな感じでときどき使うので忘れがちなコマンドは関数に定義しておくとよい。関数名を忘れると思うが、その場合には$PROFILEを覗いてどんな関数名だったか思い出せばよい。2年とか3年とか使っていなかったコマンドを$PROFILEに関数として書いてあったので使い方を思い出したということも結構あるものだ。

こんな感じで$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 pagers
$_linux_pagers = @("less", "lv")

# Linux PATH and commands
$_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 commands functions
ForEach($n in $_linux_command_paths) {
    $_n = (Split-Path -Leaf $n)
    $_linux_functions += "
        function $_n {
            if (`$Input.Length) {
                `$Input.Reset()
                `$Input | wsl $n ([String]`$(_path_to_linux `$Args)).Split(' ')
            }
            else {
                wsl $n ([String]`$(_path_to_linux `$Args)).Split(' ')
            }
        }"
}

# Generate Linux pagers functions
ForEach($_n in $_linux_pagers) {
    $_linux_functions += "
        function $_n {
            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 $_n
                wsl $_n `$(_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 $_n `$(_path_to_linux `$Args).Split(' ')
            }
        }"
}

# Function that converts Windows paths to Linux paths
$_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 n
Remove-Variable _n
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
}

# 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

#========================================================================
# cd shortcuts
#========================================================================
function cd1 { cd (dir -Directory)[-1] }
function cd2 { cd (dir -Directory)[-2] }
function cd3 { cd (dir -Directory)[-3] }
function cd4 { cd (dir -Directory)[-4] }
function cd5 { cd (dir -Directory)[-5] }
function cd6 { cd (dir -Directory)[-6] }
function cd7 { cd (dir -Directory)[-7] }
function cd8 { cd (dir -Directory)[-8] }
function cd9 { cd (dir -Directory)[-9] }
function cd10 { cd (dir -Directory)[-10] }
function cd11 { cd (dir -Directory)[-11] }
function cd12 { cd (dir -Directory)[-12] }
function cd13 { cd (dir -Directory)[-13] }
function cd14 { cd (dir -Directory)[-14] }
function cd15 { cd (dir -Directory)[-15] }
function cd16 { cd (dir -Directory)[-16] }
function cd17 { cd (dir -Directory)[-17] }
function cd18 { cd (dir -Directory)[-18] }
function cd19 { cd (dir -Directory)[-19] }
function cd20 { cd (dir -Directory)[-20] }
#function cd1 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-1] }
#function cd2 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-2] }
#function cd3 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-3] }
#function cd4 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-4] }
#function cd5 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-5] }
#function cd6 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-6] }
#function cd7 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-7] }
#function cd8 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-8] }
#function cd9 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-9] }
#function cd10 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-10] }
#function cd11 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-11] }
#function cd12 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-12] }
#function cd13 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-13] }
#function cd14 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-14] }
#function cd15 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-15] }
#function cd16 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-16] }
#function cd17 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-17] }
#function cd18 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-18] }
#function cd19 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-19] }
#function cd20 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-20] }
function doc { cd ~/Documents/ }
function dwm { cd ~/Downloads/ }
function drv { cd ~/OneDrive/ }
function pic { cd ~/Pictures/ }
function vid { cd ~/Videos/ }

#========================================================================
# utilities shortcuts
#========================================================================
function g_pull { git pull }
function g_push { git commit -m 'updated'; git push origin master }
function g_add { git add }

function getpass { openssl rand -base64 20 }