簡體   English   中英

如何在 Windows 批處理文件中運行 PowerShell 腳本

[英]How to run a PowerShell script within a Windows batch file

如何將 PowerShell 腳本嵌入到與 Windows 批處理腳本相同的文件中?

我知道這種事情在其他情況下是可能的:

  • 使用sqlcmd將 SQL 嵌入到批處理腳本中,並巧妙地安排文件開頭的 goto 和注釋
  • 在 *nix 環境中,您希望運行腳本的程序名稱在腳本的第一行被注釋掉,例如#!/usr/local/bin/python

可能沒有辦法做到這一點——在這種情況下,我將不得不從啟動腳本中調用單獨的 PowerShell 腳本。

我考慮過的一種可能的解決方案是回顯 PowerShell 腳本,然后運行它。 這樣做的一個很好的理由是,嘗試這樣做的部分原因是為了利用 PowerShell 環境的優勢,而沒有諸如轉義字符之類的痛苦

我有一些不尋常的約束,想找到一個優雅的解決方案。 我懷疑這個問題可能會引來各種各樣的回答:“你為什么不嘗試解決這個不同的問題呢。” 我只想說這些是我的限制,對此感到抱歉。

有任何想法嗎? 是否有巧妙的注釋和轉義字符的合適組合可以使我實現這一目標?

關於如何實現這一目標的一些想法:

  • 一行末尾的克拉^是延續 - 就像 Visual Basic 中的下划線
  • 符號&通常用於分隔命令echo Hello & echo World在不同的行上產生兩個回聲
  • %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. 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.

這一行只將正確的行傳遞給 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

聽起來您正在尋找有時稱為“多語言腳本”的內容。 對於 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 那是 PowerShell v1; 在 v2 中可能更簡單,但我沒有看過。

這里已經討論了這個話題。 主要目標是避免使用臨時文件以減少緩慢的 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 讀取和執行%~f0 ,這是 cmd.exe 執行的腳本的完整路徑。

這似乎有效,如果您不介意一開始 PowerShell 中的一個錯誤:

dosps.cmd

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

還要考慮這個“多語言”包裝腳本,它支持嵌入式 PowerShell 和/或 VBScript/JScript 代碼 它改編自作者本人flabdablet於 2013 年發布的這個巧妙的原作,但由於僅提供鏈接答案,因此在 2015 年被刪除。

改進了凱爾出色答案的解決方案:

創建一個包含以下內容的批處理文件(例如sample.cmd ):

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

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

筆記:

  • 批處理文件運行時,會在%TEMP%文件夾中創建一個臨時的*.ps1文件,隨后會進行清理; 這樣做可以大大簡化(合理地)穩健地傳遞參數,只需使用%*
  • 以上調用Windows PowerShell 要調用跨平台的 PowerShell (Core) v7+ 版本,請將上面代碼中的powershell替換為pwsh

技術說明

  • Line <# ::是一個混合行,PowerShell 將其視為注釋塊的開始,但cmd.exe忽略,這是從npocmaka 的回答中借用的技術。

  • 因此,以@開頭的批處理文件命令會被 PowerShell 忽略,但會由cmd.exe執行; 由於最后一個@ -prefixed 行以exit /b結尾,它在那里退出批處理文件, cmd.exe忽略文件的其余部分,因此可以自由包含非批處理文件代碼,即 PowerShell 代碼。

  • #>行結束包含批處理文件代碼的 PowerShell 注釋塊。

  • 因為整個文件是一個有效的 PowerShell 文件,所以不需要findstr技巧來提取 PowerShell 代碼; 但是,由於 PowerShell 僅執行文件擴展.ps1腳本,因此必須創建批處理文件的(臨時)副本 %TEMP%\\%~n0.ps1在以批處理文件 ( %~n0 ) 命名的%TEMP%文件夾中創建臨時副本,但擴展.ps1 臨時文件在完成時自動刪除。

  • 請注意,需要 3單獨的cmd.exe語句來傳遞 PowerShell 命令的退出代碼。
    (使用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]

請注意嵌入的" 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 在他的優秀回答中指出的那樣

正如他所解釋的,\\"...\\"序列中使用^^^ (sic) 轉義以下cmd.exe元字符可以解決問題:

& | < >

使用上面的例子:

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

這支持與 Carlos 發布的解決方案不同的參數,並且不會像 Jay 發布的解決方案那樣破壞多行命令或param的使用。 唯一的缺點是此解決方案會創建一個臨時文件。 對於我的用例,這是可以接受的。

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

在沒有完全理解你的問題的情況下,我的建議是這樣的:

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

或者更好

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

我目前對這項任務的偏好是多語言標題,其工作方式與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 標頭放置為多行,每行一個命令。 首先,我認為更容易看到發生了什么:命令行足夠短,不會跑出我的編輯窗口的右側,左側的標點列在視覺上將其標記為標題塊,上面的標簽被嚴重濫用第一行說是。 其次, delgoto命令在它們自己的行上,因此即使某些非常時髦的東西作為腳本參數傳遞,它們仍然會運行。

