【PowerShell】関数の作り方|param・戻り値・パイプライン対応・引数の検証

【PowerShell】関数の作り方|param・戻り値・パイプライン対応・引数の検証 PowerShell

同じ処理を何度も使うなら、関数にまとめると便利です。PowerShellの関数はfunctionで定義しますが、他の言語に慣れていると戸惑う点がいくつかあります。呼び出すときにかっこを付けないこと、引数はparamで受け取ること、そして最大の落とし穴であるreturnを書かなくても、関数内の出力がすべて戻り値になることです。

特に戻り値の挙動は、知らないと「余計な値が混ざる」という分かりにくいバグの原因になります。この記事では、実機で確認しながら、引数の受け取り方・戻り値・検証・パイプライン対応までを整理します。

先に結論

  • 関数はfunction 名前 { 処理 }で定義し、かっこなし名前 -引数 値)で呼び出します。
  • 引数はparam([int]$A, [string]$B = "既定")のようにparamで受け取ります。
  • 最重要returnを書かなくても、関数内で出力された値はすべて戻り値になります。returnは早く抜けるだけです。
  • そのため、デバッグの文字列などを途中で出力すると戻り値に混ざります。画面表示にはWrite-Hostを使います。
  • 引数は[Parameter(Mandatory)]で必須、[ValidateSet(...)]などで検証できます。
  • processブロックを使うと、関数をパイプライン対応にできます。

関数の中では、条件分岐やループ、エラー処理をよく使います。比較演算子と条件分岐ループ完全ガイドtry-catchでエラー処理もあわせて参考になります。

スポンサーリンク

関数の基本(定義とかっこなしの呼び出し)

関数はfunctionに続けて名前と{ }で定義します。呼び出すときは、コマンドと同じようにかっこを付けません。名前の付け方は「動詞-名詞」(Get-Userなど)が慣例です。

function-basic.ps1
# 定義
function Get-Greeting {
    "こんにちは"
}

# 呼び出し(かっこは付けない)
Get-Greeting        # こんにちは

# NG: かっこを付けると別の意味になる
# Get-Greeting()    # 引数として空配列を渡す扱いになり、意図と違う

実機でも、Get-Greetingはそのままこんにちはを返しました。Get-Greeting()のようにかっこを付けると、コマンド呼び出しではなく「空の引数を渡す」など意図しない動きになるため、付けないでください。

paramで引数を受け取る

引数は、関数の先頭にparam( )を書いて受け取ります。型([int]など)や既定値も指定できます。呼び出しは-引数名 値の形(名前指定)か、順番に渡す位置指定です。

param.ps1
function Add-Numbers {
    param(
        [int]$A,
        [int]$B = 10   # 既定値(指定しなければ 10)
    )
    $A + $B
}

Add-Numbers -A 3 -B 4   # 7(名前で指定)
Add-Numbers 3 4         # 7(位置で指定)
Add-Numbers -A 5        # 15(B は既定の 10)

# 型を指定すると、合わない値は自動変換 or エラーになる
function Greet {
    param([string]$Name = "ゲスト")
    "ようこそ、$Name さん"
}
Greet -Name "山田"   # ようこそ、山田 さん

実機でも、Add-Numbers -A 5Bが既定値10になり15を返しました。型を付けると、想定外の型が渡されたときに変換またはエラーになり、安全になります。

【最重要】returnしなくても出力が戻り値になる

ここがPowerShellの関数で最も重要なポイントです。多くの言語ではreturnで返した値だけが戻り値ですが、PowerShellでは関数内で出力された値がすべて戻り値になりますreturnは「そこで関数を抜ける」だけで、それ以前の出力も戻り値に含まれます。

return-gotcha.ps1
function Get-Multi {
    "a"
    "b"
    return "c"
}

$result = Get-Multi
$result.Count   # 3 ← a, b, c がすべて返る!
$result         # a, b, c

# return は「値を返して抜ける」とは限らない。早期に抜けるだけ
function Get-First {
    param([int]$N)
    if ($N -lt 0) { return "負の数です" }   # ここで抜ける
    "正の数: $N"
}
デバッグの出力が戻り値に混ざる

