Tags: PowerShell

PowerShell でスクリプトを書いたら引数バリデーションが便利だった

必要にかられて Windows 上で動かす簡単なスクリプトを書く際、せっかくなので PowerShell で書いたら引数バリデーションがかなり簡単に書けたのでメモしておく。

開発環境

  • Windows 10 Pro バージョン20H2
  • PowerShell 7.1.3

本文

まず公式サイトのリンクを貼っておく。必ず最新のドキュメントを読もう。

https://docs.microsoft.com/en-us/powershell/scripting/learn/ps101/09-functions?view=powershell-7.1

ここから実例を書いていく。

まず上記のドキュメントは関数について書いてあるので引数バリデーション等は関数でしか使えないのかというと、そうではない。
関数を使わなくても引数バリデーションは使える。
Linux でシェルを書いたり、Windows でバッチを書いたりするようにちょっとしたスクリプトを書くなら関数化しないほうが多いと思うので、そういう前提で説明していく。

例として、今から以下のようなスクリプトを書きたいとする。

とあるアプリの Git リポジトリを指定したブランチで git clone する。
スクリプト呼び出し時のコマンドライン引数でアプリ名を指定し、URL はスクリプト内で定義することにする。
アプリ名は “front” と “admin” の 2 種類だけに対応する。

env というリポジトリも指定したブランチで git clone する。(ブランチを指定しない場合は master ブランチとする。)

env リポジトリにある settings.yml を、アプリのリポジトリにコピーする。

これを PowerShell の利点をいかさず愚直に書くとたとえば次のようになると思う。

test.ps1

if ( [string]::IsNullorEmpty($Args[0]) ) {
    Write-Error -Message "アプリ名が指定されていません" -ErrorAction Stop
} else {
    $AppName = $Args[0]
}

if ( [string]::IsNullorEmpty($Args[1]) ) {
    Write-Error -Message "アプリのブランチ名が指定されていません" -ErrorAction Stop
} else {
    $AppGitBranch = $Args[1]
}

if ( [string]::IsNullorEmpty($Args[1]) ) {
    $EnvGitBranch = "master"
} else {
    $EnvGitBranch = $Args[2]
}


switch($AppName) {
    "front" {
        $Url = "https://front.git"
        break
    }
    "admin" {
        $Url = "https://admin.git"
    }
    default {
        Write-Error -Message "front と admin 以外は無効です" -ErrorAction Stop
    }
}

Write-Output -InputObject "clone 開始"
git clone -b $AppGitBranch --depth=1 $Url
git clone -b $EnvGitBranch --depth=1 "https://env.git"
Write-Output -InputObject "clone 完了"

Write-Output -InputObject "コピー開始"
Join-Path -Path "env" -ChildPath "settings.yml" | Copy-Item -Path {$_} -Destination $AppName
Write-Output -InputObject "コピー完了"

このスクリプトを実行すると次のように一応バリデーションもできていて問題は無い。

PS C:\Users\hoge\Desktop> .\test.ps1
Write-Error: アプリ名が指定されていません
PS C:\Users\hoge\Desktop> .\test.ps1 front
Write-Error: アプリのブランチ名が指定されていません
PS C:\Users\hoge\Desktop> .\test.ps1 fron a
Write-Error: front と admin 以外は無効です
PS C:\Users\hoge\Desktop> .\test.ps1 front feature/test
clone 開始
clone 完了
コピー開始
コピー完了
PS C:\Users\hoge\Desktop> .\test.ps1 front feature/test a
clone 開始
clone 完了
コピー開始
コピー完了
PS C:\Users\hoge\Desktop>

ただ PowerShell ならもっと簡潔に書ける。このスクリプトをちょっと PowerShell らしくすると次のように書き直せる。

test2.ps1

Param (
    [Parameter(Mandatory=$true, HelpMessage="アプリ名を front か admin で指定してください。")]
    [ValidateSet("front", "admin")]
    [string]$AppName,

    [Parameter(Mandatory=$true, HelpMessage="アプリのブランチ名を指定してください。")]
    [string]$AppGitBranch,

    [string]$EnvGitBranch="master"
)

