Visual Studio 2022のPre-buildイベントでPowerShellが動かない原因と対策|エラー9009・複数行スクリプト対応

Visual Studio 2022 の「Pre-build イベント」に PowerShell をそのまま貼り付けたら、エラー 9009 や「$content はコマンドとして認識されません」が出て失敗する――この手のトラブルは “PowerShell のつもりで書いたコードを、まず cmd.exe が解釈してしまう” ことが原因です。この記事では、なぜ起きるのかを腹落ちするまで整理し、複数行スクリプトを安全に動かす実践的な書き方をまとめます。

目次

起きている現象:PowerShell の変数や配列アクセスが「未知のコマンド」扱いになる

Visual Studio 2022 のプロジェクト設定にある「ビルド前イベント(Pre-build event)」欄に、PowerShell スクリプトを複数行で直接貼り付けると、次のような症状が出やすくなります。

  • ビルドが停止し、終了コード 9009 が表示される
  • $content はコマンドとして認識されません」「$parts[0] はコマンドとして認識されません」など、PowerShell 構文が cmd のコマンドとして解釈されたようなエラーになる
  • PowerShell(ターミナル)から実行すると動くのに、ビルドイベントからだと動かない

この状況でまず押さえるべき結論はシンプルです。

Pre-build イベントに書いた文字列は、最初に cmd.exe が解釈します。PowerShell の構文は cmd.exe には通りません。

原因:Pre-build イベントは cmd.exe 経由で実行される

Visual Studio のビルドイベント(Pre-build / Post-build)は、「その欄に書いたコマンドを実行してくれる」機能ですが、実行の起点は PowerShell ではなく Windows のコマンドプロンプトです。つまり、あなたが欄に貼った内容は、まず cmd.exe の文法として読み取られます。

たとえば PowerShell では当たり前の次のような記述は、cmd.exe から見ると意味不明です。

$content = Get-Content "build.txt"
$parts = $content -split " "
$major = [int]$parts[0]

cmd.exe は $content を「実行ファイル名(コマンド名)」だと思って探しに行きます。当然そんなコマンドは見つからないので「認識されません」になり、結果としてよく見かける 終了コード 9009(コマンドが見つからない系の失敗)に繋がります。

さらにややこしいのが 複数行 です。Pre-build イベント欄に改行を含む内容を貼ると、行ごとに分割されて cmd.exe に渡されます。すると PowerShell の変数代入行や配列アクセス行が “単独の cmd コマンド” として扱われ、エラーが連鎖します。

症状実際に起きていること代表的な対策
エラー 9009cmd.exe が「その名前のコマンドが見つからない」と判断powershell.exe(または pwsh)を明示して起動する
「$content はコマンドとして認識されません」PowerShell の変数が cmd.exe のコマンド名として解釈された-Command の中に PowerShell を閉じ込める/-File で .ps1 を呼ぶ
PowerShell では動くのに VS では動かない実行シェル(cmd vs PowerShell)とクォート規則が違うクォートの設計を見直す/EncodedCommand を使う

結論:ビルドイベント欄には「PowerShell を起動する cmd コマンド」を書く

Pre-build イベントを安定させる最短ルートは、ビルドイベント欄に PowerShell のコードを書こうとしないことです。代わりに、cmd.exe から PowerShell を起動する 1 本のコマンドとして記述します。

まずは “動く最小形” を押さえましょう。

最小構成:PowerShell を 1 行で起動する

"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "Write-Host 'Hello from PowerShell'"
  • powershell.exe のパスは フルパス推奨(PATH の影響を避ける)
  • -NoProfile:プロファイル(ユーザーのカスタム設定)で挙動が変わる事故を防ぐ
  • -NonInteractive:ビルド中に入力待ちになって固まるのを防ぐ
  • -ExecutionPolicy Bypass:実行ポリシーで止まる環境向け(組織ポリシーが厳しい場合は後述の「署名」等も検討)

Windows PowerShell と PowerShell 7(pwsh)どっちを使うべき?

VS2022 のビルドイベントから PowerShell を呼ぶとき、選択肢は主に 2 つです。

