PowerShellスクリプトの引数設計とモジュール化 ── 「動くスクリプト」から「人に渡せるスクリプト」へ

· 更新日: · · PowerShell, Windows, スクリプト, 自動化, 運用改善, 業務効率化, 既存資産活用, コマンドライン

更新履歴(3件・最終更新 2026年08月02日)

この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。

記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
タスクスケジューラの「操作」タブの記入例を3欄の表で追加しました。`process`ブロックを書かなかった場合のNG例(3件流すと最後の1件しか処理されず、しかもエラーが出ない)を対比で追加し、コメントベースヘルプのキーワード表、ドットソースの説明とモジュール化との使い分け、`$env:PSModulePath`の確認コマンドを追記しました。
初版公開
この記事を引用する(DOI(登録済みアーカイブ): 10.5281/zenodo.21590164)

以下のDOIは過去に登録されたアーカイブを指しており、現在の本文とは一致しない場合があります。現在の本文を参照するときは、このページのURLを使用してください。

小村 豪(2026)「PowerShellスクリプトの引数設計とモジュール化 ── 「動くスクリプト」から「人に渡せるスクリプト」へ」合同会社小村ソフト. https://comcomponent.com/blog/powershell-param-module-design/

DOI(登録済みアーカイブ)
10.5281/zenodo.21590164
DOI(前回登録した版)
10.5281/zenodo.21733109

「環境が変わるたびにスクリプトを書き換える」「担当者以外は使い方が分からない」「引数を間違えたのに、既定値のまま動いてしまう」。人に渡すPowerShellスクリプトでは、処理そのものだけでなく、何を受け取り、どう確認して実行し、どこへ配るかを決める必要があります。

最初から大きなモジュールを作り直す必要はありません。動いている処理を出発点に、paramと[CmdletBinding()]で入口を整え、入力検証、パイプライン処理、ヘルプ、-WhatIfを加えます。共通化が必要になったら.psm1へ切り出し、配布する段階で配置先やマニフェストを整えます。

この記事は、中小企業の情シス・運用担当者が、手元の「動くスクリプト」を「人に渡せるスクリプト」へ育てるための実務手順です。PowerShell 7.xを基準にし、Windows PowerShell 5.1との違いも、その機能を使う箇所で確認します。 コード例は各段階の形を示すものです。宣言だけの例や省略付きのモジュール例もあるため、すべてを上から一つのファイルへ貼り合わせる手順ではありません。

1. まず結論 ── 入口・処理・配布を順に整える

先に引数と実行時の振る舞いを整え、その後で共通化・配布を考えます。 引数の形式は検証属性へ、変更の確認は標準の-WhatIf/-Confirmへ寄せ、独自の仕組みを増やさないのが基本です。12

今困っていること 先に整えるもの 読む章
環境ごとにコードを書き換える、引数の間違いに気づけない param・[CmdletBinding()]・型・必須指定・検証属性 2章
複数件を渡すと処理が抜ける パイプライン入力とprocess、複数件の確認 3章
使い方や変更対象が分からず、人に渡せない Get-HelpとShouldProcessによるリハーサル 4章
共通関数のコピーが増えている .psm1への切り出しと公開関数の明示 5章
サーバーで見つからない、無人実行で止まる、版を追えない 配置先・マニフェスト・タスクの起動条件・Git管理 6章

必須指定だけでは、無人実行の入力待ちまでは防げません。また、[string]という型だけでは数値からの変換を拒否できず、SupportsShouldProcessの宣言だけで独自の変更処理が守られるわけでもありません。宣言と、その宣言を使う処理をセットで確認することが要点です。

人に渡すまでに整える境界引数の入口と実行中の振る舞いを整えてから、共通化と配布の仕組みへ進みます。引数と検証を宣言複数件の処理を確認ヘルプと変更確認を用意共通関数をモジュールへ配置・起動・版を管理

図1: 引数の入口と実行中の振る舞いを整えてから、共通化と配布の仕組みへ進みます。

図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全18件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle

2. 引数と入力検証を入口に集める

2.1 変わる値をparamへ出し、CmdletBindingを付ける

サーバー名やパス、保持日数を本体へ直書きすると、環境変更のたびに処理本体へ手を入れることになります。まず、次の例を出発点にします。

# よくある例: 変数直書き。環境が変わるたびに本体を書き換えることになる
$logDir = "D:\Logs\AppA"
$days = 90
Get-ChildItem $logDir -Filter *.log |
    Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$days) }

これをparamブロックと[CmdletBinding()]で書き直します。

[CmdletBinding()]
param(
    # 必須。指定し忘れた呼び出しは実行前に止まる
    # (注: [string]は意図の明示であって拒否ではない。数値などは文字列へ自動変換されて
    #  渡ってくるため、厳密に弾きたい条件は後述の検証属性で書く)
    [Parameter(Mandatory)]
    [string]$LogDir,

    # 省略可能な引数には業務上安全な既定値を持たせる
    [int]$Days = 90
)