switch($AppName) {
    "front" {
        $Url = "https://front.git"
        break
    }
    "admin" {
        $Url = "https://admin.git"
    }
}

Write-Output -InputObject "clone 開始"
git clone -b $AppGitBranch --depth=1 $Url
git clone -b $EnvGitBranch --depth=1 "https://env.git"
Write-Output -InputObject "clone 終了"

Write-Output -InputObject "コピー開始"
Join-Path -Path "env" -ChildPath "settings.yml" | Copy-Item -Path {$_} -Destination $AppName
Write-Output -InputObject "コピー完了"

引数バリデーションまわりがすっきりした。
見比べればわかると思うが解説すると以下のような感じ。

  • Parameter(Mandatory=$true): 必須入力を意味する
  • ValidateSet(“front”, “admin”): front か admin 以外は受け付けないことを意味する
  • [string]$EnvGitBranch=“master”: 文字列型の引数を受け付け、かつ指定がない場合は master を指定したことになる

ValidateSet によって $AppName については front, admin 以外の指定はエラーとなるため、$Url を定義するとき front, admin 以外を考慮する必要が亡くなった。

更にこれを引数なしで実行してみると、単にエラーになるのではなく必須入力の引数を対話で聞いてくれるようになる。

PS C:\Users\hoge\Desktop> .\test2.ps1

cmdlet test2.ps1 at command pipeline position 1
Supply values for the following parameters:
(Type !? for Help.)
AppName: 

Type !? for Help. というのはスクリプト内に書いた HelpMessage を表示するということで、言われた通り !? と打つとヘルプメッセージが出る。

PS C:\Users\hoge\Desktop> .\test2.ps1

cmdlet test2.ps1 at command pipeline position 1
Supply values for the following parameters:
(Type !? for Help.)
AppName: !?
アプリ名を front か admin で指定してください。
AppName

また呼び出し時に引数名を指定することで、引数の順番を自由にできる。

PS C:\Users\hoge\Desktop> .\test2.ps1 -AppGitBranch feature/test -EnvGitBranch feature/testenv -AppName front
clone 開始
clone 終了
コピー開始
コピー完了
PS C:\Users\hoge\Desktop>

ここから更に、“clone 開始” といったメッセージを通常は出力せず、スクリプトがうまく動かない等の場合に出力を指定した場合だけ出力するようにしたい。これも CmdletBinding というのを書くだけで簡単にできる。

test3.ps1

[CmdletBinding()]
Param (
    [Parameter(Mandatory=$true, HelpMessage="アプリ名を front か admin で指定してください。")]
    [ValidateSet("front", "admin")]
    [string]$AppName,

    [Parameter(Mandatory=$true, HelpMessage="アプリのブランチ名を指定してください。")]
    [string]$AppGitBranch,

    [string]$EnvGitBranch="master"
)

switch($AppName) {
    "front" {
        $Url = "https://front.git"
        break
    }
    "admin" {
        $Url = "https://admin.git"
    }
}

Write-Debug -Message "clone 開始"
git clone -b $AppGitBranch --depth=1 $Url
git clone -b $EnvGitBranch --depth=1 "https://env.git"
Write-Debug -Message "clone 終了"

Write-Debug -Message "コピー開始"
Join-Path -Path "env" -ChildPath "settings.yml" | Copy-Item -Path {$_} -Destination $AppName
Write-Debug -Message "コピー完了"

test2.ps1 と test3.ps1 の違いは

  • スクリプト冒頭に [CmdletBinding()] と書いたことと
  • “clone 開始” といったメッセージの出力を Write-Debug に変えたこと

である。

[CmdletBinding()] と書くことによって、下記 URL に載っているような Common Paramters というのが使えるようになる。

https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_commonparameters?view=powershell-7.1