選択肢実行ファイル特徴向いているケース
Windows PowerShell 5.1%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exeWindows 標準搭載で “必ずある” のが強いチーム全員の環境差を最小にしたい/ビルドマシンも含め確実に動かしたい
PowerShell 7+pwsh.exe(例:C:\Program Files\PowerShell\7\pwsh.exe新機能・高速化・クロスプラットフォーム。だが “入っている前提” が必要開発環境を揃えられる/将来的に CI でも pwsh を使う方針

迷ったらまずは “必ず存在する” Windows PowerShell 5.1 で固めるのが無難です。PowerShell 7 を採用するなら、チーム全体でインストールとパスを統一し、ビルドイベントの呼び出し先もフルパスで固定するのがおすすめです。

複数行スクリプトを動かす方法:現場で使う 3 パターン

ここからが本題です。複数行の PowerShell 処理を Pre-build イベントで扱うなら、現実的な選択肢は次の 3 つです。

方法概要メリットデメリット
-Command に 1 行で埋め込む; で連結し、PowerShell を 1 文字列に閉じ込めるファイル不要。設定だけで完結長くなると地獄。クォート事故が起きやすい
-File で .ps1 を呼ぶスクリプトはプロジェクトに置き、ビルドイベントは起動だけ圧倒的に管理しやすい。レビューも容易ファイル配置とパス設計が必要
-EncodedCommand を使うPowerShell コードを Base64 化して渡すクォート地獄から解放される生成手順が必要。可読性は下がる

おすすめは基本的に 「-File で .ps1 を呼ぶ」です。どうしても 1 行で済ませたい場合だけ -Command、特殊なクォート事故を避けたいなら -EncodedCommand、という使い分けが実務では安定します。

-Command で複数行を 1 行に連結する(セミコロン方式)

質問にあるような “バージョン番号を分割してインクリメントする” 例を、Pre-build イベント向けに書き直すと次の形になります。

例:build.txt を読み込み、4番目の数値(build)だけ +1 して書き戻す

"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "& { $path = 'build.txt'; $content = Get-Content -LiteralPath $path -Raw; $parts = $content -split '\s+'; if($parts.Count -lt 4){ throw 'build.txt の形式が不正です(4要素必要)'; } $major=[int]$parts[0]; $minor=[int]$parts[1]; $patch=[int]$parts[2]; $build=[int]$parts[3]+1; $new = '{0} {1} {2} {3}' -f $major,$minor,$patch,$build; Set-Content -LiteralPath $path -Value $new -NoNewline -Encoding UTF8; Write-Host ('Updated: ' + $new) }"

ポイントは次の通りです。

  • PowerShell の本体は -Command の中に入れる(cmd.exe から隔離する)
  • & { ... }(スクリプトブロック)で囲むと、複数文をまとめやすい
  • PowerShell 文字列は基本 シングルクォートを使う(cmd 側のクォートと衝突しにくい)
  • Get-Content -Raw で 1 文字列として読み、分割する(行数や末尾改行の影響を受けにくい)
  • 形式が違う場合に throw して、ビルドを明確に落とす(サイレントに間違ったバージョンを作らない)

この方法でも動きますが、正直なところ 1 行が長くなった時点で保守性が急落します。次の節の -File 方式に切り替えると一気に安定します。

おすすめ:.ps1 ファイルに分離して -File で呼び出す

Pre-build イベントは “実行の入口” に徹し、処理はプロジェクト配下の .ps1 に逃がすのが一番ラクで事故が少ないです。

配置例

  • $(ProjectDir)build\prebuild.ps1 を作る(build フォルダは任意)
  • バージョン情報などの入出力ファイルもプロジェクト配下に置く(相対パス地獄を防ぐ)

Pre-build イベント欄(Visual Studio 2022)の例

"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$(ProjectDir)build\prebuild.ps1" -ProjectDir "$(ProjectDir)" -Configuration "$(ConfigurationName)"

ここでの重要点は、プロジェクトディレクトリを引数で渡すことです。ビルドイベントのカレントディレクトリは状況で揺れることがあり、相対パスだけに頼ると「自分の環境では動く」が再発します。引数で明示して、スクリプト側で必ずそれを起点にするのが安全です。

prebuild.ps1 の例(堅牢寄り)

param(
  [Parameter(Mandatory=$true)][string]$ProjectDir,
  [Parameter(Mandatory=$true)][string]$Configuration
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

# 例:Release のときだけ実行したい場合

if($Configuration -ne 'Release'){
Write-Host "Skip prebuild for configuration: $Configuration"
exit 0
}

$versionFile = Join-Path $ProjectDir 'build.txt'

if(-not (Test-Path -LiteralPath $versionFile)){
throw "ファイルが見つかりません: $versionFile"
}

$content = Get-Content -LiteralPath $versionFile -Raw
$parts = $content -split '\s+'

if($parts.Count -lt 4){
throw "build.txt の形式が不正です。例: '1 0 0 123'"
}

$major = [int]$parts[0]
$minor = [int]$parts[1]
$patch = [int]$parts[2]
$build = [int]$parts[3] + 1

$new = '{0} {1} {2} {3}' -f $major,$minor,$patch,$build

Set-Content -LiteralPath $versionFile -Value $new -NoNewline -Encoding UTF8
Write-Host "Updated build.txt: $new"

この形にしておくと、

  • スクリプトが長くなっても読みやすい(レビューもできる)
  • パスが安定する(Join-Path / $(ProjectDir) 起点)
  • 失敗時に理由が分かる(throw でビルドを止める)

という “ビルド周りで最も欲しい性質” が揃います。

クォート事故を根絶したいとき:-EncodedCommand を使う

cmd.exe と PowerShell のクォートが何重にも絡むと、たとえ -Command で囲っても、環境差や記号(&|() など)で壊れることがあります。そんなときに強いのが -EncodedCommand です。

-EncodedCommand は、PowerShell コードを UTF-16LE で Base64 エンコードして渡す方式で、cmd.exe のクォートや特殊文字の影響を受けにくくなります。

エンコード文字列の作り方(PowerShell で生成)

$code = @'
$content = Get-Content -LiteralPath "build.txt" -Raw
$parts = $content -split "\s+"
$build = [int]$parts[3] + 1
$parts[3] = $build
$new = ($parts[0..3] -join ' ')
Set-Content -LiteralPath "build.txt" -Value $new -NoNewline -Encoding UTF8
'@

$bytes = [Text.Encoding]::Unicode.GetBytes($code)
[Convert]::ToBase64String($bytes)

生成した Base64 を Pre-build イベントに貼ると、次のようになります(例)。

"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand BASE64文字列

ただし、この方式は “設定欄を見ただけでは何をやっているか分かりにくい” ので、チーム運用なら基本は -File を推奨します。EncodedCommand は、どうしても 1 行に閉じ込める必要がある場合の切り札、という位置づけが扱いやすいです。

実務で差が出る:Visual Studio のマクロを使ったパス設計

ビルドイベントで一番壊れやすいのは、ほぼ間違いなく “パス” です。カレントディレクトリの違い、スペースを含むパス、構成(Debug/Release)での出力先差分などが混ざると、再現が面倒なバグになります。

Visual Studio では、ビルドイベントに次のようなマクロ(MSBuild プロパティ)を埋め込めます。頻出のものを表にまとめます。

マクロ意味(ざっくり)よくある用途注意点
$(ProjectDir)プロジェクトファイルのあるフォルダスクリプトや設定ファイルの基準パス末尾に \ が付くことが多い。二重スラッシュに注意
$(SolutionDir)ソリューションのあるフォルダ複数プロジェクト共通のスクリプト配置ソリューション外でビルドされると値が変わることも
$(ConfigurationName)Debug / Release など構成で処理を分岐文字列比較は大小文字やカスタム構成に注意
$(TargetPath)ビルド成果物のフルパス成果物に対する後処理(署名、コピーなど)Pre-build ではまだ生成前のことがある(Post-build向き)
$(OutDir)出力ディレクトリ生成物の配置先を使った処理末尾スラッシュ有無に注意

結論として、ファイルを読む/書く処理をするなら、

  • ビルドイベント側で $(ProjectDir) を渡す
  • スクリプト側で Join-Path を使って組み立てる

この 2 段構えが、一番トラブルが減ります。

cmd.exe 側の落とし穴:特殊文字と複数条件分岐

「PowerShell の中は分かった。じゃあ cmd 側は何に気を付ければいい?」という話も重要です。Pre-build イベントは cmd.exe を経由するため、cmd の特殊文字が混ざると事故ります。

cmd.exe で特に危険な文字

  • &(コマンド連結)
  • |(パイプ)
  • < >(リダイレクト)
  • ()(ブロック)

PowerShell の -Command の中でも & は普通に使うため、見た目が似ていて混乱しがちです。基本戦略は次のどちらかです。

  • cmd の制御(if など)を極力書かず、PowerShell 側で分岐する
  • どうしても cmd で if を書くなら、まずはシンプルにし、複雑化する前に -File へ移行する

例:Release のときだけ prebuild.ps1 を走らせる(cmd 側で分岐)

if "$(ConfigurationName)"=="Release" "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$(ProjectDir)build\prebuild.ps1" -ProjectDir "$(ProjectDir)" -Configuration "$(ConfigurationName)"

ただし、分岐が増えるほど cmd の地雷が増えます。個人的には、分岐は PowerShell に寄せたほうが壊れにくいと感じます(先ほどの prebuild.ps1 例のように、Configuration を見てスキップする)。

ビルドを止める/止めないの判断:終了コードとエラーの扱い

Pre-build でよくある失敗は、「実は処理に失敗しているのに、ビルドが通ってしまい、後で気付く」パターンです。バージョン更新や生成物の差し替えなど、ビルドの品質に直結する処理なら、失敗時はビルドを落とした方が安全です。

PowerShell でビルドを確実に落とすには、次のどちらかを使います。

  • throw "エラーメッセージ"(例外を投げる)
  • exit 1(非 0 で終了する)

逆に「失敗してもビルドは続行したい」処理(ログ出力や任意のファイルコピーなど)であれば、try/catch で握りつぶすのも一手です。ただしこの場合は、何が起きたかを必ず出力して、気付ける状態にしておきましょう。

例:失敗は警告として出し、ビルドは継続する

try {
  # 何か処理
} catch {
  Write-Host ("[WARN] Prebuild failed: " + $_.Exception.Message)
  exit 0
}

品質に関わる処理かどうかで、落とす/継続するを決めるのが実務的です。

「PowerShell では動くのに VS では動かない」時に見るチェックリスト

原因が cmd.exe 経由だと分かっても、実際には複数要因が重なっていることがあります。切り分け用に、よく効くチェック項目をまとめます。

チェック項目見落としがちな理由対処
powershell.exe / pwsh の場所PATH 依存だと環境差が出るフルパスで呼ぶ(%SystemRoot%… 推奨)
実行ポリシー(ExecutionPolicy)開発PCは通るが別PCやCIで止まる-ExecutionPolicy Bypass / 署名 / 運用方針を決める
カレントディレクトリ手動実行時の場所とビルド時の場所が違う$(ProjectDir) を渡し、Join-Path で絶対パス化
クォート(” と ‘)cmd と PowerShell でルールが違う外側(cmd)は “、内側(PS文字列)は ‘ を基本にする
プロファイルの影響profile.ps1 で関数やエイリアスが変わる-NoProfile を付ける
エラーの握りつぶし失敗しているのにビルドが通る$ErrorActionPreference=’Stop’ と throw で明確化

さらに一段上:SDK-style(.NET)なら MSBuild ターゲット化も検討

Pre-build イベントは手軽ですが、複雑化すると設定欄に依存し、差分管理が難しくなります。SDK-style の .NET プロジェクト(多くの .NET 6/7/8/9 以降)なら、ビルドイベントではなく MSBuild ターゲットとして定義すると、構成管理や CI との整合が取りやすくなります。

たとえば .csproj に次のようなターゲットを追加し、Exec で PowerShell を呼ぶ方法です。

<Target Name="RunPrebuildScript" BeforeTargets="BeforeBuild">
  <Exec Command=' "%25SystemRoot%25\System32\WindowsPowerShell\v1.0\powershell.exe" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$(ProjectDir)build\prebuild.ps1" -ProjectDir "$(ProjectDir)" -Configuration "$(Configuration)" ' />
</Target>

このやり方なら、ビルド手順がプロジェクトファイルに含まれるため、チームでの再現性が上がります。「ビルドイベント欄に何が書かれていたっけ?」問題も起きにくくなります。

まとめ:Pre-build イベントに PowerShell を書くときの最適解

  • Pre-build イベントは cmd.exe 経由で実行されるため、PowerShell の複数行コードをそのまま貼るとエラー 9009 や「認識されません」が起きる
  • ビルドイベント欄には PowerShell のコードではなく、powershell.exe(または pwsh)を起動する 1 本のコマンドを書く
  • 短い処理なら -Command で 1 行化(& { ... }; 連結)
  • 実務で一番安定するのは -File で .ps1 を呼ぶ方式(パスは $(ProjectDir) を渡してスクリプト側で絶対化)
  • クォートの地雷を避けたいときは -EncodedCommand が有効

「動いたけど怖い」状態のまま放置すると、バージョン番号や生成物がズレて後工程で爆発します。Pre-build こそ、再現性と保守性を優先して “スクリプト分離+パス明示+失敗時は止める” の三点セットで固めるのがおすすめです。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次