Get-ChildItem -LiteralPath $LogDir -Filter *.log -File |
    Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$Days) }

2.2 型・必須指定・共通パラメーターの役割を分ける

paramは受け取る値を宣言する場所、[CmdletBinding()]はコマンドレットと同じ流儀で関数やスクリプトを動かすための宣言です。これを付けた関数は高度な関数(advanced function)になり、次の3つが使えます。3

  • 共通パラメーターが自動で付く。-Verbose、-Debug、-ErrorAction、-ErrorVariableなどを自分で実装しなくても、呼び出し側が使えるようになります。スクリプト内のWrite-Verboseが-Verbose指定時だけ表示される、という標準的な振る舞いがそのまま手に入ります。
  • 引数の間違いが実行前に止まる。高度な関数では、未定義のパラメーター名や、対応する位置パラメーターのない余分な引数を渡すとパラメーターバインドが失敗します。-Dyas 30のようなタイプミスが黙って無視されて既定値で動いてしまう、という事故がなくなります。3
  • $PSCmdletが使える。後述するShouldProcessなど、コマンドレット向けの機能への入口です。4
CmdletBindingで入口を標準化する未定義の引数をバインド時に止め、受け付けた呼び出しでは共通パラメーターとPSCmdletを使えます。対応しない対応する呼び出しの引数宣言に対応しているかバインドエラー本体で共通機能を使う

図2: 未定義の引数をバインド時に止め、受け付けた呼び出しでは共通パラメーターとPSCmdletを使えます。

型は必ず付けますが、型指定と入力値の制限は同じものではありません。例の[string]には数値なども文字列へ変換されて渡るため、許可したくない値は検証属性で表します。省略可能な引数の既定値は、業務上の安全側に倒します。

[Parameter(Mandatory)]を付けた引数が省略されると、PowerShellは本体の実行前に入力を促します。渡し忘れたまま既定値で動くことは防げますが、タスクでは誰も答えられないプロンプト待ちになり得ます。無人実行は-NonInteractiveで起動するところまで含めて設計し、具体的な記入例は6.3節で確認してください。1

2.3 形式は検証属性、オン・オフはswitchで受ける

単一パラメーターの形式チェックを宣言と一緒に置くと、本体のチェック漏れや、チェックより先に変更処理が走る事故を減らせます。検証で不正と判断された入力は本体へ渡しません。複数パラメーターの組み合わせや、実行時の状態に依存する確認は本体側のifで扱います。1

オン・オフは[switch]で宣言します。$trueや$falseを文字列で受け取る独自フラグにせず、指定すれば有効、省略すれば無効という標準の形にします。1

次は入力宣言だけの例です。存在するフォルダー、保持日数の範囲、動作モード、空でない名前をそれぞれ制約として持たせます。

[CmdletBinding()]
param(
    # 存在するフォルダーだけを受け付ける。$_ が検証対象の値
    [Parameter(Mandatory)]
    [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
    [string]$LogDir,

    # 保持日数は1〜3650日に制限。0や負数で「全ファイル対象」になる事故を防ぐ
    [ValidateRange(1, 3650)]
    [int]$Days = 90,

    # 選択肢を固定。タブ補完も効くようになる
    [ValidateSet('Zip', 'Move', 'ReportOnly')]
    [string]$Mode = 'ReportOnly',

    # 空文字・$nullを弾く。文字列引数の基本装備
    [ValidateNotNullOrEmpty()]
    [string]$ReportName = 'log-report',

    # オン/オフはswitch。指定すれば$true、省略すれば$false
    [switch]$IncludeSubfolders
)

使い分けの目安を整理します。

属性 用途 現場での典型例
ValidateSet 値を固定の選択肢に制限し、タブ補完を効かせる1 動作モード、環境名(Dev/Test/Prod)
ValidateRange 数値・日付の範囲制限1 保持日数、リトライ回数、ポート番号
ValidateScript 任意のスクリプトで検証。$falseか例外で失敗1 パスの存在確認、日付の前後関係
ValidateNotNullOrEmpty $null・空文字・空コレクションを拒否1 ほぼすべての文字列引数
ValidatePattern 正規表現での形式チェック1 伝票番号、ホスト名の命名規則
検証属性は型より前へ置く属性・型・変数名の順に宣言し、型変換との順序が意図とずれないようにします。検証属性型変数名

図3: 属性・型・変数名の順に宣言し、型変換との順序が意図とずれないようにします。

2.4 属性の順序と5.1の制約を確認する

検証属性は宣言順に注意が必要です。型より後に検証属性を書くと、型変換前の値が検証されて予期しない失敗を招くことがあるため、属性 → 型 → 変数名の順で書くのが公式ドキュメント推奨のベストプラクティスです。1 また、ValidateScriptのErrorMessage引数(独自エラーメッセージ)はPowerShell 6以降の機能で、Windows PowerShell 5.1では使えません。1 5.1混在環境では、検証スクリプト内でthrowして自前のメッセージを出すか、既定メッセージのままにしておくのが無難です。

ValidateScriptで凝った検証を書き始めたら、それはテストを書くサインでもあります。検証ロジック自体の動作確認は「PesterによるPowerShellのテスト整備」で整理した型に載せると壊れにくくなります。

形式チェックと実行時の確認を分ける単一の入力形式は宣言で確認し、組み合わせや実行時の状態は本体側で確認します。入力値型・検証属性で確認本体で組み合わせを確認実行時の状態を確認処理へ進む

図4: 単一の入力形式は宣言で確認し、組み合わせや実行時の状態は本体側で確認します。

3. パイプライン入力は複数件で確かめる

3.1 入力の宣言と1件ごとの処理をセットにする

自作関数もGet-Content servers.txt | Test-AppServerのようにパイプラインで使えると、PowerShellの標準コマンドと同じ感覚で組み合わせられます。必要なのはValueFromPipelineの宣言と、processブロックの2つです。1

パイプラインでのbegin・process・endbeginは最初に一度、processは届いた入力ごとに、endは最後に一度動きます。あるないbeginで準備入力をパラメーターへprocessで処理次の入力があるかendで結果を返す

図5: beginは最初に一度、processは届いた入力ごとに、endは最後に一度動きます。

function Test-AppServer {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [string[]]$ComputerName
    )
    begin   { $results = @() }   # パイプライン処理の前に1回だけ実行
    process {
        # パイプラインから流れてくる要素ごとに実行される
        foreach ($name in $ComputerName) {
            $results += [pscustomobject]@{
                ComputerName = $name
                # -ComputerNameは5.1と7の両方で使える(7で追加された-TargetNameは5.1に無い)
                Reachable    = Test-Connection -ComputerName $name -Count 1 -Quiet
            }
        }
    }
    end     { $results }         # 最後に1回だけ実行
}

