PowerShellから外部exeを正しく呼ぶ ── 引数のクォート・終了コード・文字化けの落とし穴

· 更新日: · · PowerShell, Windows, 自動化, スクリプト, プロセス, 文字コード, 運用改善, バッチ処理

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

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

記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
まず自分の環境を確認する節を追加しました(7.3が境目で、7.0〜7.2は新方式に入っていません)。引用符を付ける関数の変換前後の対応表を7行追加し、バックスラッシュが次の文字によって扱いを変える非対称性を補足しました。デッドロックの発生順序の図と、UTF-8の出力をCP932で読んだときの文字化けの対応表も追加しています。
本文中の関連記事へのリンクの文言を、リンク先の現在のタイトルに揃えました。
見出しの「--%」が表示上ダッシュ1文字に変換され、本文中の表記と食い違って見えていた表示崩れを修正しました。
初版公開
この記事を引用する(DOI(登録済みアーカイブ): 10.5281/zenodo.21547433)

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

小村 豪(2026)「PowerShellから外部exeを正しく呼ぶ ── 引数のクォート・終了コード・文字化けの落とし穴」合同会社小村ソフト. https://comcomponent.com/blog/powershell-native-command-arguments/

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

「cmd.exeでは動くのに、PowerShellから呼ぶと引数が壊れる」。外部ツールや社内EXEを自動化するときは、処理内容より先に、引数が届くまでの経路を確認します。空白、引用符、空文字列、%、丸括弧は、その経路の違いが表れやすい値です。

さらに、直接呼び出しの引数渡しはPowerShell 7.3で変わっています。5.1向けの回避策を新方式へ持ち込むと二重エスケープになり、逆に新方式を前提にした値が5.1では壊れます。日本語の出力では、引数の問題とは別に文字コードも確認が必要です。1

この記事では、環境と呼び出し方を選ぶ → 引数を渡す → 出力と終了結果を受け取る → 届いた値を確認するの順に整理します。対象はWindows上の外部プログラムとの境界です。エラー処理全体は「PowerShellのエラー処理と再実行設計」を参照してください。

まず自分の環境を確認する

最初に、実際にスクリプトを動かすホストのバージョンを確認します。

$PSVersionTable.PSVersion

5.1.x はWindows PowerShell 5.1、7.x はPowerShell 7です。ただし、7.0〜7.2は、3章で説明する7.3以降の新しい引数渡しには含まれません。両方をインストールしていても、挙動を決めるのは実行中のホストです。

1. まず結論 ── 呼び出し方と確認するものを分ける

通常の呼び出しは & $exe @args を第一候補にします。ただし、5.1で空文字列や引用符を含む値を渡すときは、配列だけでは解決しません。 この場合は、5章の ProcessStartInfo.Arguments と引用符付けを使い分けます。12

判断すること 基本方針 詳細
どの引数を、どの環境から渡すか まず配列。5.1の空文字列・引用符は別対応。7では ProcessStartInfo.ArgumentList も使える 2〜5章
固定文字列や特別な起動条件があるか --% は固定文字列用。別ウィンドウ・別ユーザー・昇格は Start-Process 4章・5.5節
出力をどう受け取り、成否をどう決めるか 文字コードは送受信で分け、終了コードとstderrも別に扱う 6〜7章
書き方を変えても直らないか エスケープを増やす前に、バインドと受け取った引数を確認する 8章

--% は変数を渡す方法ではなく、Start-Process は引用符の問題を一律に解決する方法でもありません。呼び出し方を選ぶことと、引数が保たれる条件を確認することは、別の作業です。13

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

外部コマンド呼び出しの確認順序実行環境と引数の内容から呼び出し方を選び、出力と終了結果を確認します。実行するホストを確認引数の値と起動条件を確認呼び出し方を選ぶ出力と終了コードを確認不明なら届いた値を調べる

図1: 実行環境と引数の内容から呼び出し方を選び、出力と終了結果を確認します。

2. なぜ引数が化けるのか ── 引数モードという前提

2.1 まずPowerShellが記号を解釈する

PowerShellはコマンド行をトークンに分解し、式モードと引数モードのどちらかで解釈します。コマンド呼び出しが現れると、それ以降は引数モードで解析されます。引数モードでは、入力は基本的に「展開可能な文字列」として扱われ、$ 始まりは変数参照、引用符は文字列の開始、( ) は式の開始……といった具合に、記号に構文上の意味があります。1

引数モードでは、空白を含む値は引用符で囲みます。, ( ) { } | & < > @ # などのメタ文字をリテラルとして渡すときは、PowerShell側のエスケープも考えます。1

つまり、コマンドプロンプトにそのまま貼れば動く文字列でも、PowerShellでは別のものとして解釈されうる、ということです。古典的な例が icacls です。1

# cmd.exeなら通るが、PowerShell 2.0時代はカッコが式として解釈されてエラーになった
icacls X:\VMS /grant Dom\HVAdmin:(CI)(OI)F

# バッククォートでメタ文字をエスケープする(読みにくい)
icacls X:\VMS /grant Dom\HVAdmin:`(CI`)`(OI`)F

# 停止解析トークンで「ここから先はリテラル」と宣言する(PowerShell 3.0以降)
icacls X:\VMS --% /grant Dom\HVAdmin:(CI)(OI)F
構文としての記号と渡したい値PowerShellが解釈する記号と、外部プログラムへ渡したい文字を区別します。PowerShellへ書いたコマンド引数モードで解析変数や記号の意味を解釈外部プログラムへ渡す値

