PowerShell 7をインタラクティブシェルとして使う - コマンド関数定義を自動化

【連載】

PowerShell Core入門 - 基本コマンドの使い方

【第111回】PowerShell 7をインタラクティブシェルとして使う - コマンド関数定義を自動化

[2020/08/14 08:00]後藤大地 ブックマーク ブックマーク

関数定義を自動化しよう

前回までに、Windows 10でLinuxコマンドを利用するための関数を定義する方法を取り上げた。そこで解説したように、Windows 10のパスをLinuxのパスに変換することで、まるでネイティブなWindows 10のコマンドのようにLinuxのコマンドを利用することができる。

パスの変換を行うユーザー定義関数_path_to_linuxを使った関数は次の通りだ。使いたいLinuxコマンドが出てきたら、この部分を追加していけばよい。

function less {
    wsl less $(_path_to_linux $Args)
}
function lv {
    wsl lv $(_path_to_linux $Args)
}
function vi {
    wsl vi $(_path_to_linux $Args)
}
function vim {
    wsl vim $(_path_to_linux $Args)
}
function nvim {
    wsl nvim $(_path_to_linux $Args)
}
function tree {
    wsl tree $(_path_to_linux $Args)
}
function git {
    wsl git $(_path_to_linux $Args)
}

使いたいコマンドが少ないうちはこれでよいのだが、増えてくるとこのコードが何百行にも及ぶようになる。シンプルなものなのでそれでも良いのだが、今回はこの部分のコードを少し書き換えて、自動的に関数定義を行うように変更する。いくつかのTipsを紹介することになるので、PowerShellスクリプトの使い方としても面白いサンプルではないかと思う。

関数定義を自動化する方法

まず、次のような配列を定義することを考える。「使いたいLinuxコマンドが増えたら、ここにそのコマンド名を追加すれば完了」というかたちにしたい。この配列から自動的に関数を定義していくわけだ。

$_linux_command_names = @('less', 'lv', 'vi', 'vim', 'nvim', 'tree', 'git')

PowerShellでは配列名に変数を使うことができない。ここでは次の方法で関数を作成し、PowerShellに反映させる方法を取る。

  1. 関数定義をファイルに書き出す
  2. ファイルを読み込む


PowerShellではほかのシェルと同じように「. ファイルパス.ps1」のようにすることで、指定したファイルの中身を読み込んで処理することができるので、この特性を利用する。関数定義をファイルに書き出し、それを読み込むことで関数を定義させるわけだ。これなら動的に関数を定義していくことができる。

先ほどの処理をもう少し詳しく書くと、次のようになる。

  1. 関数定義を変数に保存する
  2. 一時ファイルを作成する
  3. 一時ファイルに関数定義を書き出す
  4. 一時ファイルを読み込んで処理する
  5. 一時ファイルを削除する
  6. 作業に用いた不要な変数を削除する


一時ファイルは拡張子が「ps1」である必要があるのだが、PowerShellが標準で提供している一時ファイル作成用のコマンドレットでは拡張子を指定することができない。このため、次のように手順を変更し、PowerShellの一時ファイル作成用コマンドレットを使いつつ、拡張子が「ps1」のファイルを用意して利用するようにする。

これを加味して手順を整理すると次のようになる。

  1. 関数定義を変数に保存する
  2. 一時ファイル1を作成する
  3. 一時ファイル1に拡張子「ps1」を付けた一時ファイル2を作成する
  4. 一時ファイル1を削除する
  5. 一時ファイル2に関数定義を書き出す
  6. 一時ファイル2を読み込んで処理する
  7. 一時ファイル2を削除する
  8. 作業に用いた不要な変数を削除する


それでは上記手順を実現するスクリプトを見ていこう。まず、次のように変数に関数定義を割り当てていく。

ForEach($n in $_linux_command_names) {
    $_linux_functions += "
        function $n {
            wsl $n `$(_path_to_linux `$Args)
        }"
}

上記コードの特徴は、変数に保存する段階で「$(ドル記号)」そのものを含める必要があるため、ドル記号の前にバッククォートを書いていることだ。バッククォートを書くとその後のドル記号は変数を意味せず、ただの文字としてのドル記号として処理される。これで変数に保存する文字列にドル記号を含めることができる。

_path_to_linux関数は上記手順で読み込ませても$PROFILEにそのまま書いておいてもどちらでもよいのだが、ここではヒアドキュメントの使い方を示す例として上記と同じ変数に含めておく。次のように記述しておけばよい。

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

