Programming Field

バッチファイルにPowerShellスクリプトを埋め込む

Windowsのバッチファイルはシステムレベルでサポートされているテキストベースの実行可能ファイルです。コピーなどのファイル操作や特定の環境変数の設定を用いたプログラムの実行に使うことができます。一方PowerShellはWindows 7以降では標準搭載されているシェルであり、.NETのライブラリを扱うこともできる点が特徴です。

ここでは、バッチファイルとPowerShellスクリプトファイルを1つのファイルにまとめて記述してしまうという方法を記載しています。

※ この項に記述されている内容を使うと比較的容易にPowerShellコードを実行することができるようになるため、セキュリティー上問題がある可能性があります。そのため、作成したデータの取り扱いにはご注意ください。

※ 続編としてPowerShellに引数を解釈させる方法を「バッチファイルにPowerShellスクリプトを埋め込む その2」にて紹介しています。本ページの内容をベースに記述していますが、一番シンプルな方法を使う場合は、そちらのページの末尾をご覧ください。

ファイルをスクリプトとしてPowerShellで実行させる方法

ファイルをスクリプトとして実行するには以下の3通りが考えられます。

  1. 「-File <filePath> <args>」オプションの利用
  2. リダイレクトと「-Command -」オプションの利用
  3. PowerShellの「Get-Content」「Invoke-Expression」コマンドの利用

「-File <filePath> <args>」オプションの利用

PowerShellプログラムには「-File <filePath> <args>」オプションがあり、「<filePath>」にスクリプトファイル、「<args>」に引数を与えることで、ファイルに記述されたスクリプトを実行することができます。ただしスクリプトファイルは「.ps1」拡張子で終わるファイルである必要があります。なお、システムで無効になっている場合はこのコマンドによるスクリプト実行ができないという制約もあります。

スクリプトを記述したファイルを実行するには「.ps1」拡張子を持つファイルである必要がありますが、今回はバッチファイルとして実行できる必要があるため、この方法は利用できません。

リダイレクトと「-Command -」オプションの利用

一方、PowerShellには「-Command」オプションも存在します。これは引数に直接スクリプトを指定する方法ですが、「-Command -」と指定すると標準入力からスクリプトを読み取って実行するようになります。これを利用することで、拡張子が「.ps1」ではないファイルであっても入力リダイレクトでデータを送り込むことができます。

ただし、この方法を用いた場合はPowerShellスクリプト内でコンソールのキーボード入力を受け取ることができなくなり、Ctrl+Cによる中断もできなくなります。スクリプト内でユーザー入力を受け付ける必要が無い場合は問題ありませんが、ユーザー入力が必要である場合は使用できません。

PowerShellの「Get-Content」「Invoke-Expression」コマンドの利用

もう一つの方法は、PowerShell内で利用できる「Get-Content」「Invoke-Expression」の各コマンドを用いるという内容です。「Get-Content」はファイルの中身を読み取ってObject[]型(各要素は各行に対応するString型)として返し、「Invoke-Expression」は引数に文字列型を渡してその内容をコマンドとして実行できるというものです。

ファイルの内容をInvoke-Expressionに渡すPowerShellのスクリプトは以下の通りです。

Invoke-Expression -Command ((Get-Content "any-file.ext") -join "`n")

※ Invoke-Expression の-CommandオプションはString型を受け付けること、Get-Content が返すObject[]の各要素は改行文字が取り除かれていることから、-join 演算子を用いて改行文字を加えた上で文字列に変換して Invoke-Expression に渡しています。
※ なお、PowerShellのバージョン3.0以降が利用可能であれば、「Get-Content」の「-Raw」オプションを利用することで「(Get-Content "any-file.ext") -join "`n"」を「Get-Content "any-file.ext" -Raw」と記述することができます。[2016/10/30 追記]

これを PowerShell.exe のコマンドラインにすると以下のようになります。

PowerShell.exe -Command "iex -Command ((gc 'any-file.ext') -join \"`n\")"

※ 「"」内で「"」文字を使う際は「\」を付けます。PowerShell.exe はこれを正しく解釈してコマンド内に「"」が入るようにしています。
※ 「iex」は「Invoke-Expression」、「gc」は「Get-Content」の Alias として定義されています。

この方法ではリダイレクトを用いないため、標準入力からユーザー入力を取得することもできます。

注意点としては、(2番目の方法も同様ですが)スクリプトは引数が無い状態で実行されるため、PowerShell内の「args」変数は空になります。何かしらの引数を処理したい場合は外側のバッチファイルで解析する必要があります。

また、記述しているPowerShellスクリプトが巨大なものであった場合は結合処理に時間がかかる可能性がありますが、スクリプトはそこまで巨大になるものではないため特に問題にはならないと考えられます。

バッチファイルとしてもPowerShellスクリプトとしても動作するコマンド

バッチファイルでは画面上にコマンドが表示されないようするため、「@echo off」などを先頭に書くのが通例となっていますが、このコマンド列をそのままPowerShellスクリプトとして実行するとエラーとなります。