Write-Debug というのは Common Parameters の一つである Debug オプションに対応した出力関数で、通常は “clone 開始” といったメッセージを出力せず、Debug オプションを設定した場合のみ出力するようにできる。

PS C:\Users\hoge\Desktop> .\test3.ps1 -AppGitBranch feature/test -EnvGitBranch feature/testenv -AppName front
PS C:\Users\hoge\Desktop> .\test3.ps1 -AppGitBranch feature/test -EnvGitBranch feature/testenv -AppName front -Debug
DEBUG: clone 開始
DEBUG: clone 終了
DEBUG: コピー開始
DEBUG: コピー完了
PS C:\Users\hoge\Desktop>

最後にせっかくなので Get-Help でヘルプを出せるようにしてみる。
書式は下記 URL に定められている通り。

https://docs.microsoft.com/en-us/powershell/scripting/developer/help/examples-of-comment-based-help?view=powershell-7.1

test4.ps1

<#
    .SYNOPSIS
    アプリのソースに settings.yml をコピー

    .DESCRIPTION
    指定アプリの Git リポジトリを指定したブランチで git clone する。
    env というリポジトリも指定したブランチで git clone する。(ブランチを指定しない場合は master ブランチとする。)
    env リポジトリにある settings.yml を、アプリのリポジトリにコピーする。

    .INPUTS
    なし

    .OUTPUTS
    なし

    .EXAMPLE
    PS>.\test3.ps1 admin feature/test

    .EXAMPLE
    PS>.\test4.ps1 -AppGitBranch feature/test -EnvGitBranch feature/testenv -AppName front
#>


[CmdletBinding()]
Param (
    [Parameter(Mandatory=$true, HelpMessage="アプリ名を front か admin で指定してください。")]
    [ValidateSet("front", "admin")]
    # アプリ名。front か admin。
    [string]$AppName,

    [Parameter(Mandatory=$true, HelpMessage="アプリのブランチ名を指定してください。")]
    # アプリのブランチ名。
    [string]$AppGitBranch,

    # env のブランチ名。未指定の場合は master。
    [string]$EnvGitBranch="master"
)

switch($AppName) {
    "front" {
        $Url = "https://front.git"
        break
    }
    "admin" {
        $Url = "https://admin.git"
    }
}

Write-Debug -Message "clone 開始"
git clone -b $AppGitBranch --depth=1 $Url
git clone -b $EnvGitBranch --depth=1 "https://env.git"
Write-Debug -Message "clone 終了"

Write-Debug -Message "コピー開始"
Join-Path -Path "env" -ChildPath "settings.yml" | Copy-Item -Path {$_} -Destination $AppName
Write-Debug -Message "コピー完了"

これで Get-Help [ファイルのフルパス] で書いたコメントをヘルプとして出せるようになる。

PS C:\Users\hoge\Desktop> Get-Help C:\Users\hoge\Desktop\test4.ps1

NAME
    C:\Users\hoge\Desktop\test4.ps1

SYNOPSIS
    アプリのソースに settings.yml をコピー


SYNTAX
    C:\Users\hoge\Desktop\test4.ps1 [-AppName] <String> [-AppGitBranch] <String> [[-EnvGitBranch] <String>] [<CommonPa
    rameters>]


DESCRIPTION
    指定アプリの Git リポジトリを指定したブランチで git clone する。
    env というリポジトリも指定したブランチで git clone する。(ブランチを指定しない場合は master ブランチとする。)
    env リポジトリにある settings.yml を、アプリのリポジトリにコピーする。


RELATED LINKS

REMARKS
    To see the examples, type: "Get-Help C:\Users\hoge\Desktop\test4.ps1 -Examples"
    For more information, type: "Get-Help C:\Users\hoge\Desktop\test4.ps1 -Detailed"
    For technical information, type: "Get-Help C:\Users\hoge\Desktop\test4.ps1 -Full"

PS C:\Users\hoge\Desktop>

REMARKS に書いてある通り、オプションをつけると引数の説明等も出力できる。

今回はこれで終了。