図2: PowerShellが解釈する記号と、外部プログラムへ渡したい文字を区別します。

2.2 解析した後にも引数の組み立てがある

もう一つの前提が、解析後の引数がプログラムへ渡る経路です。Windows PowerShell 5.1では、解析された引数はスペース区切りで1本の文字列に組み立て直されてからプロセスに渡されます。この「組み立て直し」の過程で、引数の中に含まれていた引用符が落ちたり、空文字列の引数が消えたりする ── これが5.1時代の定番の事故です。1

従来方式で引数が変わる場所5.1では解析した値を一つのコマンドラインへ組み立て直すため、空文字列や埋め込み引用符が失われます。PowerShellが解析した値従来方式で一文字列へ結合外部プロセスへ渡す空文字列や引用符が変わる

図3: 5.1では解析した値を一つのコマンドラインへ組み立て直すため、空文字列や埋め込み引用符が失われます。

3. PowerShell 7.3の破壊的変更 ── $PSNativeCommandArgumentPassing

PowerShell 7.3で、この組み立て方が変更されました。公式が明確に「Windows PowerShell 5.1の挙動からの破壊的変更」と書いている箇所です。1

3.1 三つのモードとWindowsの互換例外

新しい挙動は $PSNativeCommandArgumentPassing 環境設定変数で切り替えられ、値は Legacy(従来)・Standard・Windows の3つ。Windowsプラットフォームの既定値は Windows、非Windowsは Standard です。14

Windows と Standard の違いは1点だけで、Windows モードのときは以下の呼び出しが自動的に Legacy 方式になります。1

Legacy方式が自動適用される呼び出し
cmd.exe / cscript.exe / wscript.exe
拡張子が .bat .cmd .js .vbs .wsf のファイル

古いバッチやWSHスクリプトが「PowerShellを7に上げた途端に引数の受け取り方が変わって壊れる」事故を防ぐための例外です。逆に言うと、$PSNativeCommandArgumentPassing を Standard や Legacy に明示設定すると、この判定は行われなくなります。1

Windowsモードの互換分岐7.3以降のWindowsモードは、指定された旧来のホストや拡張子だけをLegacy方式へ切り替えます。はいいいえWindowsモードで呼び出す互換対象のホスト・拡張子かLegacy方式新しい方式Standard・Legacyの明示設定この自動判定は行わない

図4: 7.3以降のWindowsモードは、指定された旧来のホストや拡張子だけをLegacy方式へ切り替えます。

3.2 埋め込み引用符と空文字列が保持される

新方式で改善されるのは次の2点です。1

以下の例に出てくる TestExe -echoargs は、受け取った引数を Arg 0 is <...> の形で1つずつ表示するだけの検証用ツールです。PowerShell本体のテスト資産に含まれるもので、Windowsに標準で入っているコマンドではありません。手元で同じことを確かめる方法は8章にまとめてあるので、読み進めるうえでは「引数がそのまま見える窓」だと思ってください。

# (1) 文字列に埋め込んだ引用符が保持される
$a = 'a" "b'
TestExe -echoargs $a 'c" "d' e" "f
# Arg 0 is <a" "b>
# Arg 1 is <c" "d>
# Arg 2 is <e f>

# (2) 空文字列の引数が消えずに残る
TestExe -echoargs '' a b ''
# Arg 0 is <>
# Arg 1 is <a>
# Arg 2 is <b>
# Arg 3 is <>

3.3 PowerShellのエスケープと下位の規則を混ぜない

"C:\Program Files (x86)\Microsoft\" のような引用符付きのパス文字列をそのまま渡したい場合、Windows / Standard モードなら素直に書けます。1

# 7.3以降(Windows / Standardモード)
TestExe -echoargs '"C:\Program Files (x86)\Microsoft\"'

