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

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

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

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

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

この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。

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

DOI(最新版)
10.5281/zenodo.21590164
DOI(この版)
10.5281/zenodo.21733109

「担当者が書いたPowerShellスクリプトが動いているが、その人しか触れない」「サーバー名やパスがコードのあちこちに直書きされていて、環境が変わるたびに本体を書き換えている」「引数を間違えて渡しても黙って動いてしまい、後で気づいた」── 運用自動化の相談で、スクリプトそのものよりも「スクリプトの渡し方・育て方」が問題になっているケースは非常に多いです。

個人の手元で動かすだけなら、変数直書きの「動くスクリプト」で困りません。しかし、タスクスケジューラに載せる、同僚に渡す、複数サーバーで使い回す、となった瞬間に、引数の設計と共通処理の整理が品質を決めます。幸いPowerShellには、この「人に渡せるスクリプト」への道具立てが言語機能として最初から用意されています。paramブロック、検証属性、コメントベースヘルプ、そしてモジュールです。

この記事では、中小企業の情シスや運用担当者がすでに持っている「動くスクリプト」を出発点に、引数設計 → 入力検証 → ヘルプと-WhatIf対応 → .psm1モジュール化 → 社内共有、という順で段階的に品質を引き上げる実務手順を整理します。PowerShell 7.xを基準にしつつ、Windows PowerShell 5.1しか使えない現場での注意点も都度補足します。

1. まず結論

  • 引数はparamブロックで宣言し、[CmdletBinding()]を付けて「高度な関数」にするのが出発点です。共通パラメーター(-Verbose、-ErrorActionなど)が自動で付き、未定義のパラメーターを渡すとバインドエラーになるため、タイプミスが黙って無視される事故を防げます。1
  • 必須の引数は[Parameter(Mandatory)]で宣言し、型を必ず付けます。必須指定を忘れた呼び出しは実行前に止まり、型が合わない値も実行前に弾かれます。2
  • 形式チェックはif文ではなく検証属性(ValidateSet/ValidateRange/ValidateScript/ValidateNotNullOrEmpty)に寄せます。検証に失敗すると関数は呼び出されず、「途中まで実行してから壊れる」を構造的に消せます。エラーは入口で早く出すのが原則です。2
  • オン/オフの引数は[switch]で宣言します。$trueや$falseを文字列で受け取る自己流のフラグ引数は、呼び出し側の事故のもとです。2
  • コメントベースヘルプ(.SYNOPSIS/.EXAMPLE)を書けば、自作コマンドにもGet-Helpが効きます。「使い方はコードを読んで」からの脱却が、人に渡す最低条件です。3
  • 変更系の関数はSupportsShouldProcessを宣言して-WhatIf/-Confirmに対応させます。呼び出し側が実行前に影響範囲を確認できることは、運用スクリプトの安全装置として費用対効果が最大です。4
  • 複数スクリプトで使い回す関数は.psm1モジュールに切り出し、$env:PSModulePath配下に置きます。置き場所さえ正しければImport-Module不要で自動読み込みされます。psd1マニフェストは「配る段階」で追加すれば十分です。5678
  • モジュールとスクリプトはGitでバージョン管理し、共有フォルダ配布では実行ポリシーとの関係に注意します。UNCパス上のスクリプトはRemoteSignedで実行を拒否されることがあります。9

この記事の知識マップ

PowerShellスクリプトを人に渡せる品質にするには、paramブロックに[CmdletBinding()]を付けて高度な関数にし、共通パラメーターを自動的に使えるようにすることが出発点になる。必須引数は[Parameter(Mandatory)]で渡し忘れによる既定値実行を防ぎ、ValidateSetなどの検証属性は形式が不正な値を実行前に弾いて途中まで実行されてから壊れる事故を防ぐ。パイプライン入力を受ける関数はValueFromPipelineとprocessブロックを組み合わせないと最後の1件しか処理されない罠があり、変更系の関数はSupportsShouldProcessで-WhatIfによる意図しない実行の防止を組み込む。複数スクリプトで使う共通処理は.psm1モジュールに切り出してExport-ModuleMemberで公開範囲を明示し、PSModulePath配下に置くと自動読み込みされ、配る段階でモジュールマニフェストを追加する。

PowerShellの引数設計とモジュール化の知識マップparamブロックと[CmdletBinding()]が検証属性やパイプライン入力・SupportsShouldProcessをどう支え、共通処理を.psm1モジュール化してPSModulePathやモジュールマニフェストへ配置する流れと、各段階で防げる典型的な事故の関係を示す図前提とする前提とする前提とする前提とする前提とする実装を担う前提とする防止するより先に行うべきで構成できる利用する前提とする前提とする推奨される対応防止する防止する防止する軽減するparamブロック.psm1スクリプトモジュール[Parameter(Mandatory)]検証属性(ValidateSet/ValidateRange/ValidateScript等)[switch]パラメーターValueFromPipelineとprocessブロック[CmdletBinding()]属性共通パラメーターSupportsShouldProcessと-WhatIf/-Confirm最後の1件しか処理されない罠ドットソース$env:PSModulePath(モジュールの探索パス)Export-ModuleMemberモジュールマニフェスト(.psd1)PowerShell実行ポリシーコメントベースヘルプ必須引数の渡し忘れによる既定値実行途中まで実行されてから壊れる事故タイプミス引数の黙殺変更系処理の意図しない実行

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

2. 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) }

[CmdletBinding()]は「この関数(スクリプト)をコンパイル済みコマンドレットと同じ流儀で動かす」という宣言で、これを付けた関数は高度な関数(advanced function)になります。効果は3つあります。1

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

Mandatoryを付けた引数が省略されると、PowerShellは実行前に入力を促します。「必須なのに渡し忘れたまま既定値で動いた」を防ぐ、最初の安全装置です。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より後ろに並べます。「開始(オプション)」を空にすると作業ディレクトリが既定の場所になるため、相対パスを使うスクリプトでは必ず指定してください。

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

3. 入力検証は入口で ── Validate属性で「エラーを早く」

引数の形式チェックを関数本体のif文で書くと、チェック漏れや「チェックより前に副作用が走る」バグが混入しがちです。PowerShellでは検証属性でパラメーター宣言に検証を同居させられます。検証は関数が呼び出される前に行われ、失敗すれば本体は1行も実行されません。2

[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 値を固定の選択肢に制限し、タブ補完を効かせる2 動作モード、環境名(Dev/Test/Prod)
ValidateRange 数値・日付の範囲制限2 保持日数、リトライ回数、ポート番号
ValidateScript 任意のスクリプトで検証。$falseか例外で失敗2 パスの存在確認、日付の前後関係
ValidateNotNullOrEmpty $null・空文字・空コレクションを拒否2 ほぼすべての文字列引数
ValidatePattern 正規表現での形式チェック2 伝票番号、ホスト名の命名規則

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

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

4. パイプライン入力の基本 ── ValueFromPipelineとprocessブロック

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

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件しか処理されない、という典型的なバグになります。10 beginとendは省略可能なので、迷ったら「processに本体、集計が要るならbegin/end」と覚えておけば実務では十分です。

罠の実感が湧きにくいところなので、NG例と並べておきます。

# 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本入れてください。

5. Get-Helpと-WhatIfを効かせる ── 人に渡す最低条件

5.1. コメントベースヘルプ

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

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

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

5.2. SupportsShouldProcessと-WhatIf

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

両方を組み込んだ、渡せる品質の関数の完成形はこうなります。

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を明示的に渡します。4

6. 共通処理を.psm1モジュールにする

6.1. .psm1とExport-ModuleMember

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

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

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

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

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

モジュール化の作り方は拍子抜けするほど簡単で、関数を書いたファイルを.psm1拡張子で保存するだけです。5

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

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

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

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

6.2. 置き場所 ── $env:PSModulePathと自動読み込み

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

スコープ 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が自動でインポートします(モジュールの自動読み込み)。7 つまり利用者は「コマンドが最初から入っているかのように」使えます。試行錯誤中のモジュールをフルパスでImport-Moduleして動作確認し、固まったらPSModulePath配下に配置する、という流れが実務のリズムです。

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

6.3. psd1マニフェストは「配る段階」で

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

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

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

6.4. 社内共有とバージョン管理の勘所

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

7. 実務の定石(判断表)

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

8. まとめ

  • paramブロックと[CmdletBinding()]で高度な関数にすることが「人に渡せるスクリプト」の出発点です。共通パラメーターが付き、引数の間違いが実行前に止まります。
  • 型指定・[Parameter(Mandatory)]・安全側の既定値・検証属性で、エラーを入口で早く出します。ValidateSetはタブ補完という使い勝手も向上させます。
  • パイプライン入力はValueFromPipelineとprocessブロックのセットで受けます。processを忘れると最後の1件しか処理されません。
  • コメントベースヘルプでGet-Helpを効かせ、変更系はSupportsShouldProcessで-WhatIfに対応させます。リハーサルできることが運用の安全装置になります。
  • 共通関数は.psm1に切り出し、Export-ModuleMemberで公開範囲を明示して、PSModulePath配下に配置します。5.1と7でパスが違う点に注意してください。
  • psd1マニフェストは配る段階で。原本はGitで管理し、共有フォルダ配布では実行ポリシー(UNCパスとRemoteSignedの関係)を確認します。

関連記事

関連する相談領域

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

参考リンク

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

  2. 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 15

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

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

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

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

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

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

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

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

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

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

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

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

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

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

よくある質問

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

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

ブログ一覧に戻る