PowerShellでは「@( )」が特殊演算子の1つとして定義されており、( )内に記述されたコマンド/ステートメントの結果をまとめて配列として返す動作をします。例えば

@(echo off
echo test)

というコマンドは、「@( )」全体が「off」文字列と「test」文字列を要素に持つObject[]型データとなります。

さらに、PowerShellでは文字列リテラル(" "や' 'で囲まれた文字列)内での改行が可能となっています。そのため、

@(echo 'hello
rem world')

というコマンドはPowerShellでは「hello(改行)rem world」文字列を要素に持つObject[]型データとなります。

一方、バッチファイルでも「( )」は有効な文字列ですが、' 'は引用符としては用いられないため、上記のコードは「echo 'hello」と「rem world'」という2つのコマンドをまとめたものと解釈されることとなり、結果「'hello」という文字列が画面に出力されます。そこで、

@(echo '> NUL
echo off)
echo Batch file
exit /b 0
')

と記述すると、バッチファイルとしては「Batch file」を出力するファイル、PowerShellとしては「> NUL(改行)echo off)(改行)echo Batch file(改行)exit /b 0(改行)」という文字列を持つObject[]型データを出力するコマンドになります。

※ バッチファイルでは「exit」が実行されるとそれ以降の記述がすべて無視されます。

これによりバッチファイルでもPowerShellでも動作するスクリプトが記述できますが、このままでは「@( )」のデータが出力されてしまうため、パイプラインを利用して適当な(影響の少ない)コマンドにデータを渡す必要があります。例として、以下のように「Set-Variable」(Alias: sv)を用いて何も出力されないようにします

@(echo '> NUL
echo off)
echo Batch file
exit /b 0
') | sv -Name TempVar

これをPowerShellで実行すると、変数「TempVar」が前述の「文字列を持つObject[]型データ」単一の文字列データを持つ値として作成されますが、データの出力(≒画面への出力)がなくなります。

※ 単一の要素を持つ配列をパイプライン経由で Set-Variable に(-Value引数として)渡すと、変数の値は配列自体ではなくその要素がセットされます。[2015/02/17 修正]

そして、Set-Variable を実行した次の行からは任意のPowerShellコマンドを記述すること出来るため、バッチファイル内にPowerShellスクリプトを埋め込むことができるようになります。

上記の内容を踏まえて、『PowerShellの「Get-Content」「Invoke-Expression」コマンドの利用』の方法と『バッチファイルとしてもPowerShellスクリプトとしても動作するコマンド』を組み合わた例を紹介します。

以下のソースコードはバッチファイルとしてもPowerShellスクリプトとしても解釈することができ、以下の内容をバッチファイルとして保存し、それをそのままバッチファイルとして実行すると自身をPowerShellスクリプトとして実行し、PowerShellのコマンドが実行されます。

@(echo '> NUL
echo off)
setlocal enableextensions
set "THIS_PATH=%~f0"
set "PARAM_1=%~1"
PowerShell.exe -Command "iex -Command ((gc \"%THIS_PATH:`=``%\") -join \"`n\")"
exit /b %errorlevel%
-- この1つ上の行までバッチファイル
') | sv -Name TempVar

# ここからPowerShellスクリプト
$currentTime = [System.DateTime]::Now
echo "This batch file is executed as PowerShell script (file = $env:THIS_PATH, param = $env:PARAM_1)"
echo "Current time is $currentTime"

例えば、このバッチファイルを「Y:\any\dir\test.bat」として保存し、「hoge」という引数を付けて実行すると、

This batch file is executed as PowerShell script (file = Y:\any\dir\test.bat, param = hoge)
Current time is 02/01/2015 12:34:56

などといった出力が行われ、バッチファイルながらPowerShellのスクリプトが実行されることが分かります。

※ 「PowerShell.exe」で始まる行では、1行目から「' '」による括りが続いているため、gc の引数を「' '」ではなく「" "」で囲みます。また、バッチファイルのファイル名に「`」記号が含まれているとエスケープが働いてしまうため、バッチファイルレベルでの環境変数展開時に「`」を「``」に置き換えています(置き換えについては「%」をご覧ください)。[2015/02/17 追記]
※ 環境変数の展開方法に拡張構文を用いているためSetlocalに「enableextensions」を付けるのがより適切です。(多くの環境では既定で拡張構文が有効なため省略しても問題ないですが、既定で拡張構文を無効、または呼び出し前に無効化もできるので汎用的に使用されることを想定する場合は注意が必要です。) [2016/08/20 追記]
※ バッチファイル内で設定した環境変数はPowerShell内からも使用できます。そのため、バッチファイルに指定した引数を環境変数に設定しておけば、PowerShellコード内では環境プロバイダー経由(変数として用いる場合は「$env:」と環境変数名)でアクセスすることができます。
※ PowerShellスクリプトには(すべてを調査していませんが)特に制限はなく、「function」ブロックを記述することも可能です。

なお、PowerShellに引数を解釈させる方法については「バッチファイルにPowerShellスクリプトを埋め込む その2」をご覧ください。