我更喜歡制作臨時.ps1文件的解決方案,而不是那些依賴Invoke-Expression解決方案,純粹是因為 PowerShell 難以理解的錯誤消息將至少包含有意義的行號。

制作臨時文件所需的時間通常完全被 PowerShell 本身所需的時間淹沒,臨時文件名稱中嵌入的 128 位%RANDOM%幾乎可以保證多個並發腳本永遠不會踐踏彼此的臨時文件。 臨時文件方法的唯一真正缺點是可能丟失有關調用原始 cmd 腳本的目錄的信息,這是在第二行創建dir環境變量的基本原理。

顯然,對於 PowerShell 來說,不要對它在腳本文件上接受的文件擴展名如此挑剔會少得多,但是你會與你擁有的 shell 打架,而不是你希望擁有的 shell。

說到這一點:正如 mklement0 所觀察到的,

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

這確實中斷了,因為cmd.exe的參數解析完全沒有價值。 我通常發現,我嘗試隱藏cmd 的許多限制所做的工作越少,我給自己造成的意外錯誤就越少(我確信我可以提出包含括號的參數,這些參數會破壞 mklement0 否則無可挑剔的&符號轉義邏輯, 例如)。 在我看來,不那么痛苦,只是硬着頭皮使用類似的東西

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

當最初解析該命令行時,第一個和第三個^轉義會被吃掉; 第二個幸存下來,以逃避傳遞給powershell.exe的命令行中嵌入的& 是的,這很丑陋。 是的,它確實讓人更難假裝cmd.exe不是腳本的第一個破解。 別擔心。 如果重要,請記錄下來。

在大多數實際應用中, &問題無論如何都沒有實際意義。 大多數將作為參數傳遞給這樣的腳本的內容將是通過拖放到達的路徑名。 Windows 將引用這些內容,這足以保護空格和與符號,實際上除了引號之外的任何內容,無論如何都不允許在 Windows 路徑名中使用。

甚至不要讓我開始使用Vinyl LP's, 12"出現在 CSV 文件中。

另一個示例批處理 + PowerShell 腳本......它比其他建議的解決方案更簡單,並且具有它們都無法匹配的特性:

  • 不創建臨時文件 => 更好的性能,並且沒有覆蓋任何內容的風險。
  • 批次代碼沒有特殊前綴。 這只是普通批次。 PowerShell 代碼也是如此。
  • 將所有批處理參數正確傳遞給 PowerShell,甚至是帶有棘手字符的引號字符串,例如 ! % < > ' $
  • 雙引號可以通過加倍傳遞。
  • 標准輸入可在 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 注釋。 (或者實際上就像大多數其他腳本語言注釋一樣。)

我非常喜歡Jean-François Larvoire 的解決方案,尤其是他處理參數並將它們直接傳遞給 powershell 腳本(+1 添加)。

但它有一個缺陷。 由於我確實 npt 有評論的聲譽,因此我將更正作為新的解決方案發布。

當腳本名稱包含$字符時,作為雙引號中 Invoke-Expression 參數的腳本名稱將不起作用,因為這將在加載文件內容之前進行評估。 最簡單的補救方法是替換雙引號:

PowerShell -c ^"Invoke-Expression ('^& {' + [io.file]::ReadAllText('%~f0') + '} %ARGS%')"

就我個人而言,我更喜歡將get-content-raw選項一起使用,對我而言,這更強大:

PowerShell -c ^"Invoke-Expression ('^& {' + (get-content -raw '%~f0') + '} %ARGS%')"

但這當然只是我個人的意見。 ReadAllText works得非常完美。

為完整起見,更正后的腳本:

<# :# 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 ('^& {' + (get-content -raw '%~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

使用Invoke-Command (簡稱icm ),我們可以將以下 4 行 header 添加到 ps1 文件中,使其成為有效的 cmd 批處理:

<# : batch portion
@powershell -noprofile "& {icm -ScriptBlock ([Scriptblock]::Create((cat -Raw '%~f0'))) -NoNewScope -ArgumentList $args}" %*
@exit /b %errorlevel%
: end batch / begin powershell #>

"Result:"
$args | %{ "`$args[{0}]: $_" -f $i++ }

如果要使 args[0] 指向腳本路徑,請將%*更改為"'%~f0'" %*

您可以在 Powershell 腳本之前添加三行,僅使用塊注釋,然后將其保存為批處理文件。 然后,您可以有一個批處理文件來運行 Powershell 腳本。 例子:

psscript.bat

@echo off
@powershell -command "(Get-Content -Encoding UTF8 '%0' | select-string -pattern '^[^@]')" | @powershell -NoProfile -ExecutionPolicy ByPass
@goto:eof
<# Must use block comment; Powershell script starts below #>
while($True) {
    Write-Host "wait for 3s"
    Start-Sleep -Seconds 3
}

匯集一些想法

<# :
@powershell -<%~f0&goto:eof
#>

Write-Output "Hello World" 
Write-Output "Hello World again" 

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM