web-dev-qa-db-ja.com

Windowsバッチファイル内でPowerShellスクリプトを実行する方法

Windowsバッチスクリプトと同じファイルにPowerShellスクリプトを埋め込むにはどうすればよいですか?

私はこの種のことが他のシナリオで可能であることを知っています:

  • sqlcmdと、ファイルの先頭にあるgotoとコメントの巧妙な配置を使用してSQLをバッチスクリプトに埋め込む
  • スクリプトを実行したいプログラムの名前がスクリプトの最初の行にコメントアウトされている* nix環境では、たとえば#!/usr/local/bin/pythonのようになります。

これを行う方法がない場合があります。その場合、起動スクリプトとは別のPowerShellスクリプトを呼び出す必要があります。

私が検討した解決策の1つは、PowerShellスクリプトをエコーアウトして実行することです。これをしないする正当な理由は、これを試みる理由の一部が、PowerShell環境の利点を苦痛なしに使用することであるということです。例、 エスケープ文字

いくつかの異常な制約があり、エレガントな解決策を見つけたいと思います。この質問は、さまざまな回答を餌にしているのではないかと思います。「代わりに、この別の問題を解決してみませんか」。これらは私の制約であると言えば十分です、それについては申し訳ありません。

何か案は?これを達成するための巧妙なコメントとエスケープ文字の適切な組み合わせはありますか?

これを達成する方法に関するいくつかの考え:

  • 行末のカラット^は続きです-VisualBasicのアンダースコアのように
  • アンパサンド&は通常、コマンドを分離するために使用されますecho Hello & echo Worldは、別々の行に2つのエコーを生成します
  • %0は、現在実行中のスクリプトを提供します

したがって、このようなもの(私がそれを機能させることができれば)は良いでしょう:

# & call powershell -psconsolefile %0
# & goto :EOF
/* From here on in we're running Nice juicy powershell code */
Write-Output "Hello World"

を除いて...

  • それは機能しません...なぜなら
  • ファイルの拡張子がPowerShellの好みに合っていません:Windows PowerShell console file "insideout.bat" extension is not psc1. Windows PowerShell console file extension must be psc1.
  • CMDも状況に完全に満足しているわけではありませんが、'#', it is not recognized as an internal or external command, operable program or batch file.でつまずきます。
21
Don Vince

これは、PowerShellに正しい行を渡すだけです。

dosps2.cmd

@findstr/v "^@f.*&" "%~f0"|powershell -&goto:eof
Write-Output "Hello World" 
Write-Output "Hello some@com & again" 

正規表現 は、@fで始まり&を含む行を除外し、それ以外はすべてPowerShellに渡します。

C:\tmp>dosps2
Hello World
Hello some@com & again
19

「ポリグロットスクリプト」と呼ばれることもあるものを探しているようです。 CMD-> PowerShellの場合、

@@:: This prolog allows a PowerShell script to be embedded in a .CMD file.
@@:: Any non-PowerShell content must be preceeded by "@@"
@@setlocal
@@set POWERSHELL_BAT_ARGS=%*
@@if defined POWERSHELL_BAT_ARGS set POWERSHELL_BAT_ARGS=%POWERSHELL_BAT_ARGS:"=\"%
@@PowerShell -Command Invoke-Expression $('$args=@(^&{$args} %POWERSHELL_BAT_ARGS%);'+[String]::Join([char]10,$((Get-Content '%~f0') -notmatch '^^@@'))) & goto :EOF

引用符で囲まれた引数をサポートする必要がない場合は、それをワンライナーにすることもできます。

@PowerShell -Command Invoke-Expression $('$args=@(^&{$args} %*);'+[String]::Join([char]10,(Get-Content '%~f0') -notmatch '^^@PowerShell.*EOF$')) & goto :EOF

http://blogs.msdn.com/jaybaz_ms/archive/2007/04/26/powershell-polyglot.aspx から取得。それがPowerShellv1でした。 v2の方が簡単かもしれませんが、私は調べていません。

12
Jay Bazuzi

ここ トピックが議論されました。主な目標は、一時ファイルの使用を回避して、低速のI/O操作を減らし、冗長な出力なしでスクリプトを実行することでした。

そして、これが私によると最良の解決策です:

<# :
@echo off
setlocal
set "POWERSHELL_BAT_ARGS=%*"
if defined POWERSHELL_BAT_ARGS set "POWERSHELL_BAT_ARGS=%POWERSHELL_BAT_ARGS:"=\"%"
endlocal & powershell -NoLogo -NoProfile -Command "$input | &{ [ScriptBlock]::Create( ( Get-Content \"%~f0\" ) -join [char]10 ).Invoke( @( &{ $args } %POWERSHELL_BAT_ARGS% ) ) }"
goto :EOF
#>

param(
    [string]$str
);

$VAR = "Hello, world!";

function F1() {
    $str;
    $script:VAR;
}

F1;

さらに良い方法( ここ を参照):

<# : batch portion (begins PowerShell multi-line comment block)


@echo off & setlocal
set "POWERSHELL_BAT_ARGS=%*"

echo ---- FROM BATCH
powershell -noprofile -NoLogo "iex (${%~f0} | out-string)"
exit /b %errorlevel%

: end batch / begin PowerShell chimera #>

$VAR = "---- FROM POWERSHELL";
$VAR;
$POWERSHELL_BAT_ARGS=$env:POWERSHELL_BAT_ARGS
$POWERSHELL_BAT_ARGS

ここで、POWERSHELL_BAT_ARGSは、バッチ部分で最初に変数として設定されるコマンドライン引数です。

トリックはバッチリダイレクトの優先度にあります-リダイレクトは他のコマンドよりも優先度が高いため、この行<# ::<#のように解析されます。

ただし、バッチファイルの:で始まる行は、ラベルとして使用されます。つまり、実行されません。それでも、これは有効なPowerShellコメントのままです。

残っているのは、PowerShellがcmd.exeによって実行されるスクリプトへのフルパスである%~f0を読み取って実行するための適切な方法を見つけることだけです。

6
npocmaka

最初にPowerShellで1つのエラーを気にしないのであれば、これはうまくいくようです。

dosps.cmd

@powershell -<%~f0&goto:eof
Write-Output "Hello World" 
Write-Output "Hello World again" 
4

this "polyglot"ラッパースクリプト も検討してください。これは組み込みPowerShellおよび/またはVBScript/JScriptcodをサポートします e; 2013年に作者自身 flabdablet が投稿した この独創的なオリジナル から改作されましたが、リンクのみの回答であったために衰退しました。 2015年。

カイルの優れた答え を改善するソリューション:

<# ::
@setlocal & copy "%~f0" "%TEMP%\%~0n.ps1" >NUL && powershell -NoProfile -ExecutionPolicy Bypass -File "%TEMP%\%~0n.ps1" %*
@set "ec=%ERRORLEVEL%" & del "%TEMP%\%~0n.ps1"
@exit /b %ec%
#>

# Paste arbitrary PowerShell code here.
# In this example, all arguments are echoed.
'Args:'
$Args | % { 'arg #{0}: [{1}]' -f ++$i, $_ }

注:後でクリーンアップされる一時的な*.ps1ファイルは、%TEMP%フォルダーに作成されます。そうすることで、%*を使用するだけで、引数を(合理的に)堅牢に渡すことが大幅に簡素化されます。

  • <# ::は、PowerShellがコメントブロックの開始と見なすハイブリッド行ですが、cmd.exeは、 npocmakaの回答 から借用した手法を無視します。

  • したがって、@で始まるバッチファイルコマンドは、PowerShellでは無視されますが、cmd.exeによって実行されます。最後の@-プレフィックス行はexit /bで終わるため、バッチファイルはすぐに終了します。cmd.exeはファイルの残りの部分を無視しますしたがって、バッチファイル以外のコード、つまりPowerShellコードを自由に含めることができます。

  • #>行は、バッチファイルコードを囲むPowerShellコメントブロックを終了します。

  • したがって、ファイル全体が有効なPowerShellファイルであるため、PowerShellコードを抽出するためにfindstrのトリックは必要ありません。ただし、PowerShellはファイル名拡張子が.ps1のスクリプトのみを実行するため、バッチファイルの(一時的な)コピーを作成する必要があります。 %TEMP%\%~0n.ps1は、バッチファイル(%TEMP%)にちなんで名付けられた%~0nフォルダーに一時コピーを作成しますが、代わりに拡張子.ps1を付けます。一時ファイルは、完了すると自動的に削除されます。

  • PowerShellコマンドの終了コードを渡すには、cmd.exeステートメントの3つの別々のが必要であることに注意してください。
    setlocal enabledelayedexpansionを使用すると、仮想的に単一行として実行できますが、引数で!文字が不要に解釈される可能性があります。 。)


引数の受け渡しの堅牢性を示すには:

上記のコードがsample.cmdとして保存されていると仮定して、次のように呼び出します。

sample.cmd "val. w/ spaces & special chars. (\|<>'), on %OS%" 666 "Lisa \"Left Eye\" Lopez"

次のようなものが生成されます。

Args:
arg #1: [val. w/ spaces & special chars. (\|<>'), on Windows_NT]
arg #2: [666]
arg #3: [Lisa "Left Eye" Lopez]

embedded "chars。 \"として渡されました。
ただし、埋め込まれた"文字に関連するエッジケースがあります。

:: # BREAKS, due to the `&` inside \"...\"
sample.cmd "A \"rock & roll\" life style"

:: # Doesn't break, but DOESN'T PRESERVE ARGUMENT BOUNDARIES.
sample.cmd "A \""rock & roll\"" life style"

これらの問題はcmd.exeの欠陥のある引数の解析に起因します、そして最終的にはこれらの欠陥を非表示しようとするのは無意味です flabdabletは彼の優れた答えで指摘しています

彼が説明しているように、次のcmd.exeメタ文字を^^^シーケンス内の\"...\"(sic)でエスケープすると、問題が解決します:

& | < >

上記の例を使用すると:

:: # OK: cmd.exe metachars. inside \"...\" are ^^^-escaped.
sample.cmd "A \"rock ^^^& roll\" life style"
3
mklement0

これは、Carlosによって投稿されたソリューションとは異なり、引数をサポートし、複数行のコマンドや、Jayによって投稿されたソリューションのようなparamの使用を中断しません。唯一の欠点は、このソリューションが一時ファイルを作成することです。私のユースケースでは、それは許容範囲です。

@@echo off
@@findstr/v "^@@.*" "%~f0" > "%~f0.ps1" & powershell -ExecutionPolicy ByPass "%~f0.ps1" %* & del "%~f0.ps1" & goto:eof
2
Kyle

このタスクに対する私の現在の好みは、 mklement0の最初のソリューション :とほとんど同じように機能するポリグロットヘッダーです。

<#  :cmd header for PowerShell script
@   set dir=%~dp0
@   set ps1="%TMP%\%~n0-%RANDOM%-%RANDOM%-%RANDOM%-%RANDOM%.ps1"
@   copy /b /y "%~f0" %ps1% >nul
@   powershell -NoProfile -ExecutionPolicy Bypass -File %ps1% %*
@   del /f %ps1%
@   goto :eof
#>

# Paste arbitrary PowerShell code here.
# In this example, all arguments are echoed.
$Args | % { 'arg #{0}: [{1}]' -f ++$i, $_ }

いくつかの理由から、cmdヘッダーを複数行に配置し、それぞれに1つのコマンドを配置することを好みます。まず、何が起こっているのかを確認する方が簡単だと思います。コマンドラインは編集ウィンドウの右側からはみ出さないように十分に短く、左側の句読点の列は、ひどく乱用されたラベルが付いているヘッダーブロックとして視覚的にマークします。最初の行はそうだと言っています。次に、delコマンドとgotoコマンドはそれぞれの行にあるため、スクリプト引数として本当にファンキーなものが渡されても実行されます。

PowerShellの不可解なエラーメッセージに少なくとも意味のある行番号が含まれるため、.ps1に依存するソリューションよりも一時的なInvoke-Expressionファイルを作成するソリューションを好むようになりました。

一時ファイルの作成にかかる時間は、通常、PowerShell自体が動作するまでに完全に費やされます。また、一時ファイルの名前に埋め込まれた128ビット相当の%RANDOM%は、複数の同時スクリプトが実行されないことをほぼ保証します。お互いの一時ファイルを踏みにじることはありません。一時ファイルアプローチの唯一の本当の欠点は、元のcmdスクリプトが呼び出されたディレクトリに関する情報が失われる可能性があることです。これは、2行目に作成されたdir環境変数の理由です。

明らかに、PowerShellがスクリプトファイルで受け入れるファイル名拡張子についてそれほど肛門的でないことははるかに迷惑ではありませんが、あなたが望んでいたシェルではなく、あなたが持っているシェルと戦争をします。