押さえるべき点は1つだけです。パイプライン入力を受けるならprocessブロックに処理を書くこと。processブロックがないと、パイプラインで複数の値を流しても最後の1件しか処理されない、という典型的なバグになります。4 beginとendは省略可能なので、迷ったら「processに本体、集計が要るならbegin/end」と覚えておけば実務では十分です。

3.2 引数呼び出しだけで確認を終えない

次はprocessがないNG例です。同じ3件でも、引数でまとめて渡す場合とパイプラインで流す場合の結果が異なります。

# NG例: processブロックがない。パイプラインで3件流しても最後の1件しか処理されない
function Test-AppServerBad {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [string[]]$ComputerName
    )
    # begin/process/endを1つも書かないと、本体はまとめてendブロック扱いになり「最後に1回」しか動かない。
    # パイプラインの要素は1件ずつパラメーターに束縛されるので、endに来た時点で残っているのは最後の1件
    foreach ($name in $ComputerName) {
        [pscustomobject]@{ ComputerName = $name }
    }
}

'SV01', 'SV02', 'SV03' | Test-AppServerBad   # → SV03 の1行だけ。SV01とSV02は黙って捨てられる
'SV01', 'SV02', 'SV03' | Test-AppServer      # → 3行返る(前掲のprocessブロックあり版)

厄介なのはエラーが1件も出ないことです。しかもTest-AppServerBad -ComputerName 'SV01','SV02','SV03'のように引数で渡した場合は3件とも正しく処理されるため、引数呼び出しだけで動作確認していると気づけません。パイプライン入力を受ける関数を書いたら、複数件を流す確認を必ず1本入れてください。

確認する呼び出し方を二つにする引数で複数件を渡す確認に加え、パイプラインで複数件を流して件数を比較します。同じ三つの入力引数でまとめて渡すパイプラインで順に流すそれぞれの結果件数を確認

図6: 引数で複数件を渡す確認に加え、パイプラインで複数件を流して件数を比較します。

4. 使い方を伝え、変更前にリハーサルする

4.1 Get-Helpで使い方と前提を読めるようにする

PowerShell使いは、知らないコマンドに出会ったらまずGet-Helpを叩きます。自作関数がその文化に乗れるかどうかは、コメントベースヘルプを書いてあるかで決まります。特殊なキーワード付きコメントを書いておくだけで、Get-Helpが標準コマンドレットと同じ形式でヘルプを表示してくれます。5

よく使うキーワードを一覧にします。全部書く必要はなく、最低限は.SYNOPSISと.EXAMPLE、人に渡すなら.DESCRIPTIONと.PARAMETERまで、という順で足していけば十分です。5

キーワード 何を書くか
.SYNOPSIS 1行の要約。Get-Helpの先頭に出る
.DESCRIPTION 詳しい説明。前提条件や副作用はここへ
.PARAMETER パラメーター名 パラメーターごとの説明。キーワードの後ろにパラメーター名を続ける
.EXAMPLE 使用例。1行目に実行するコマンド、続く行にその説明。複数書ける
.INPUTS パイプラインで受け取れるオブジェクトの型
.OUTPUTS 返すオブジェクトの型
.NOTES 補足。作者、更新日、既知の制限など
.LINK 関連コマンドやURL。最初のURLはGet-Help -Onlineの飛び先になる