# 同じ結果をLegacyモード(5.1相当)で得るには、引用符の二重エスケープが要る
TestExe -echoargs "\""C:\Program Files (x86)\Microsoft\\"""

ここで一つ注意があります。バックスラッシュ(\)はPowerShellのエスケープ文字ではありません。上の例で \" が登場するのは、下位の.NET API(ProcessStartInfo.ArgumentList)がバックスラッシュをエスケープ文字として扱うためで、PowerShellの構文としてのエスケープはバッククォート(`)です。この2種類が混ざるのが、この分野が難しく見える最大の理由です。12

5.1と7が混在する環境では、引用符を含む引数の回避策を、そのまま両方へ流用しないでください。5章では、通常の値を配列で渡す場合と、5.1の空文字列・引用符を ProcessStartInfo で扱う場合を分けます。5.1と7の共存方針そのものは「Windows PowerShell 5.1とPowerShell 7の違い」を参照してください。

異なる層のエスケープPowerShellのバッククォートと、下位の引用符付けで使うバックスラッシュは、別の層の規則です。PowerShellの構文バッククォートでエスケープ下位のコマンドライン規則引用符とバックスラッシュどの層で値が変わるか確認

図5: PowerShellのバッククォートと、下位の引用符付けで使うバックスラッシュは、別の層の規則です。

4. --%(停止解析トークン)の正しい使いどころと限界

--% は、それ以降の文字をPowerShellが解釈せずそのまま渡すためのトークンです(PowerShell 3.0以降)。公式は「Windowsプラットフォームのネイティブコマンドでの使用のみを想定」と明記しています。1

PS> cmd /c echo "a|b"
'b' is not recognized as an internal or external command,
operable program or batch file.

PS> cmd /c --% echo "a|b"
"a|b"

4.1 固定文字列でも展開と効果範囲を確認する

強力ですが、制約は正確に知っておく必要があります。1

制約 内容
環境変数だけは展開される %USERPROFILE% のような %<名前>% は必ず展開される。%% によるエスケープは不可。未定義名はそのまま通る
PowerShell変数は使えない $path などは展開されず、リテラル文字列として渡る
効果範囲 次の改行かパイプ(|)まで。バッククォートによる行継続で延長できず、; で終了もできない
リダイレクト不可 >file.txt などは引数としてそのまま対象コマンドに渡ってしまう

つまり --% が使えるのは「渡す内容が完全に固定文字列で、% を含まない」場合に限られます。スクリプトで変数を組み立てて渡す典型的な自動化では、この条件はまず満たせません。「とりあえず --% を付ける」という運用は、% を含むパスワードやワイルドカードを渡した瞬間に破綻します。

停止解析トークンを選べる条件停止解析トークンは変数を使う自動化の代わりではなく、固定文字列で制約を満たす場合に使います。必要不要PowerShell変数が必要か配列やProcessStartInfoを使う固定文字列の内容を確認パーセント展開と効果範囲制約を満たすなら停止解析

図6: 停止解析トークンは変数を使う自動化の代わりではなく、固定文字列で制約を満たす場合に使います。

4.2 バッチへ信頼できない入力を連結しない

なお公式ドキュメントは、バッチファイルに信頼できない入力を渡さないよう警告しています。バッチへの引数は生のコマンドライン文字列としてcmd.exeへ渡るためです。1 ユーザー入力やファイル名をバッチに連結して渡す設計は、コマンド注入の温床になります。値は一時ファイルや環境変数で受け渡すか、バッチ自体をPowerShellに移行してください(「そのバッチファイル、PowerShellに移行すべき?」)。

バッチへ渡す値の経路信頼できない入力をバッチのコマンドラインへ連結せず、別の受け渡し経路やPowerShellへの移行を選びます。ユーザー入力やファイル名バッチの引数へ連結しない一時ファイルや環境変数で渡すバッチをPowerShellへ移す

図7: 信頼できない入力をバッチのコマンドラインへ連結せず、別の受け渡し経路やPowerShellへの移行を選びます。

5. 変数を確実に渡す ── 配列とProcessStartInfoを選ぶ

5.1 先に5.1での例外を押さえる

Windows PowerShell 5.1では、配列スプラッティングも従来のコマンドライン再構築を通ります。 空白や日本語を含む通常の値には使えますが、空文字列は消え、引用符を含む値は変形します。まずこの条件で使い分けます。1

ここでいう「通常の値」には、空白を含み末尾がバックスラッシュのパスを含めません。 5.1では、閉じ引用符の直前に来るバックスラッシュにも注意が必要です。5.4節の変換表で説明する規則に沿って Arguments を組み立て、8章の方法で受け取った値を確認してください。空文字列や埋め込み引用符だけが確認対象ではありません。12

5.1で渡す引数 方法
空白や日本語を含む通常の値 配列スプラッティング(& $exe @args)で十分
空文字列、引用符を含む値 ProcessStartInfo + 上記のエスケープ、または --%(固定文字列のみ)

7.3以降の新方式ではこの二つの問題が解消されます。ただし、3章の Legacy 設定や Windows モードの互換例外は別です。「PowerShell 7だから全部同じ」ではなく、バージョンと適用される方式を確認します。1

変数の値から渡し方を選ぶ5.1では空文字列・引用符・末尾のバックスラッシュなどに注意し、7ではArgumentListも選択肢になります。はいいいえ変数の値を渡したい5.1で注意する値があるかArgumentsへ正しく引用符付け配列で指定を始める7ではArgumentListも使える

図8: 5.1では空文字列・引用符・末尾のバックスラッシュなどに注意し、7ではArgumentListも選択肢になります。

5.2 通常の値は配列に一引数ずつ入れる

変数の値を含む引数を渡すときの第一候補は、配列に1引数ずつ入れてスプラッティングする書き方です。通常の値なら、配列の各要素を一つの引数として指定できるため、空白を含むパスも引用符を自分で書く必要がありません。

$exe  = 'C:\Program Files\MyTool\convert.exe'
$args = @(
    '--input',  'D:\受注データ\2026年07月.csv'   # 空白や日本語を含んでも問題ない
    '--output', 'D:\出力\result.json'
    '--mode',   'strict'
)

& $exe @args                      # 配列スプラッティング。各要素が1引数になる
if ($LASTEXITCODE -ne 0) { throw "変換に失敗しました (ExitCode=$LASTEXITCODE)" }

呼び出し演算子 & は、パスに空白を含むexeを実行するときにも必要です('C:\Program Files\...' はそのままでは文字列リテラルとして評価されるだけで実行されません)。

渡ったかどうかが不安なら、推測でエスケープを増やす前に8章の方法で実際の引数を確認してください。書き方を変える前と後で、届いた引数が同じかどうかを見るのが最短です。

実行ファイルと引数配列実行ファイルのパスと引数の値を別々に持ち、呼び出し演算子で実行します。実行ファイルのパス呼び出し演算子で実行一要素ずつ指定した引数配列終了コードを確認

図9: 実行ファイルのパスと引数の値を別々に持ち、呼び出し演算子で実行します。

5.3 PowerShell 7ではArgumentListで引数を組み立てる

引数を一つずつ制御したい、出力の文字コードをプロセス単位で指定したい場合は、.NETの ProcessStartInfo を直接使います。ArgumentList へ値を追加すれば、必要な引用符付けは.NET側が行います。自分で引用符を重ねたり、PowerShellの従来方式で一つの文字列へ組み立て直したりする必要がありません。2

ArgumentList は.NET Core 2.1以降のAPIです。.NET Framework上で動くWindows PowerShell 5.1には存在しません。 5.1で空文字列や引用符を扱う方法は5.4節に分けます。2

リダイレクトするときは、読み始めてから待つ

引数の指定とは別に、標準出力と標準エラーの読み取り順序も設計します。

出力のリダイレクトで最も多い事故がデッドロックです。パイプのバッファには上限があり、埋まると子プロセスは書き込みでブロックします。したがって、WaitForExit() を先に呼んで後から読むのはもちろん、片方のストリームを ReadToEnd() で同期的に読み切ってからもう片方を読む書き方も危険です(読んでいない側のバッファが先に埋まると、子プロセスが止まり、ReadToEnd() は永久に返りません)。

同期読み取りで停止する順序親がstderrだけを読み切ろうと待つ間にstdoutが満杯になると、子が終了できず親の待機も終わりません。親はstderrの終端を待つ親も待機したまま止まる子がstdoutへ書き続ける未読のstdoutが満杯子が書き込みで停止stderrは閉じない

図10: 親がstderrだけを読み切ろうと待つ間にstdoutが満杯になると、子が終了できず親の待機も終わりません。

次の例は、両方を非同期で読み始めてから終了を待ち、最後に結果を取り出す構成です。

# 【PowerShell 7以降】引数の組み立てを.NETに任せる
$psi = [System.Diagnostics.ProcessStartInfo]::new()
$psi.FileName = 'C:\Program Files\MyTool\convert.exe'
foreach ($a in '--input', $inputPath, '--output', $outputPath) {
    $psi.ArgumentList.Add($a)     # 1要素=1引数。引用符は自分で書かない(7専用)
}
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError  = $true
$psi.UseShellExecute        = $false
# 出力の文字コードを明示できるのもProcessStartInfoの利点(6章)
$psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
$psi.StandardErrorEncoding  = [System.Text.Encoding]::UTF8

$proc = [System.Diagnostics.Process]::Start($psi)

# 標準出力と標準エラーは「同時に」読み出す。片方を同期的に読み切ってから
# もう片方を読むと、待っている間にもう一方のパイプバッファが埋まって
# 子プロセスが書き込みでブロックし、そのままデッドロックする
$stdoutTask = $proc.StandardOutput.ReadToEndAsync()
$stderrTask = $proc.StandardError.ReadToEndAsync()
$proc.WaitForExit()
$stdout = $stdoutTask.GetAwaiter().GetResult()
$stderr = $stderrTask.GetAwaiter().GetResult()

if ($proc.ExitCode -ne 0) {
    throw "変換に失敗 (ExitCode=$($proc.ExitCode)): $stderr"
}
両方の出力を読み始めてから待つ二つの非同期読み取りを開始してからプロセスの終了を待ち、それぞれの結果と終了コードを確認します。プロセスを起動stdoutの非同期読み取り開始stderrの非同期読み取り開始プロセスの終了を待つ両方の結果と終了コードを取得

図11: 二つの非同期読み取りを開始してからプロセスの終了を待ち、それぞれの結果と終了コードを確認します。

片方だけをリダイレクトする設計も選択肢です。子プロセスの扱い全般は「Windowsアプリで子プロセスを安全に扱うチェックリスト」を参照してください。

5.4 Windows PowerShell 5.1ではArgumentsを組み立てる

5.1では ArgumentList がないため、Arguments に引用符を付けた一つの文字列を設定します。この手組みには細かい規則があるので、通常の値は5.2節の配列を使い、空文字列や引用符が必要な場合にこの方法を選びます。

次のコードは、5.3節の例と組み合わせるための引数の組み立て部分です。5.1では ArgumentList.Add のループは実行せず、代わりにこの Arguments を設定します。実行ファイル、リダイレクト、起動と待機などの設定は別途必要です。

# 【5.1】ArgumentListがないため、引用符を含む1本の文字列を自分で組み立てる
$quote = {
    param([string] $s)
    if ($s -eq '') { return '""' }                   # 空文字列は "" にしないと引数ごと消える
    if ($s -notmatch '[\s"]') { return $s }          # 囲む必要がなければそのまま

    # Windowsのコマンドライン解析規則に合わせる:
    #   (1) " の直前のバックスラッシュ列を2倍にし、" 自体を \" にする
    #   (2) 末尾のバックスラッシュ列も2倍にする。閉じ引用符の直前に来るため、
    #       そのままだと \" と解釈されて引用符が閉じず、後続の引数まで壊れる
    #       (例: 'C:\Program Files\input\' → "C:\Program Files\input\\")
    $e = $s -replace '(\\*)"', '$1$1\"'
    $e = $e -replace '(\\+)$', '$1$1'
    '"' + $e + '"'
}
$psi.Arguments = (@('--input', $inputPath, '--output', $outputPath) |
                  ForEach-Object { & $quote $_ }) -join ' '

正規表現だけ見ても何をしているのか掴みにくいので、入力と出力の対応を並べます。この7例が分かれば、規則そのものを覚える必要はありません。

渡したい値(変数の中身) $quote が返す文字列 効いている規則
strict strict 空白も引用符もないので、囲まずそのまま返す
空文字列 "" 何も書かないと引数ごと消えるため、空の引用符を置く
D:\受注データ\2026年07月.csv D:\受注データ\2026年07月.csv 日本語が入っていても、空白がなければ囲まない
C:\Program Files\input "C:\Program Files\input" 空白があるので、全体を引用符で囲むだけ
C:\Program Files\input\ "C:\Program Files\input\\" 規則(2)。末尾の \ を2倍にする。1つのままだと閉じ引用符とくっついて \" と読まれ、引用符が閉じないまま次の引数まで巻き込む
say "hi" "say \"hi\"" 規則(1)。値の中の " を \" にして、区切りではなく文字として渡す
a\"b "a\\\"b" 規則(1)の全体像。" の直前にある \ の並びをまず2倍にし、そのうえで " を \" にする

最後の行が、この関数がややこしく見える理由そのものです。バックスラッシュは「次が " のときだけ」エスケープ文字として働くという、Windowsのコマンドライン解析規則の非対称性に合わせているためこうなります。

引用符付け関数の分岐空文字列を明示し、囲む必要のある値では引用符直前と末尾のバックスラッシュを処理します。はいいいえ含まない含む渡したい文字列空文字列か空の引用符を返す空白か引用符を含むかそのまま返す引用符直前と末尾を処理全体を引用符で囲む

図12: 空文字列を明示し、囲む必要のある値では引用符直前と末尾のバックスラッシュを処理します。

5.5 Start-Processは起動の仕方を変えたいときに使う

出力をパイプラインで受け取る、終了コードを調べるだけなら、通常は直接呼び出しを使います。Start-Process は、別ウィンドウ、別ユーザー、管理者への昇格(-Verb RunAs)、標準出力・標準エラーのファイルへのリダイレクトなど、起動の仕方を制御するときの選択肢です。3

既定では終了を待ちません。 終了コードが必要なときは -Wait -PassThru を併用し、返されたProcessオブジェクトの ExitCode を確認します。直接呼び出しの $LASTEXITCODE と、取得する場所を混同しないでください。3

また、Start-Process -ArgumentList と、5.3節の .NET の ProcessStartInfo.ArgumentList は別物です。前者は配列を指定しても空白で結合するため、空白や引用符を含む引数の引用符付けは別途考える必要があります。引数を厳密に扱いたいという理由だけで、Start-Processへ置き換えないでください。3

起動方法と終了結果の取得特別な起動条件にはStart-Processを選び、終了コードは待機して取得したProcessオブジェクトから読みます。通常の呼び出し直接呼び出してLASTEXITCODE別ウィンドウ・別ユーザー・昇格Start-Processで起動WaitとPassThruを併用ProcessのExitCodeを読む

図13: 特別な起動条件にはStart-Processを選び、終了コードは待機して取得したProcessオブジェクトから読みます。

6. 文字化けを直す ── [Console]::OutputEncodingと$OutputEncoding

6.1 受信と送信で設定を分ける

日本語環境で問題になりやすいのが文字化けです。ポイントは、方向によって使われる設定が違うことです。

方向 使われる設定 症状
外部コマンドの出力をPowerShellが受け取る [Console]::OutputEncoding UTF-8出力のツールの結果が「譁�喧縺�」のように化ける
PowerShellから外部コマンドへパイプで文字列を送る $OutputEncoding 送った日本語が相手側で化ける
出力の受信とパイプへの送信外部コマンドから受け取る場合と、PowerShellから文字列を送る場合では使う設定が違います。外部コマンドの出力バイト列Console.OutputEncodingで復号PowerShellの文字列OutputEncodingで符号化別の外部コマンドへ送信

図14: 外部コマンドから受け取る場合と、PowerShellから文字列を送る場合では使う設定が違います。

6.2 文字化けの形から復号の食い違いを確認する

化け方は、元の文字列と並べて見ると一気に分かりやすくなります。UTF-8で出力された日本語をCP932(Shift_JIS)として復号すると、次のようになります。

本来の文字列 UTF-8出力をCP932で復号した結果
こんにちは 縺薙s縺ォ縺。縺ッ
エラー 繧ィ繝ゥ繝シ
日本語 譌・譛ャ隱 + 復号できないバイト

ひらがな・カタカナが 縺 繧 繝 で始まる2文字の組に化けるのが目印です(UTF-8のひらがな・カタカナが E3 81 E3 82 E3 83 で始まり、その先頭2バイトがCP932ではこれらの文字に当たるためです)。漢字のように復号できないバイトが混ざる場合は、置換文字になったり文字が脱落したりして、上の表の3行目のように長さも合わなくなります。

$OutputEncoding は「PowerShellが文字列をネイティブコマンドへ送るときに使うエンコーディング」を決める環境設定変数です。4 一方、外部コマンドが吐いたバイト列を文字列に復号するのは [Console]::OutputEncoding の役目です。片方だけ直して「まだ化ける」となるのは、この2つを混同しているケースがほとんどです。

6.3 一時的な変更は元に戻す

次の例はUTF-8のツールに合わせる場合です。設定を変える前に、相手が実際に使うエンコーディングを確認してください。

# UTF-8で出力する外部ツールを、Windows PowerShell 5.1から呼ぶときの定番対処
$prevOut = [Console]::OutputEncoding
$prevPs  = $OutputEncoding
try {
    [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)  # BOMなしUTF-8
    $OutputEncoding           = [System.Text.UTF8Encoding]::new($false)
    $result = & $exe --list
}
finally {
    # セッション全体に影響するので必ず戻す
    [Console]::OutputEncoding = $prevOut
    $OutputEncoding           = $prevPs
}

ProcessStartInfo を使う場合は、前章のとおり StandardOutputEncoding / StandardErrorEncoding をプロセス単位で指定できるため、セッションの設定を触らずに済みます。副作用が小さいのはこちらです。Windows全般の文字コード事情は「Windowsの文字コードと改行コード」にまとめてあります。

文字コード設定の影響範囲セッションの設定を変更するならfinallyで復元し、ProcessStartInfoならプロセス単位で出力の復号を指定します。どの単位で設定するかセッションの設定を一時変更finallyで元へ戻すProcessStartInfoで出力を指定セッションの設定は変えない

図15: セッションの設定を変更するならfinallyで復元し、ProcessStartInfoならプロセス単位で出力の復号を指定します。

7. 終了コードとstderr ── 「成功したのに失敗扱い」を防ぐ

外部プログラムの成否判定は $LASTEXITCODE で行います。非ゼロ終了コードは既定ではErrorRecordを生成せず、try/catchにも入りません。5 この基本は「PowerShellのエラー処理と再実行設計」で詳しく扱ったので、ここでは外部プロセス固有の2点だけ補足します。

7.1 stderrの出力と終了コードを別々に見る

stderrへの出力は「失敗」ではありません。多くのCLIツールは進捗やログをstderrに書きます。PowerShellはネイティブコマンドのstderr出力をエラーストリームに流すため、画面が赤くなって「失敗した」ように見えますが、終了コードが0なら成功です。Windows PowerShell 5.1では、stderrに書かれただけで $? が $false になることがありましたが、PowerShell 7では非ゼロ終了コードのときだけ $false になるよう修正されています。6

診断出力と成否を分けるstderrには進捗やログも出るため、出力の有無だけで失敗扱いせず、終了コードを確認します。外部コマンドの実行結果stdoutとstderrを記録終了コードで成否を判定stderrがあるだけで失敗にしない

図16: stderrには進捗やログも出るため、出力の有無だけで失敗扱いせず、終了コードを確認します。

7.2 出力元を区別したいなら合流させない

2>&1 で合流させた後の型はバージョンで違います。Windows PowerShell 5.1ではstderrの各行がErrorRecordとして混ざりますが、PowerShell 7.4以降はネイティブコマンドのリダイレクト出力がバイトストリームとして扱われ、合流後は文字列データになります。78 つまり「ErrorRecordかどうかで仕分ける」書き方は7.4以降では機能しません。両方の出力が必要なら、合流させずに別々に受け取るのが確実です。

コードの前半は分離して扱う例、後半は区別せずログへまとめる場合の別例です。同じ処理を二度実行する手順ではありません。

# 【推奨】stdoutとstderrを分けて受け取る(バージョン差の影響を受けない)
$errFile = [System.IO.Path]::GetTempFileName()
try {
    $stdout = & $exe --import $csvPath 2> $errFile
    $stderr = Get-Content -Path $errFile -Raw

    # ログには両方残す
    $stdout | Add-Content -Path $logPath -Encoding utf8
    if ($stderr) { $stderr | Add-Content -Path $logPath -Encoding utf8 }

    if ($LASTEXITCODE -ne 0) {
        throw "インポート失敗 (ExitCode=$LASTEXITCODE): $stderr"
    }
    # ここに来れば成功。stderrに出力があっても失敗扱いしない
}
finally {
    Remove-Item $errFile -ErrorAction SilentlyContinue
}

# 【区別が不要な場合】単に全部まとめてログに落とすなら合流でよい
(& $exe --import $csvPath 2>&1) | ForEach-Object { $_.ToString() } |
    Add-Content -Path $logPath -Encoding utf8
出力を分けるかまとめるか出力元の区別が必要なら別々に受け取り、区別せず記録するだけなら合流を選べます。必要不要stdoutとstderrを区別するか別々に受け取る合流してログへ記録成否判定は終了コード

図17: 出力元の区別が必要なら別々に受け取り、区別せず記録するだけなら合流を選べます。

8. 実際に何が渡ったかを確認する

8.1 7.3以降では引数バインドをトレースする

推測でエスケープを増やすと事態は悪化します。実際に渡った引数を見るのが最短です。PowerShell 7.3以降は、ネイティブコマンドの引数バインドを Trace-Command でトレースできます。19

Trace-Command -Name ParameterBinding -PSHost -Expression {
    & $exe --input 'D:\受注データ\2026年07月.csv' --mode strict
}
# DEBUG: ... BIND cmd line arg [--input] to position [0]
# DEBUG: ... BIND cmd line arg [D:\受注データ\2026年07月.csv] to position [1]

8.2 呼び出し先が受け取った値も確認する

呼び出し先が自作ツールなら、受け取った args をそのまま出力する検証用モードを1つ用意しておくと、この手の調査が一瞬で終わります(PowerShellのテストツール群にある TestExe -echoargs と同じ発想です)。1 現行の5.1環境で同じことをしたい場合は、$args を列挙するだけの小さな .ps1 や、引数をそのまま表示するだけの小さなEXEを1本用意しておけば十分です。

調べたいのがネイティブEXEへの渡り方なら、同じ呼び出し経路を通るEXE側の引数表示で確かめます。.ps1 の $args 表示と、EXEが最終的に受け取る引数の確認は、調べている境界を分けて使ってください。

推測より先に受け取った値を比べる渡したい値と呼び出し先が受け取った値を比較し、書き方を変更した後も同じ条件で確かめます。渡したい値を決めるバインドをトレース(7.3以降)呼び出し先で引数を表示期待した値と比較修正後も同じ条件で確認

図18: 渡したい値と呼び出し先が受け取った値を比較し、書き方を変更した後も同じ条件で確かめます。

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

配列を第一候補とする場合も、5.1節の例外を先に確認します。空白を含むパスの末尾など、引用符を付けた後の形まで考える必要がある値は、5.4節の方法と受け取り側の確認を組み合わせてください。

状況 選択肢 判断の目安
固定文字列の引数(% を含まない) --% / 通常呼び出し エスケープが煩雑なら --% が最短。ただし変数は使えない1
変数の値を渡す 配列スプラッティング & $exe @args 第一候補。空白・日本語・記号を含んでも自分で引用符を書かない
5.1と7の両方で同じ書き方にしたい 配列スプラッティング 書き方は共通だが、5.1では空文字列や引用符を含む引数が壊れる(下記の注記)1
5.1で空文字列・引用符を含む引数を渡す ProcessStartInfo + 自前エスケープ 5.1のコマンドライン再構築を経由しないため確実(5章)
引数を1つずつ完全に制御したい(7専用) ProcessStartInfo + ArgumentList 引用符の組み立てを.NETに任せられる。.NET Core 2.1以降のAPI2
別ウィンドウ・別ユーザー・昇格が必要 Start-Process -Wait -PassThru で ExitCode を取得。出力はファイルへリダイレクト3
バッチ(.bat)に値を渡す 環境変数・一時ファイル経由 信頼できない入力を引数で渡さない(公式警告)1
出力が文字化けする [Console]::OutputEncoding(受信)/ $OutputEncoding(送信) 方向で設定が違う。プロセス単位なら StandardOutputEncoding4
成否判定 $LASTEXITCODE 非ゼロは既定でcatchに入らない。stderrへの出力は失敗を意味しない56
stdoutとstderrを区別したい 別々にリダイレクトして受け取る 2>&1 合流後の型は5.1と7.4以降で異なる78
何が渡ったか分からない Trace-Command -Name ParameterBinding 推測でエスケープを増やす前に実測する9

まとめ ── 引数・出力・終了結果を順に切り分ける

最初に、実行中のPowerShellと適用される引数渡し方式を確認します。通常の値は配列へ一つずつ入れ、5.1の空文字列・引用符は別対応にします。固定文字列に使う --%、起動方法を変える Start-Process、引数と出力を制御する ProcessStartInfo は、それぞれ目的が違います。

そのうえで、出力をリダイレクトするなら読み取りと待機の順序を整え、文字コードを送受信で合わせます。stderrの有無ではなく終了コードで判定し、出力元を残したいなら合流させません。 迷ったときは引用符を増やす前に、実際に届いた引数を確認してください。

外部プロセスとの境界の最終確認引数が届くこと、出力を正しく読めること、終了結果を判定できることを別々に確認します。引数の値が保たれる出力を滞らず読み取れる文字コードが合っている終了コードで判定できる実行環境ごとに確かめる

図19: 引数が届くこと、出力を正しく読めること、終了結果を判定できることを別々に確認します。

サンプルコードのダウンロード

この記事で扱ったコードは、そのまま動かせる形にまとめて配布しています。引数の引用符付けモジュールと、ProcessStartInfo による実行が入っています。

サンプルコードをダウンロード(zip)

この記事のサンプルは、PowerShell 7.6 で実際に実行して検証しています(Pester 22件)。zipに含まれる Invoke-SampleTests.ps1 を実行すれば、お手元でも同じ検証を再現できます。

# 構文解析 + 静的解析 + Pesterテスト
./Invoke-SampleTests.ps1

設定値(パス、サーバー名、テナントIDなど)は例です。そのまま本番環境で実行せず、自社の環境に合わせて読み替えてください。

関連記事

関連する相談領域

合同会社小村ソフトでは、バッチ資産のPowerShell化、外部ツール・社内EXEを組み合わせた自動化処理の設計、「環境によって動いたり動かなかったりする」スクリプトの原因調査を扱っています。

参考リンク

  1. Microsoft Learn, about_Parsing. 式モードと引数モードの区別、引数モードのメタ文字、バッククォートによるエスケープ、ネイティブコマンドへ渡す引数が解析後にスペース区切りの1文字列へ結合されること、PowerShell 3.0以降の停止解析トークン --% の仕様(環境変数のみ展開、%% エスケープ不可、効果は改行かパイプまで、リダイレクト不可)、PowerShell 7.3でネイティブコマンドのコマンドライン解析が変更された破壊的変更、$PSNativeCommandArgumentPassing の値(Legacy/Standard/Windows)とWindowsでの既定値、Windowsモードでcmd.exe・cscript.exe・wscript.exeおよび.bat/.cmd/.js/.vbs/.wsfがLegacy方式になること、バックスラッシュがPowerShellのエスケープ文字ではないこと、バッチファイルへ信頼できない入力を渡さないよう警告していること、7.3でネイティブコマンドの引数バインドをトレースできるようになったことについて。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19 ↩20 ↩21 ↩22 ↩23 ↩24 ↩25

  2. Microsoft Learn, ProcessStartInfo.ArgumentList プロパティ. 引数をコレクションとして個別に指定でき、必要な引用符付けやエスケープをランタイムが行うこと、バックスラッシュがエスケープ文字として扱われること、および適用対象が.NET Core 2.1以降(.NET Framework には存在しない)であるため、.NET Framework上で動作するWindows PowerShell 5.1では利用できないことについて。あわせてProcess.StandardOutput プロパティの、標準出力と標準エラーの両方をリダイレクトして同期的に読む場合に発生し得るデッドロックと、その回避方法(一方を非同期で読む)についての注意事項も参照。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  3. Microsoft Learn, Start-Process. 既定では新しいプロセスの完了を待たないこと、-Wait による待機、-PassThru によるProcessオブジェクトの取得と ExitCode、-RedirectStandardOutput / -RedirectStandardError によるファイルへのリダイレクト、-Verb RunAs による昇格、-Credential による別ユーザー実行について。 ↩ ↩2 ↩3 ↩4 ↩5

  4. Microsoft Learn, about_Preference_Variables. $PSNativeCommandArgumentPassing がプラットフォーム依存の既定値を持つ環境設定変数であること、$OutputEncoding がPowerShellから他のアプリケーションへ文字列を送るときに使われるエンコーディングを決めることについて。 ↩ ↩2 ↩3

  5. Microsoft Learn, about_Error_Handling. ネイティブコマンドの非ゼロ終了コードが $? を $false にし $LASTEXITCODE に格納される一方、ErrorRecordを生成せずtry/catchにも入らないことについて。 ↩ ↩2

  6. Microsoft Learn, Differences between Windows PowerShell 5.1 and PowerShell 7.x. PowerShell 7ではネイティブコマンドがstderrに書き込んだだけでは $? が $false にならず、非ゼロ終了コードのときのみ $false になるよう変更されたことについて。 ↩ ↩2

  7. Microsoft Learn, about_Redirection. PowerShellの出力ストリームの番号体系、2>&1 によるエラーストリームの成功ストリームへの合流、ネイティブコマンドのstderr出力の扱いについて。 ↩ ↩2

  8. Microsoft Learn, What’s New in PowerShell 7.4. リダイレクト演算子がネイティブコマンドの出力をバイトストリームとして保持するようになり、PowerShellが内容を解釈したり整形を加えたりしなくなったこと(破壊的変更)、その結果2>&1で合流させたstderrが文字列データとして扱われることについて。 ↩ ↩2

  9. Microsoft Learn, Trace-Command. -Name ParameterBinding によるパラメーターバインドのトレース、-PSHost によるホストへの出力、-Expression によるトレース対象の指定について。 ↩ ↩2

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

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

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

よくある質問

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

PowerShellからexeを呼ぶと、引数のダブルクォートが消えてしまいます。なぜですか?
PowerShellが引数を解析してから外部プログラムに渡す仕組みのためです。Windows PowerShell 5.1では、解析後の引数がスペース区切りの1本の文字列に組み立て直されるため、埋め込んだ引用符が失われたり、空文字列の引数が消えたりします。PowerShell 7.3ではこの挙動が変更され、埋め込まれた引用符と空文字列の引数が保持されるようになりました。注意したいのは、5.1では配列のスプラッティング(& $exe @args)でもこの組み立て直しを通ることです。引用符を含む値や空文字列を確実に渡したい場合、スプラッティングは解決になりません。固定文字列なら停止解析トークン --% が使えますが、変数を含む場合は使えません。確実なのは、Windowsのコマンドライン規則に沿って自分で引用符を付け、ProcessStartInfoのArguments文字列として渡す方法です(ArgumentListは.NET Core 2.1以降のAPIで5.1では使えません)。本文にその実装例を載せています。
--%(停止解析トークン)を使えば、どんな引数でも安全に渡せますか?
いいえ、制約が多いので万能ではありません。--%以降はリテラル扱いになりますが、%USERPROFILE%のような環境変数参照だけは展開されるため、%を含む文字列は意図せず置き換わります(%%によるエスケープも使えません)。またPowerShellの変数は一切展開できず、効果は次の改行かパイプ記号までで、リダイレクトも書けません。変数の値を渡す必要がある時点で--%は使えないので、その場合はProcessStartInfoやStart-Processを検討してください。
バッチファイル(.bat)に外部から受け取った文字列を渡しても大丈夫ですか?
信頼できない入力をバッチファイルに渡すのは避けてください。Windowsではバッチファイルへの引数がcmd.exeへ生のコマンドライン文字列として渡るため、公式ドキュメントも「信頼できない入力は別の方法で渡すこと」と明記しています。ファイル名やユーザー入力をそのまま連結すると、コマンド注入の余地が残ります。渡す値は一時ファイルや環境変数経由にする、あるいはバッチをPowerShellスクリプトに置き換えるのが安全です。
外部コマンドの出力が日本語で文字化けします。どこを直せばよいですか?
PowerShellが外部コマンドの標準出力を復号するときに使う[Console]::OutputEncodingを、そのコマンドが実際に出力しているエンコーディングに合わせます。UTF-8で出力するツールなら[Console]::OutputEncoding = [System.Text.Encoding]::UTF8を設定してから呼び出します。逆にPowerShellから外部コマンドへ文字列をパイプで送るときは$OutputEncodingが使われるので、送信側と受信側の2つを別々に考えるのがコツです。設定はセッション単位なので、スクリプト内で一時的に変更したら元に戻してください。
Start-Processと直接呼び出し(&)はどう使い分けますか?
出力をパイプラインで受け取りたい、終了コードだけ見たいという通常のケースは直接呼び出し(&や単純なコマンド名)が基本です。Start-Processは、別ウィンドウで起動したい、別ユーザーで実行したい、管理者権限に昇格したい(-Verb RunAs)、標準出力をファイルにリダイレクトしたいといった「起動の仕方」を制御したい場合に使います。ただしStart-Processは既定で完了を待たないため、終了コードが必要なら-Waitと-PassThruを併用してExitCodeプロパティを見る必要があります。

著者プロフィール

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

小村 豪

合同会社小村ソフト 代表

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

ブログ一覧に戻る