この挙動でハマりやすいのが、デバッグ用の文字列を途中で出力してしまうケースです。実機で確認したところ、関数内に"処理中..."のような裸の文字列を書くと、本来返したい値と一緒に戻り値に混入しました(件数が2件になった)。画面に表示したいだけなら、戻り値に入らないWrite-Hostを使ってください。

write-host-clean.ps1
# NG: 裸の文字列は戻り値に混ざる
function Get-CountNG {
    $items = 1, 2, 3
    "処理中..."        # これも戻り値になってしまう
    $items.Count
}
# 戻り値: "処理中...", 3 ← 2件混ざる

# OK: 画面表示は Write-Host(戻り値に入らない)
function Get-CountOK {
    $items = 1, 2, 3
    Write-Host "処理中..."   # 画面に出るだけ。戻り値には入らない
    $items.Count
}
# 戻り値: 3 だけ

実機でも、Write-Hostで出力したものは戻り値に含まれず、Get-CountOKの戻り値は3だけになりました。「関数の戻り値にしたいもの」と「画面に見せたいだけのもの」を分けて、後者はWrite-Host(またはWrite-Verbose)を使うのが鉄則です。

引数を必須にする・検証する

引数に条件を付けると、不正な値での実行を防げます。[Parameter(Mandatory)]で必須、[ValidateSet(...)]で選択肢の限定、[ValidateRange(...)]で範囲の限定ができます。

validation.ps1
function New-Account {
    param(
        # 必須(指定しないと入力を求められる)
        [Parameter(Mandatory)]
        [string]$Name,

        # 決まった値のどれかだけ許可する
        [ValidateSet("admin", "user", "guest")]
        [string]$Role = "user",

        # 範囲を限定する
        [ValidateRange(0, 120)]
        [int]$Age
    )
    "アカウント: $Name ($Role, $Age 歳)"
}

New-Account -Name "山田" -Role "admin" -Age 30   # OK
# New-Account -Name "山田" -Role "xxx"           # エラー(xxx は許可外)
# New-Account -Name "山田" -Age 999              # エラー(範囲外)

実機でも、ValidateSetの許可外の値やValidateRangeの範囲外を渡すと、例外(ParameterBindingValidationException)で止まりました。検証を付けておくと、関数の中でifを書かなくても、入口で不正な値をはじけます。ほかに、空を禁止する[ValidateNotNullOrEmpty()]や、正規表現で形式を縛る[ValidatePattern(...)]もあります。

関数をパイプライン対応にする

関数をパイプライン(|)で使えるようにするには、引数にValueFromPipelineを付け、本体をprocessブロックで囲みます。processは、流れてくる要素ごとに実行されます。

pipeline-function.ps1
function Double-It {
    param(
        [Parameter(ValueFromPipeline)]
        [int]$Num
    )
    process {
        $Num * 2
    }
}

# パイプラインで使える
1, 2, 3 | Double-It      # 2, 4, 6

# 引数で渡しても使える
Double-It -Num 5         # 10

実機でも、1, 2, 3 | Double-It2, 4, 6を返しました。processブロックを使わないと、パイプラインの最後の1件しか処理されないため、流すデータを1件ずつ扱うときは必ずprocessに書きます。前処理・後処理が必要ならbeginendブロックも使えます。

変数のスコープ(関数内はローカル)

関数の中で作った変数は、その関数の中だけで有効なローカル変数です。関数の外の同名の変数には影響しません。

scope.ps1
$message = "外側の値"

function Test-Scope {
    $message = "内側の値"   # これは関数内だけの別物
    Write-Host $message     # 内側の値
}

Test-Scope
$message   # 外側の値(関数内の代入は影響しない)

# 外の変数を関数内から変えたいときは $script: を付ける(多用は避ける)
$count = 0
function Add-One { $script:count++ }
Add-One
$count   # 1