それについて言えば:mklement0が観察するように、

# BREAKS, due to the `&` inside \"...\"
sample.cmd "A \"rock & roll\" life style"

cmd.exeの完全に価値のない引数解析のため、これは確かに壊れます。私は一般的に、cmdの多くの制限をhideしようとする作業が少なければ少ないほど、私が自分自身に引き起こす予期しないバグが少なくなることを発見しました(私はたとえば、mklement0の非の打ちどころのないアンパサンドエスケープロジックを壊す括弧を含む引数を思い付くことができると確信しています。私の見解では、弾丸を噛んで次のようなものを使用するだけで、それほど痛みはありません

sample.cmd "A \"rock ^^^& roll\" life style"

最初と3番目の^エスケープは、そのコマンドラインが最初に解析されるときに食べられます。 2つ目は、&に渡されたコマンドラインに埋め込まれたpowershell.exeをエスケープするために存続します。はい、これは醜いです。はい、それはcmd.exeがスクリプトで最初にクラックされるものではないふりをするのを難しくします。心配しないでください。重要な場合は文書化してください。

ほとんどの実際のアプリケーションでは、&の問題はとにかく議論の余地があります。このようなスクリプトへの引数として渡されるもののほとんどは、ドラッグアンドドロップで到着するパス名になります。 Windowsはそれらを引用します。これはスペースとアンパサンドを保護するのに十分であり、実際には引用符以外のものであり、Windowsのパス名では許可されていません。

Vinyl LP's, 12"をCSVファイルで表示することすら始めないでください。

1
flabdablet

あなたの質問を完全に理解しないと、私の提案は次のようになります。

@echo off
set MYSCRIPT="some cool powershell code"
powershell -c %MYSCRIPT%

またはそれ以上

@echo off
set MYSCRIPTPATH=c:\work\bin\powershellscript.ps1
powershell %MYSCRIPTPATH%
1
John Weldon

別のサンプルバッチ+ PowerShellスクリプト...他の提案されたソリューションよりも単純であり、それらのいずれにも一致しない特性があります。

  • 一時ファイルを作成しない=>パフォーマンスが向上し、何かを上書きするリスクがありません。
  • バッチコードの特別な接頭辞はありません。これは通常のバッチです。 PowerShellコードについても同じことが言えます。
  • !のようなトリッキーな文字を含む引用符で囲まれた文字列も含め、すべてのバッチ引数をPowerShellに正しく渡します。 %<> '$
  • 二重引用符は、それらを2倍にすることで渡すことができます。
  • PowerShellでは標準入力を使用できます。 (バッチ自体をPowerShellにパイプするすべてのバージョンとは異なります。)

このサンプルは言語遷移を表示し、PowerShell側はバッチ側から受け取った引数のリストを表示します。

<# :# PowerShell comment protecting the Batch section
@echo off
:# Disabling argument expansion avoids issues with ! in arguments.
setlocal EnableExtensions DisableDelayedExpansion

:# Prepare the batch arguments, so that PowerShell parses them correctly
set ARGS=%*
if defined ARGS set ARGS=%ARGS:"=\"%
if defined ARGS set ARGS=%ARGS:'=''%

:# The ^ before the first " ensures that the Batch parser does not enter quoted mode
:# there, but that it enters and exits quoted mode for every subsequent pair of ".
:# This in turn protects the possible special chars & | < > within quoted arguments.
:# Then the \ before each pair of " ensures that PowerShell's C command line parser 
:# considers these pairs as part of the first and only argument following -c.
:# Cherry on the cake, it's possible to pass a " to PS by entering two "" in the bat args.
echo In Batch
PowerShell -c ^"Invoke-Expression ('^& {' + [io.file]::ReadAllText(\"%~f0\") + '} %ARGS%')"
echo Back in Batch. PowerShell exit code = %ERRORLEVEL%
exit /b

###############################################################################
End of the PS comment around the Batch section; Begin the PowerShell section #>

echo "In PowerShell"
$Args | % { "PowerShell Args[{0}] = '$_'" -f $i++ }
exit 0

バッチコメントには、他のほとんどの人が使用する::ではなく:#を使用していることに注意してください。これにより、実際にはPowerShellコメントのように見えます。 (または、実際には他のほとんどのスクリプト言語のコメントと同様です。)