4.2 WhatIfの宣言と、変更を止める判定を分ける

削除・移動・設定変更を行う関数には、[CmdletBinding(SupportsShouldProcess)]を宣言します。これだけで-WhatIfと-Confirmパラメーターが自動で追加され、本体では$PSCmdlet.ShouldProcess()の戻り値で実際に変更を実行するかを分岐します。2

パラメーターを追加する宣言と、実際の変更前に行う判定はセットです。 この例では、対象ファイルの確認は行い、ShouldProcessが許可した場合だけ削除します。

変更直前でShouldProcessを判定する対象を選んでから変更の可否を判定し、WhatIfや確認の拒否時には削除へ進みません。実行する実行しない対象ファイルを選ぶShouldProcessで判定そのファイルを削除削除を行わない

図7: 対象を選んでから変更の可否を判定し、WhatIfや確認の拒否時には削除へ進みません。

4.3 ヘルプと変更確認を組み込んだ関数

次のRemove-OldAppLogは、入力検証、ヘルプ、-WhatIfを組み合わせた例です。

function Remove-OldAppLog {
    <#
    .SYNOPSIS
    指定フォルダーから保持期限を過ぎたログファイルを削除します。

    .DESCRIPTION
    LastWriteTimeが保持日数より古い*.logファイルを削除します。
    -WhatIfで削除対象の確認のみ行えます。

    .EXAMPLE
    Remove-OldAppLog -LogDir 'D:\Logs\AppA' -Days 90 -WhatIf
    削除対象を表示するだけで、実際には削除しません。
    #>
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
        [string]$LogDir,

        [ValidateRange(1, 3650)]
        [int]$Days = 90
    )
    process {
        $limit = (Get-Date).AddDays(-$Days)
        # -Fileでフォルダーを除外する(".log"で終わる名前のフォルダーを誤って消さない)
        Get-ChildItem -LiteralPath $LogDir -Filter *.log -File |
            Where-Object { $_.LastWriteTime -lt $limit } |
            ForEach-Object {
                # ShouldProcessが$falseを返すのは -WhatIf 時と -Confirm で拒否された時
                if ($PSCmdlet.ShouldProcess($_.FullName, "削除")) {
                    Remove-Item -LiteralPath $_.FullName
                }
            }
    }
}

Remove-OldAppLog -LogDir D:\Logs\AppA -WhatIfと打てば「What if: …」の一覧が出るだけで何も消えません。変更系スクリプトを人に渡すときは、-WhatIfでのリハーサル手順をセットで渡す── これが当サイトで繰り返し勧めている運用の型です。実際のログ整理スクリプトへの組み込み例は「PowerShellスクリプト応用 ── ログ調査・アーカイブ・レポート化を安全に自動化する」で詳述しています。

なお、呼び出した先のコマンドまで-WhatIfが常に伝播することを過信しない、という注意も公式の解説記事にあります。確実を期すなら内側のRemove-Itemなどに-WhatIf:$WhatIfPreferenceを明示的に渡します。2

実行手順と一緒にリハーサルを渡す利用者がヘルプで用途を調べ、WhatIfで対象を確認してから実行できる形にします。Get-Helpで用途を確認WhatIfで対象を確認対象と条件を見直す確認した条件で実行

図8: 利用者がヘルプで用途を調べ、WhatIfで対象を確認してから実行できる形にします。

5. 共通処理をモジュールへ切り出す

5.1 ドットソースで足りる段階と、モジュールへ進む段階

関数が育ってくると、複数のスクリプトから同じ関数を使いたくなります。コピペで増やすと修正が全コピーに波及しなくなるので、共通関数はまとめて持ちます。

その手前の選択肢がドットソースです。スクリプトのパスの前にドットと空白を付けて実行すると、そのスクリプトが呼び出し元のスコープで実行され、中で定義した関数や変数がそのまま呼び出し元に残ります。6

# 共通関数を書いた Common.ps1 を読み込む(先頭のドットと空白がドットソース)
. C:\Scripts\Common.ps1

# Common.ps1 の中で定義した関数を、そのまま呼び出せる
Remove-OldAppLog -LogDir 'D:\Logs\AppA' -WhatIf

手軽ですが、呼ぶ側がファイルの物理パスを知っている必要があり、公開範囲も選べません(関数も変数もエイリアスも全部そのまま流れ込みます)。1つのスクリプトを分割する程度なら十分ですが、2本目のスクリプトから同じ関数を使いたくなった時点でモジュール化に進むのが判断の目安です。

ドットソースは呼び出し元へ定義を残すドットソースでは共通ファイルを現在のスコープで実行し、定義した項目を呼び出し元から使います。呼び出し元のスコープCommon.ps1をドットソース関数・変数などが残る呼び出し元から使う