ここではヒアドキュメントとして「@’」と「’@」で囲むことがポイントだ。「@’」と「’@」で囲むとドル記号が文字として解釈されるので、前述したようにバッククォートでエスケープする必要がなくなり、記述が煩雑にならなくて済む。なお、「@”」と「”@」で囲むとドル機能は変数として展開される。今回は「@’」と「’@」が適切だ。

生成されたファイルのフルパスを取得し、これに拡張子「ps1」を加えて関数定義を書き出すファイルのパスを作る。一時ファイルは、次のようにNew-TemporaryFileコマンドレットで作成する。

$_temp = New-TemporaryFile
$_temp_ps1 = $_temp.FullName + ".ps1"
Remove-Item $_temp

関数定義を上記処理で生成したパスへ書き出し、「.」で読み込んで関数定義を実施する。

$_linux_functions | Out-File $_temp_ps1
. $_temp_ps1
Remove-Item $_temp_ps1

この段階でやりたかったことはできたので、不要な変数を削除してここの処理を完了とする。

Remove-Variable _temp
Remove-Variable _temp_ps1

上記流れを1つのスクリプトにまとめると次のようになる。

#========================================================================
# Linux command definition used via wsl
#========================================================================
$_linux_command_names = @('less', 'lv', 'vi', 'vim', 'nvim', 'tree', 'git')

# Generate Linux command function
ForEach($n in $_linux_command_names) {
    $_linux_functions += "
        function $n {
            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

長くなったように見えるかもしれないが、該当部分のソースコード量はそれほど変わっていない。今後はコマンドを追加してもこの部分はほとんど長くならない。定義する関数を変更したくなった場合も、1カ所書き換えるだけで全ての関数定義を変更できるので、何かと便利だ。

実行サンプル

今回のサンプルではLinuxコマンドと関数定義についてはそのまま変数に残してある。PowerShell起動後に次のようにそれらを表示させることができる。

$PROFILEで定義された変数を確認

PowerShellの関数を次のように表示させることで、LinuxコマンドがPowerShellの関数として定義されていることも確認できる。

LinuxコマンドがPowerShellコマンドとして定義されていることを確認

なかなか汎用的に使える状態になってきたのではないだろうか。

付録: $PROFILE

本連載時点での$PROFILEを次に掲載しておく。参考にしてもらえれば幸いだ。

#========================================================================
# Linux command definition used via wsl
#========================================================================
$_linux_command_names = @('less', 'lv', 'vi', 'vim', 'nvim', 'tree', 'git')

# Generate Linux command function
ForEach($n in $_linux_command_names) {
    $_linux_functions += "
        function $n {
            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

# Individual Linux command function definition
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
}

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

#========================================================================
# Short name definition
#========================================================================
function ll { Get-ChildItem -Force }
function la { Get-ChildItem -Force }

※ 本記事は掲載時点の情報であり、最新のものとは異なる場合がございます。予めご了承ください。

一覧はこちら

連載目次

もっと知りたい!こちらもオススメ

【連載】RPA入門 - ツールで学ぶ活用シーン

【連載】RPA入門 - ツールで学ぶ活用シーン

AIには、ルールベース、機械学習、深層学習(ディープラーニング)の3つのレベルがあり、レベルが上がるに連れてより高度な人工知能を実現しますが、AIのスピンオフという位置付けで、Digital Labor(仮想知的労働者)によるホワイトカラー業務の自動化を実現するRPAが注目されています。

関連リンク

この記事に興味を持ったら"いいね!"を Click
Facebook で IT Search+ の人気記事をお届けします
注目の特集/連載
[解説動画] Googleアナリティクス分析&活用講座 - Webサイト改善の正しい考え方
[解説動画] 個人の業務効率化術 - 短時間集中はこうして作る
ミッションステートメント
教えてカナコさん! これならわかるAI入門
AWSではじめる機械学習 ~サービスを知り、実装を学ぶ~
対話システムをつくろう! Python超入門
Kubernetes入門
SAFeでつくる「DXに強い組織」~企業の課題を解決する13のアプローチ~
PowerShell Core入門
AWSで作るマイクロサービス
マイナビニュース スペシャルセミナー 講演レポート/当日講演資料 まとめ
セキュリティアワード特設ページ

一覧はこちら

今注目のIT用語の意味を事典でチェック!

一覧はこちら

会員登録(無料)

ページの先頭に戻る