PowerShellをバッチファイルに埋め込む


PowerShellのファイル(.PS1)はダブルクリックしても実行されないし、スクリプトを配布して動かしてもらおうとしても手順を伝えるのも面倒です。(手順通りに実行してくれるとも限りませんし。)
そこで、PowerShellをバッチファイル化してダブルクリックで直接起動できるようにしてラクをしちゃいましょう。

目的

PowerShell(.PS1)をバッチファイル(.BATまたは.CMD)化して、ダブルクリックで直接起動できるようにします。

ただし、バッチファイルですので、ファイルサーバ等の共有フォルダに置いて直接起動することはできません。必ずドライブレターのある場所に配置して実行しましょう。
※共有フォルダをネットワークドライブとしてドライブレターを付けてマウントすれば実行可能です。

使い方

PowerShell(.PS1)をバッチファイル(.BATまたは.CMD)化するための手順は以下の通りです。

1.バッチファイルの1行目に以下の記入します。(かなり長いです)

sample.bat
@SETLOCAL&SETLOCAL ENABLEDELAYEDEXPANSION&(chcp 65001>NUL)&PUSHD "%~dp0"&(SET SCRIPT_PATH=%~f0)&(SET SCRIPT_DIR=%~dp0)&(SET SCRIPT_NAME=%~nx0)&(SET SCRIPT_BASE_NAME=%~n0)&(SET SCRIPT_EXTENSION=%~x0)&(SET SCRIPT_ARGUMENTS=%*)&(POWERSHELL -NoLogo -Sta -NoProfile -ExecutionPolicy Unrestricted "&([scriptblock]::create('$OutputEncoding=[Console]::OutputEncoding;'+\"`n\"+((gc -Encoding UTF8 -Path \"!SCRIPT_PATH!\"|?{$_.readcount -gt 1})-join\"`n\")))" !SCRIPT_ARGUMENTS!)&(SET EXIT_CODE=!ERRORLEVEL!)&POPD&PAUSE&EXIT !EXIT_CODE!&ENDLOCAL&GOTO :EOF

2.バッチファイルの2行目以降はPowerShellになります。好きなように書いてください。

なお、バッチファイルとして保存する際は、文字コードをUTF8に指定して保存してください。
もし、Shift-JISで保存したい場合は、1行目の「gc -Encoding UTF8 -Path \"!SCRIPT_PATH!\"」の部分を「gc -Encoding Default -Path \"!SCRIPT_PATH!\"」に修正してください。

実行について

PowerShellを起動するにあたり、1行目の部分で以下の環境変数を設定しています。

環境変数名 設定内容
SCRIPT_PATH バッチファイルを指すフルパス
SCRIPT_DIR バッチファイルが格納されているディレクトリを指すフルパス
SCRIPT_NAME バッチファイル名
SCRIPT_BASE_NAME バッチファイル名(.と拡張子を除いた部分)
SCRIPT_EXTENSION バッチファイル名(.を含む拡張子部分)
SCRIPT_ARGUMENTS 引数部分

また、PUSHDでバッチファイルが格納されているディレクトリにカレントディレクトリを移動しています。
この処理が不要でしたら、1行目の中から「&PUSHD "%~dp0"」と「&POPD」を削除してください。

PowerShellを実行する部分で、実行ポリシーを「Unrestricted」に設定していますので、実行環境のセキュリティ要件に合わせて変更してください。

戻り値について

PowerShellの部分でexitを実行すると、指定した値がバッチファイルの戻り値として返されますので、他のバッチファイルやプログラムから起動して戻り値を得たい場合に重宝します。

なお、終了する際にEXITを実行していますので、他のバッチファイルからCALL文で呼び出す際は注意が必要です。
CALL文で呼び出す場合は、1行目の中にある「&EXIT !EXIT_CODE!」の部分を「&EXIT /B !EXIT_CODE!」に修正してください。

引数について

この方法で作成されたバッチファイルに引数を渡すとき、空白を含めるためにダブルクォーテーションで囲むような場合は、そのままではPowerShellによって文字列中の空白部分で区切られてしまうのでうまくいきません。

以下のように$argsの内容を表示するだけのバッチファイルを作成して試してみます。

sample.bat
@SETLOCAL&SETLOCAL ENABLEDELAYEDEXPANSION&(chcp 65001>NUL)&PUSHD "%~dp0"&(SET SCRIPT_PATH=%~f0)&(SET SCRIPT_DIR=%~dp0)&(SET SCRIPT_NAME=%~nx0)&(SET SCRIPT_BASE_NAME=%~n0)&(SET SCRIPT_EXTENSION=%~x0)&(SET SCRIPT_ARGUMENTS=%*)&(POWERSHELL -NoLogo -Sta -NoProfile -ExecutionPolicy Unrestricted "&([scriptblock]::create('$OutputEncoding=[Console]::OutputEncoding;'+\"`n\"+((gc -Encoding UTF8 -Path \"!SCRIPT_PATH!\"|?{$_.readcount -gt 1})-join\"`n\")))" !SCRIPT_ARGUMENTS!)&(SET EXIT_CODE=!ERRORLEVEL!)&POPD&PAUSE&EXIT !EXIT_CODE!&ENDLOCAL&GOTO :EOF
$args

実行例:
start /wait /b sample.bat "123 456 789"
123
456
789

引数を修正し、シングルクォーテーションで囲んだ後にダブルクォーテーションで囲む要にすることで、これを回避することができます。

実行例:
start /wait /b sample.bat "'123 456 789'"
123 456 789

強制終了について

バッチファイルを介してPowerShellを起動していますので、PowerShellの親プロセスはバッチファイル(CMD.EXE)になります。
タスクマネージャからPowerShell.EXEを直接終了させるならばバッチファイルの部分も終了しますので問題はありませんが、バッチファイル部分だけを終了させてもPowerShellの部分は終了しませんので注意が必要です。
※JP1のようなジョブ管理システムで使用する場合は、強制終了を伴う可能性があるので十分に検討して備えてください。そのままだとバッチファイル部分だけ強制終了して終わりということになってしまいます。

その他

PowerShellの実行が終わるとすぐにバッチファイルも終わってしまいますので、このままだとデバッグをする際には扱いにくいです。
1行目の「&POPD」の前後あたりに「&PAUSE」を追加するとそこで止まってくれますので、用途に応じて修正してください。

※なお、PowerShellではPAUSEに相当するスクリプトレットがありませんので、以下で代用してください。

function Pause ([int]$Timeout = -1, [string]$Message = '続行するには何かキーを押してください . . .')
{
    Write-Host -NoNewLine $Message;
    timeout /T ([System.Math]::Max([System.Math]::Min($Timeout,99999),-1)) >$null 2>&1;
    Write-Host '';
}

また、スクリプトブロックでの実行になるため、以下の自動変数はNULLになります。これらは前述の環境変数で代用してください。
$PSScriptRoot
$PSCommandPath
$MyInvocation.MyCommand.Path