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

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

更新履歴(初版のみ・2026年07月25日公開)
初版公開
この記事を引用する(DOI: 10.5281/zenodo.21547433)

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

小村 豪(2026)「PowerShellから外部exeを正しく呼ぶ ── 引数のクォート・終了コード・文字化けの落とし穴」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21547433

DOI(最新版)
10.5281/zenodo.21547433
DOI(この版)
10.5281/zenodo.21547434

「手元のコマンドプロンプトでは通るのに、PowerShellスクリプトに移した途端に外部ツールがエラーを返す」── バッチのPowerShell化や、社内EXE・OSS製CLIの自動化でほぼ必ず遭遇する現象です。原因の多くはロジックではなく、引数がプログラムに届くまでの経路にあります。パスに空白が入る、引数にダブルクォートを含める、%( ) を含む文字列を渡す。そのどれかが引き金になって、渡したはずの引数が別の形に化けます。

さらに厄介なのが、この挙動がPowerShell 7.3で変わっていることです。5.1で動くように書いた回避策が7では二重エスケープになり、7で書いたスクリプトが5.1では壊れる。日本語環境では出力の文字化けも重なります。

この記事では、PowerShellから外部プログラム(ネイティブコマンド)を呼ぶときの引数の渡り方を仕様から押さえ、--% の正しい使いどころ、確実に渡したいときのProcessStartInfo、終了コードとstderrの扱い、そして文字化け対策までを実務目線で整理します。エラー処理そのものの設計は「PowerShellのエラー処理と再実行設計」で扱っているので、本記事は「外部プロセスとの境界」に絞ります。

1. まず結論

  • PowerShellは外部プログラムの引数も一度自分で解析します。コマンド呼び出しの後ろは「引数モード(argument mode)」で解析され、空白を含む値は引用符で囲む必要があります。, ( ) { } | & < > @ # などはメタ文字なので、リテラルとして渡すならバッククォートでエスケープします。1
  • PowerShell 7.3で引数の渡し方が変わりました(破壊的変更)。埋め込まれた引用符と空文字列の引数が保持されるようになり、$PSNativeCommandArgumentPassing で挙動を選べます。Windowsの既定値は Windows です。12
  • Windows モードでは、cmd.exe・cscript.exe・wscript.exeと .bat .cmd .js .vbs .wsf だけが従来(Legacy)の渡し方になります。旧来のバッチ資産との互換のための例外です。1
  • 停止解析トークン --% は「以降を丸ごとリテラル扱い」にする最終手段です。ただし %VAR% 形式の環境変数だけは展開され、PowerShell変数は一切使えず、効果は改行かパイプまで。リダイレクトも書けません。1
  • 変数の値を確実に渡したいなら、配列で渡すのが第一候補です。& $exe @argArray のスプラッティングは各要素が独立した引数になります。ProcessStartInfo.ArgumentList なら引用符の組み立てを.NET側に任せられますが、これは.NET Core 2.1以降のAPIで5.1では使えません13
  • バッチファイルへ信頼できない入力を渡してはいけません。Windowsではバッチへの引数が生のコマンドライン文字列としてcmd.exeに渡るため、公式も「信頼できない入力は別の方法で渡せ」と警告しています。1
  • 成否は $LASTEXITCODE で判定します。非ゼロ終了コードは既定ではPowerShellのエラーにならず、try/catchにも入りません。4
  • 文字化けは送信側と受信側で別々に直します。外部コマンドの出力の復号は [Console]::OutputEncoding、PowerShellから外部コマンドへパイプで送る文字列は $OutputEncoding が担当します。2
  • 困ったら Trace-Command -Name ParameterBinding で実際に渡った引数を見ます。PowerShell 7.3以降はネイティブコマンドの引数バインドもトレースできます。15

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

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

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

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

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

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

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

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

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

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

# (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 <>

"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種類が混ざるのが、この分野が難しく見える最大の理由です。13

実務上の判断はシンプルです。5.1と7が混在する環境では、引用符を含む引数をリテラルで書くのをやめる。次章以降の「配列で渡す」「ProcessStartInfoを使う」に寄せれば、バージョン差の影響をほぼ受けません。5.1と7の共存方針そのものは「Windows PowerShell 5.1とPowerShell 7の違い」を参照してください。

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"

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

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

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

5. 変数を確実に渡す ── 配列スプラッティングとProcessStartInfo