実機でも、関数内で$messageに代入しても、関数の外の$message外側の値のままでした。関数は外の変数を読むことはできますが、書き換えたいときは$script:$global:を付けます。ただし、スコープをまたいだ書き換えは分かりにくくなるため、できるだけ戻り値で値を返す設計にするのがおすすめです。

[CmdletBinding()]で高度な関数にする

関数に[CmdletBinding()]を付けると、-Verbose-ErrorActionといった共通パラメータが使えるようになり、標準コマンドに近い「高度な関数」になります。

cmdletbinding.ps1
function Get-Report {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Path
    )

    Write-Verbose "処理を開始します: $Path"   # -Verbose 指定時だけ表示
    "レポート完了: $Path"
}

Get-Report -Path "C:\data"            # 通常実行
Get-Report -Path "C:\data" -Verbose   # 詳細メッセージも表示される

Write-Verboseは、-Verboseを付けたときだけ表示される詳細メッセージです。戻り値には入らないため、進捗の表示に向いています。本格的な関数を作るなら[CmdletBinding()]を付けておくと、後から機能を足しやすくなります。

よくある失敗

returnしていない値が勝手に返る

関数内で出力した値はすべて戻り値になります。デバッグ用の裸の文字列などが混ざらないよう、画面表示はWrite-Host、戻り値にしたい値だけを出力します。

関数をかっこ付きで呼ぶ

PowerShellの関数はGet-Greeting()ではなくGet-Greetingと呼びます。引数は名前 -引数 値の形です。

パイプライン対応にprocessを書き忘れる

ValueFromPipelineを付けてもprocessブロックがないと、最後の1件しか処理されません。流すデータはprocessで1件ずつ扱います。

関数内の変数の変更が外に反映されないと悩む

関数内の変数はローカルです。外の変数は読めますが書き換えはできません。値を返したいなら戻り値を使います。

引数のチェックをすべてif文で書く

必須・選択肢・範囲のチェックは、MandatoryValidateSetなどの属性で入口に書けます。関数の中身が読みやすくなります。

よくある質問

Qreturnを書かないと値は返らないのですか?
Aいいえ。PowerShellでは、関数内で出力された値はすべて戻り値になります。returnはその場で関数を抜けるための命令で、書かなくても最後に出力された値(や途中の出力)が返ります。意図しない値が混ざらないよう、戻り値にしたいものだけを出力してください。
Qデバッグの表示を戻り値に入れないようにするには?
A画面に出すだけならWrite-Hostを使います。Write-Hostの出力は戻り値に含まれません。進捗の詳細ならWrite-Verbose-Verbose指定時だけ表示)も便利です。
Q引数を必須にするには?
A[Parameter(Mandatory)]を引数に付けます。指定せずに呼び出すと、PowerShellが入力を求めてきます。さらに[ValidateSet(...)][ValidateRange(...)]で、許可する値も縛れます。
Q関数をパイプラインで使うには?
A引数に[Parameter(ValueFromPipeline)]を付け、本体をprocess { }ブロックで囲みます。これで1,2,3 | 関数名のように、流れてくる要素を1件ずつ処理できます。
Q関数の中で外の変数を変えられますか?
Aそのままでは変えられません。関数内の変数はローカルです。どうしても外を変えたいなら$script:$global:を付けますが、分かりにくくなるため、基本は戻り値で値を返す設計にしてください。

まとめ

  • 関数はfunction 名前 { }で定義し、かっこなしで呼び出します。
  • 引数はparam([型]$名前 = 既定値)で受け取り、名前指定・位置指定の両方で渡せます。
  • returnを書かなくても、出力された値はすべて戻り値になりますreturnは早く抜けるだけです。
  • 画面表示はWrite-Hostを使い、戻り値を汚さないようにします。
  • 引数はMandatoryValidateSetなどで検証でき、processでパイプライン対応にできます。
  • 関数内の変数はローカルです。値は戻り値で返すのが基本です。

PowerShellの関数で最初に押さえるべきは、「出力したものが全部戻り値になる」という1点です。ここさえ理解すれば、戻り値の混入で悩むことがなくなり、paramと検証属性を組み合わせて、安全で使い回しやすい関数を書けるようになります。