図9: ドットソースでは共通ファイルを現在のスコープで実行し、定義した項目を呼び出し元から使います。

5.2 公開する関数をExport-ModuleMemberで選ぶ

モジュール化は、関数を書いたファイルを.psm1拡張子で保存するだけです。7

次はファイルの構成例です。関数本体は省略しているため、4章の完成した関数などを入れて使います。

# AppOpsTools.psm1 ── 社内運用ツールの共通モジュール
function Remove-OldAppLog { <# 前章の関数 #> }
function Get-AppLogSummary { <# 集計関数 #> }

# 内部ヘルパー関数。外には見せない
function ConvertTo-InternalPath { <# ... #> }

# 公開する関数を明示する。書かなければ全関数が公開される
Export-ModuleMember -Function Remove-OldAppLog, Get-AppLogSummary

Export-ModuleMemberを書かない場合、モジュール内の関数とエイリアスはすべてエクスポートされます(変数はされません)。省略可能ですが、公開範囲を明示するのがベストプラクティスとされています。8 内部ヘルパーを隠しておけば、後で自由にリファクタリングできる余地が残ります。

呼び出し側と内部ヘルパーを分ける公開する関数を明示すると、利用側から隠した内部ヘルパーを変更しやすくなります。公開範囲を指定利用するスクリプト公開した関数内部ヘルパーExport-ModuleMember

図10: 公開する関数を明示すると、利用側から隠した内部ヘルパーを変更しやすくなります。

なお、関数名を付けて公開するときはVerb-Noun形式にし、動詞はGet-Verbで確認できる承認済み動詞から選びます。未承認の動詞でも動きますが、モジュールのインポート時に警告が出ます。9

6. 配置・無人実行・配布の条件をそろえる

モジュールができても、利用者の環境から見つかることと、予定した条件で起動することは別の確認です。配置先、版と前提条件、タスクの起動方法、配布経路の順に整えます。

6.1 実行する環境のPSModulePathを確認する

モジュールは$env:PSModulePathに列挙されたフォルダー配下に、モジュール名と同名のフォルダーを作って置きます(AppOpsTools\AppOpsTools.psm1)。フォルダー名とファイルのベース名が一致していないと、モジュールとして認識されません。107 既定の置き場所は次のとおりで、Windows PowerShell 5.1とPowerShell 7でパスが異なる点が現場のつまずきどころです。10

スコープ PowerShell 7 Windows PowerShell 5.1
自分専用(CurrentUser) $HOME\Documents\PowerShell\Modules $HOME\Documents\WindowsPowerShell\Modules
全ユーザー(AllUsers) $env:ProgramFiles\PowerShell\Modules $env:ProgramFiles\WindowsPowerShell\Modules

表を覚えるより、実際の環境で確認するほうが確実です。1行で見られます。

# 自分の環境の検索パスを1件1行で確認する(Windowsの区切り文字はセミコロン)
$env:PSModulePath -split ';'

# 区切り文字を環境から取る書き方。PowerShell 7でmacOS/Linuxも触るならこちら
$env:PSModulePath -split [System.IO.Path]::PathSeparator

同じ$env:PSModulePathという名前でも、5.1と7では中身が別物です。「モジュールが見つからない」の原因は、たいてい置いた場所がその環境の検索パスに入っていないことなので、まずこの1行を実行してから疑ってください。

正しい場所に置けば、Import-Moduleを書かなくても、モジュール内のコマンドを最初に実行したときにPowerShellが自動でインポートします(モジュールの自動読み込み)。11 つまり利用者は「コマンドが最初から入っているかのように」使えます。試行錯誤中のモジュールをフルパスでImport-Moduleして動作確認し、固まったらPSModulePath配下に配置する、という流れが実務のリズムです。

2点、環境依存の注意があります。Documentsフォルダーの実体はOneDriveのフォルダーリダイレクトで移動していることがあり、その場合ユーザースコープのモジュールもOneDrive配下に置かれます。10 また全ユーザースコープへの配置には管理者権限が必要です。サーバーに置くならAllUsersスコープにして、タスクスケジューラの実行アカウントからも見えるようにしておくと、「自分の端末では動くのにサーバーで動かない」を避けられます。

配置先は実行する環境から確認する手元で読めるだけでなく、実際のPowerShellと実行アカウントから見つかる配置先を選びます。利用するPowerShell実際のPSModulePathを確認同名フォルダーへ配置コマンドから読み込むタスクの実行アカウント

図11: 手元で読めるだけでなく、実際のPowerShellと実行アカウントから見つかる配置先を選びます。

6.2 配る段階でpsd1マニフェストを加える

モジュールマニフェスト(.psd1)は、モジュールのバージョンや依存関係などのメタデータを記述するハッシュテーブルのファイルで、必須ではありません。マニフェストで必須のキーはModuleVersionだけです。12 自分のチーム内で使っている段階では.psm1単体で十分で、他部署へ配る・バージョン管理を厳密にする段階になったらNew-ModuleManifestで生成します。12

New-ModuleManifest -Path .\AppOpsTools\AppOpsTools.psd1 `
    -RootModule 'AppOpsTools.psm1' `
    -ModuleVersion '1.0.0' `
    -FunctionsToExport 'Remove-OldAppLog', 'Get-AppLogSummary' `
    -PowerShellVersion '5.1'

生成されたpsd1はコメント付きのテンプレートになっているので、必要なキーだけ育てていけます。12

処理本体と配布時のメタデータを分けるpsm1へ共通関数を置き、配布時にpsd1へ版・前提条件・公開コマンドを記録します。psm1に共通関数チーム内で使用配布の段階でpsd1を追加版・依存関係・公開範囲

図12: psm1へ共通関数を置き、配布時にpsd1へ版・前提条件・公開コマンドを記録します。

6.3 タスクは入力待ちをせず、必要な引数を渡して起動する

2.2節で触れた必須引数の入力待ちは、起動側で防ぎます。無人実行では-NonInteractiveを指定し、入力を求める代わりにエラーで失敗させます。PowerShell 7で動かす場合の、タスクスケジューラの「操作」タブの記入例です。

欄 記入例
プログラム/スクリプト C:\Program Files\PowerShell\7\pwsh.exe
引数の追加(オプション) -NoProfile -NonInteractive -File "C:\Scripts\Remove-OldAppLog.ps1" -LogDir "D:\Logs\AppA" -Days 90
開始(オプション) C:\Scripts

Windows PowerShell 5.1で動かすなら、プログラム欄はpowershell.exeです。押さえどころは3つあります。-NoProfileでプロファイル読み込みによる環境差と起動の遅れをなくすこと、-NonInteractiveで前述のプロンプト待ちを防ぐこと、そしてスクリプトのパスも引数の値も、空白が入りうるなら引用符で囲むことです。-Fileのあとに書いたものはスクリプトの引数として渡されるので、スクリプト自身のパラメーターは-Fileより後ろに並べます。「開始(オプション)」を空にすると作業ディレクトリが既定の場所になるため、相対パスを使うスクリプトでは必ず指定してください。

必須引数と無人起動は両側で整える必須指定で渡し忘れを検出し、無人起動のNonInteractiveで答えられない入力待ちを防ぎます。スクリプトは必須を宣言必須引数の渡し忘れを検出無人起動はNonInteractive入力待ちではなくエラー

図13: 必須指定で渡し忘れを検出し、無人起動のNonInteractiveで答えられない入力待ちを防ぎます。

6.4 原本・配布先・実行ポリシーを管理する

  • 原本はGitに置きます。スクリプトとモジュールはテキストなのでGitと相性がよく、「いつ、誰が、なぜ変えたか」が追えることが運用スクリプトの信頼性そのものになります。ModuleVersionの更新をコミットと対応させておくと、サーバーに入っている版の特定が楽になります。
  • 配布は「共有フォルダから各マシンのPSModulePath配下へコピー」が基本形です。モジュールのフォルダーごとコピーすれば手動インストールできます。11 共有フォルダ上のパスを直接PSModulePathに足す構成は、共有が「遅い・切れる・いないことがある」前提(詳細は「ネットワークドライブとUNCパスの落とし穴」)を踏まえると、常用はお勧めしません。
  • 実行ポリシーとの関係に注意します。既定のRemoteSignedはローカル作成の無署名スクリプトを許可しますが、UNCパスとインターネットパスを区別しないシステムでは、UNCパス上のスクリプトが実行を拒否されることがあります。またダウンロード由来のマークが付いたファイルはブロックされ、Unblock-Fileでの解除か署名が必要です。13 社内配布を本格化するなら、コード署名との組み合わせを「PowerShellの実行ポリシーとスクリプト署名」で確認してください。
原本から各マシンへ配る原本と版はGitで管理し、共有フォルダ経由で各マシンのモジュール配置先へコピーします。Gitで原本と版を管理共有フォルダへ配布物各マシンへフォルダーごとコピー配置先で使用する

図14: 原本と版はGitで管理し、共有フォルダ経由で各マシンのモジュール配置先へコピーします。

7. 実務の定石 ── 判断表と渡す前の確認

論点 選択肢 判断の目安
引数の受け取り 変数直書き / paramブロック 2回以上使う・他人が使うならparam一択。既定値は「安全側」に倒す1
[CmdletBinding()] 付けない / 付ける 人に渡すもの・運用に載せるものは常に付ける。タイプミスが実行前に止まる3
入力チェック 本体のif文 / 検証属性 単一パラメーターの形式チェックは属性へ。組み合わせ検証だけ本体で1
変更系の安全装置 独自の-TestMode引数 / SupportsShouldProcess 自前フラグは作らない。標準の-WhatIf/-Confirmに乗る2
共通処理の持ち方 コピペ / ドットソース / .psm1モジュール 2スクリプト目で共有した時点でモジュール化。公開関数はExport-ModuleMemberで明示78
モジュールの置き場所 任意フォルダー+Import-Module / PSModulePath配下 常用するものは規定の場所へ置いて自動読み込みに乗せる。サーバーはAllUsersスコープ1011
psd1マニフェスト 最初から作る / 配る段階で作る チーム外へ出す・バージョン管理を厳密にする段階でNew-ModuleManifest12

判断表で方式を選んだら、誤った引数、複数件のパイプライン、-WhatIf、実際の実行環境からの読み込みを確認します。引数で呼べることだけ、手元で読み込めることだけで確認を終えないのが要点です。

確認は正常な一回の呼び出しだけで終えない入口の失敗、複数件の入力、変更しない実行、配布先での読み込みをそれぞれ確認します。誤った引数を拒否複数件の結果を確認WhatIfで変更されない配布先で読み込める

図15: 入口の失敗、複数件の入力、変更しない実行、配布先での読み込みをそれぞれ確認します。

8. まとめ

引数の設計は、処理を本体へ渡す前の約束を決めることです。 paramと[CmdletBinding()]で入口を標準化し、型・必須指定・安全な既定値・検証属性をそろえます。パイプライン入力はValueFromPipelineとprocessをセットにし、複数件が処理されることまで確認します。

人に渡す段階では、コメントベースヘルプで使い方を伝え、変更系にはSupportsShouldProcessと実際の判定を組み込みます。-WhatIfのリハーサル手順も、実行手順と一緒に渡してください。

共通関数は.psm1へ切り出し、Export-ModuleMemberで公開範囲を明示します。常用するものは実行環境のPSModulePathへ置き、配る段階で.psd1を加えます。5.1と7の探索パス、タスクの実行アカウント、無人起動の引数、共有フォルダと実行ポリシーを確認し、原本と版をGitで管理するところまでが「人に渡せるスクリプト」の準備です。

段階ごとに人へ渡せる形へ育てる標準の引数と安全な実行方法を用意し、共通化と配布の条件を整えて引き継ぎます。引数の約束を宣言使い方と変更確認を用意共通処理と公開範囲を整理配置・起動・版を引き継ぐ

図16: 標準の引数と安全な実行方法を用意し、共通化と配布の条件を整えて引き継ぎます。

関連記事

関連する相談領域

合同会社小村ソフトでは、属人化したPowerShellスクリプトの整理・モジュール化、運用自動化スクリプトの設計レビュー、社内配布・バージョン管理の仕組みづくりを扱っています。「動いてはいるが誰も触れない」スクリプト資産の棚卸しからでもご相談いただけます。

参考リンク

  1. Microsoft Learn, about_Functions_Advanced_Parameters. Parameter属性とMandatory、ValueFromPipeline、switchパラメーター、ValidateSet/ValidateRange/ValidateScript/ValidateNotNullOrEmpty/ValidatePatternなどの検証属性の仕様、検証失敗時に関数が呼び出されないこと、属性を型より前に宣言するのがベストプラクティスであること、ValidateScriptのErrorMessage引数がPowerShell 6以降であることについて。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14

  2. Microsoft Learn, Everything you wanted to know about ShouldProcess. SupportsShouldProcessの指定だけで-WhatIf/-Confirmが自動作成されること、$PSCmdlet.ShouldProcess()での分岐の書き方、-WhatIfの伝播を過信せず内側のコマンドに明示的に渡すことが推奨される点について。 ↩ ↩2 ↩3 ↩4

  3. Microsoft Learn, about_Functions_CmdletBindingAttribute. CmdletBinding属性が関数をコンパイル済みコマンドレットのように動作させること、共通パラメーターが自動追加されること、$PSCmdletが使えるようになること、未知のパラメーターや対応しない位置引数でバインドが失敗すること、SupportsShouldProcessがConfirm/WhatIfパラメーターを追加することについて。 ↩ ↩2 ↩3

  4. Microsoft Learn, about_Functions_Advanced_Methods. 高度な関数で使えるbegin/process/endの入力処理メソッド、ShouldProcessメソッドがprocessブロック内から呼び出されCmdletBinding属性での宣言が必要であることについて。 ↩ ↩2

  5. Microsoft Learn, about_Comment_Based_Help. .SYNOPSIS/.DESCRIPTION/.PARAMETER/.EXAMPLEなどのキーワードでコメントベースヘルプを書くと、Get-HelpがXMLヘルプと同じ形式で表示すること、スクリプトと関数それぞれでの配置ルールについて。 ↩ ↩2

  6. Microsoft Learn, about_Scripts. スクリプトが既定では独自のスコープで実行され、その中で作られた関数・変数・エイリアス・ドライブがスクリプトスコープにしか存在しないこと、パスの前にドットと空白を付けて実行する「ドットソース」を使うと現在のスコープで実行され、作られた項目が実行後もセッションに残ることについて。 ↩

  7. Microsoft Learn, How to Write a PowerShell Script Module. .psm1拡張子で保存するだけでスクリプトモジュールになること、スクリプトと同名のフォルダーに保存すること、既定では全関数が公開され変数は公開されないこと、Export-ModuleMemberで公開する関数を明示することが推奨されることについて。 ↩ ↩2 ↩3

  8. Microsoft Learn, Export-ModuleMember. Export-ModuleMemberがスクリプトモジュールからエクスポートするメンバーを指定するコマンドレットであること、未指定時は関数とエイリアスがエクスポートされ変数はされないこと、省略可能だが作者の意図を示すベストプラクティスであることについて。 ↩ ↩2

  9. Microsoft Learn, Approved Verbs for PowerShell Commands. コマンド名がVerb-Noun形式であるべきこと、承認済み動詞の一覧とGet-Verbでの確認方法、未承認動詞を含むモジュールのインポート時に警告が表示されることについて。 ↩

  10. Microsoft Learn, about_PSModulePath. $env:PSModulePathがモジュール検索フォルダーの一覧であること、PowerShell 7とWindows PowerShell 5.1でCurrentUser/AllUsersスコープの既定パスが異なること、OneDriveやフォルダーリダイレクトでDocumentsの場所が変わりうることについて。 ↩ ↩2 ↩3 ↩4

  11. Microsoft Learn, about_Modules. PSModulePath配下のモジュールはコマンド初回実行時に自動インポートされること(モジュール自動読み込み)、モジュールフォルダーごとコピーする手動インストールの方法、既定のモジュール配置場所について。 ↩ ↩2 ↩3

  12. Microsoft Learn, New-ModuleManifest. モジュールマニフェスト(.psd1)がモジュールの内容・属性・前提条件を記述するハッシュテーブルであり必須ではないこと、必須キーがModuleVersionのみであること、New-ModuleManifestがテンプレートとして使える雛形を生成することについて。 ↩ ↩2 ↩3 ↩4

  13. Microsoft Learn, about_Execution_Policies. RemoteSignedがローカル作成の無署名スクリプトを許可しインターネット由来のスクリプトに署名を要求すること、UNCパスとインターネットパスを区別しないシステムではUNCパス上のスクリプトがRemoteSignedで実行を許可されないことがあること、Unblock-Fileによるブロック解除について。 ↩

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

PowerShellのparamブロックに[CmdletBinding()]を付けると何が変わりますか?
関数やスクリプトが「高度な関数(advanced function)」として扱われ、コンパイル済みコマンドレットと同じ振る舞いを手に入れます。具体的には-Verboseや-ErrorActionなどの共通パラメーターが自動で追加され、$PSCmdlet変数が使えるようになり、未定義のパラメーターや余分な位置引数を渡すとバインドエラーになります。タイプミスした引数が黙って無視されなくなるため、運用スクリプトでは付けるのが基本です。
スクリプトの引数チェックはValidate属性とif文のどちらで書くべきですか?
パラメーターの形式チェックはValidateSetやValidateRange、ValidateScriptなどの検証属性に寄せるのが定石です。検証は関数本体が実行される前に行われ、不正な値なら処理が1行も走らずにエラーになるため、「途中まで実行されてから壊れる」事故を防げます。ValidateSetにはタブ補完が効くという実利もあります。一方、複数パラメーターの組み合わせ検証や実行時の状態に依存するチェックは本体側のif文で行います。
PowerShellの自作モジュール(.psm1)はどこに置けばよいですか?
$env:PSModulePathに含まれるフォルダー配下に「モジュール名と同名のフォルダー」を作って置きます。自分専用ならPowerShell 7では$HOME\Documents\PowerShell\Modules、全ユーザー共通なら$env:ProgramFiles\PowerShell\Modulesが既定です。Windows PowerShell 5.1ではそれぞれWindowsPowerShell\Modulesとパスが異なる点に注意してください。この場所に置けば、Import-Moduleを書かなくてもコマンド初回実行時に自動読み込みされます。
モジュールマニフェスト(psd1)は必ず作る必要がありますか?
必須ではありません。マニフェストなしの.psm1単体でもモジュールとして動作します。マニフェストが必要になるのは「配る段階」で、バージョン番号、必要なPowerShellバージョン、依存モジュール、エクスポートするコマンドの明示などのメタデータを持たせたいときにNew-ModuleManifestで生成します。マニフェストで必須のキーはModuleVersionだけなので、まずは最小構成で始めて必要に応じて育てれば十分です。
社内の共有フォルダに置いたスクリプトが実行ポリシーでブロックされるのはなぜですか?
既定のRemoteSignedポリシーは、ローカルで作成されたスクリプトは無署名でも実行を許可しますが、インターネット由来とマークされたスクリプトには署名を要求します。UNCパスとインターネットパスを区別しない構成のシステムでは、共有フォルダ上のスクリプトがRemoteSignedで実行を拒否されることがあると公式ドキュメントにも明記されています。社内配布を本格化するなら、コード署名とAllSignedの組み合わせ、またはイントラネットゾーンの構成を検討してください。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る