変数の値を含む引数を渡すときの第一候補は、配列に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\...' はそのままでは文字列リテラルとして評価されるだけで実行されません)。

さらに確実さが要るとき ── 引数を1つずつ完全に制御したい、出力の文字コードをプロセス単位で指定したい ── は、.NETの ProcessStartInfo を直接使います。ArgumentList に足した値は.NET側が適切に引用符付けするため、PowerShellの解析にもコマンドラインの再解析にも左右されません3

ただし ArgumentList は.NET Core 2.1以降のAPIで、.NET Framework上で動くWindows PowerShell 5.1の ProcessStartInfo には存在しません。3 5.1では次章の Arguments 文字列を自分で組み立てるか、前述の配列スプラッティングを使ってください。

# 【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"
}

出力のリダイレクトで最も多い事故がデッドロックです。パイプのバッファには上限があり、埋まると子プロセスは書き込みでブロックします。したがって、WaitForExit() を先に呼んで後から読むのはもちろん、片方のストリームを ReadToEnd() で同期的に読み切ってからもう片方を読む書き方も危険です(読んでいない側のバッファが先に埋まると、子プロセスが止まり、ReadToEnd() は永久に返りません)。上のように両方を非同期で読み始めてから待つか、片方だけをリダイレクトする設計にしてください。子プロセスの扱い全般については「Windowsアプリで子プロセスを安全に扱うチェックリスト」も参照してください。

Windows PowerShell 5.1で ProcessStartInfo を使う場合は、Arguments自分で引用符を付けた1本の文字列を渡すことになります。この組み立てを手で書くのは事故のもとなので、5.1では配列スプラッティング(& $exe @args)を第一候補にしてください。

# 【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 ' '

この規則の細かさこそが、5.1で手組みを避けるべき理由です。とはいえ、Windows PowerShell 5.1では配列スプラッティングも万能ではありません。渡された値は結局、従来方式でコマンドライン文字列に組み立て直されるため、空文字列の引数は消え、引用符を含む値は変形します。1 したがって5.1では次のように使い分けてください。

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

PowerShell 7ではどちらの問題も解消しているため、この使い分けが不要になります。

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

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

日本語環境で必ず当たるのが文字化けです。ポイントは、方向によって使われる設定が違うことです。

方向 使われる設定 症状
外部コマンドの出力をPowerShellが受け取る [Console]::OutputEncoding UTF-8出力のツールの結果が「譁�喧縺�」のように化ける
PowerShellから外部コマンドへパイプで文字列を送る $OutputEncoding 送った日本語が相手側で化ける

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

# 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の文字コードと改行コード」にまとめてあります。

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

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

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

(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

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

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

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]

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

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

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

10. まとめ

  • PowerShellは外部プログラムの引数も引数モードで解析します。記号には構文上の意味があるため、cmd.exeで動く文字列がそのまま通るとは限りません。
  • PowerShell 7.3で引数の渡し方が変更され(破壊的変更)、埋め込み引用符と空文字列が保持されるようになりました。Windowsの既定は Windows モードで、cmd.exeやバッチ等だけがLegacy方式になります。
  • --% は固定文字列専用の最終手段です。%VAR% が必ず展開され、PowerShell変数が使えず、効果は改行かパイプまでという制約を理解して使ってください。
  • 変数を渡すなら配列スプラッティングが第一候補です。PowerShell 7ならProcessStartInfoの ArgumentList も使えます(5.1には存在しません)。バックスラッシュはPowerShellのエスケープ文字ではない、という点が混乱の元です。
  • 標準出力と標準エラーの両方をリダイレクトするときは、必ず両方を同時に読み出してください。片方を同期的に読み切る書き方はデッドロックします。
  • 文字化けは方向で対処が違います。受け取りは [Console]::OutputEncoding、送信は $OutputEncoding、プロセス単位なら StandardOutputEncoding
  • 成否は $LASTEXITCODE。stderrへの出力は失敗ではありません。stdoutとstderrを区別したいなら、2>&1 で合流させず別々に受け取ってください(合流後の型は5.1と7.4以降で異なります)。

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

この記事で扱ったコードは、そのまま動かせる形にまとめて配布しています。引数の引用符付けモジュールと、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 26

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

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

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

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

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

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

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

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

よくある質問

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

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

ブログ一覧に戻る