Как запустить PowerShell script в пакетном файле Windows
Как встроить сценарий PowerShell в тот же файл, что и пакетный сценарий Windows?
Я знаю, что такие вещи возможны в других сценариях:
- Встраивание SQL в пакетный скрипт с использованием
sqlcmd
и хитрой компоновки goto и комментариев в начале файла - В среде * nix с именем программы, с которой вы хотите запустить сценарий в первой строке закомментированного сценария, например,
#!/usr/local/bin/python
.
Возможно, не существует способа сделать это - в этом случае мне придется вызывать отдельный сценарий PowerShell из запускающего сценария.
Одним из возможных решений, которое я рассмотрел, является вывод сценария PowerShell, а затем его запуск. Хорошая причина не делать этого заключается в том, что одна из причин, по которой это делается, заключается в использовании преимуществ среды PowerShell без необходимости, например, экранирования символов.
У меня есть некоторые необычные ограничения, и я хотел бы найти элегантное решение. Я подозреваю, что этот вопрос может вызывать различные ответы: "Почему бы вам не попытаться решить эту другую проблему?" Достаточно сказать, что это мои ограничения, извините за это.
Есть идеи? Есть ли подходящая комбинация умных комментариев и escape-символов, которые позволят мне добиться этого?
Несколько мыслей о том, как этого добиться:
- Карат
^
в конце строки является продолжением - как подчеркивание в 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.
Ответы
Ответ 1
Этот только передает правильные строки в PowerShell:
dosps2.cmd
:
@findstr/v "^@f.*&" "%~f0"|powershell -&goto:eof
Write-Output "Hello World"
Write-Output "Hello [email protected] & again"
Регулярное выражение исключает строки, начинающиеся с @f
и включающие &
, и передает все остальное в PowerShell.
C:\tmp>dosps2
Hello World
Hello [email protected] & again
Ответ 2
Похоже, вы ищете то, что иногда называют "polyglot script". Для 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 $('[email protected](^&{$args} %POWERSHELL_BAT_ARGS%);'+[String]::Join([char]10,$((Get-Content '%~f0') -notmatch '^^@@'))) & goto :EOF
Если вам не нужно поддерживать цитируемые аргументы, вы можете сделать это одним слоем:
@PowerShell -Command Invoke-Expression $('[email protected](^&{$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, но я не смотрел.
Ответ 3
Это похоже на работу, если вы не возражаете против одной ошибки в PowerShell в начале:
dosps.cmd
:
@powershell -<%~f0&goto:eof
Write-Output "Hello World"
Write-Output "Hello World again"
Ответ 4
Здесь тема уже обсуждалась. Основными целями было избежать использования временных файлов для сокращения медленных операций ввода-вывода и запуска сценария без избыточного вывода.
И вот лучшее решение по мне:
<# :
@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.
Ответ 5
Также рассмотрим этот скрипт-обертку "polyglot", который поддерживает встроенные PowerShell и/или VBScript/JScript cod e;он был адаптирован из этого оригинального оригинала, который сам автор, flabdablet, опубликовал в 2013 году, но он томился из-за ответа только по ссылке, который был удален в 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
; поскольку последняя строка @
-prefixed оканчивается на exit/b
, который тут же выходит из пакетного файла, cmd.exe
игнорирует остальную часть файла, которая поэтому может содержать код, не являющийся пакетным файлом, то есть код PowerShell.
-
Строка #>
завершает блок комментариев PowerShell, который включает код пакетного файла.
-
Поскольку файл в целом является допустимым файлом PowerShell, для извлечения кода PowerShell не требуется хитрость findstr
; однако, поскольку PowerShell выполняет только сценарии с расширением имени файла .ps1
, необходимо создать (временную) копию пакетного файла; %TEMP%\%~0n.ps1
создает временную копию в папке %TEMP%
названной для пакетного файла (%~0n
), но с расширением .ps1
; Временный файл автоматически удаляется по завершении.
-
Обратите внимание, что для передачи кода завершения команды PowerShell необходимы 3 отдельные строки операторов cmd.exe
.
(Использование setlocal enabledelayedexpansion
гипотетически позволяет сделать это одной строкой, но это может привести к нежелательной интерпретации !
Chars. В аргументах.)
Чтобы продемонстрировать надежность передачи аргумента:
Предполагая, что код выше был сохранен как 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]
Обратите внимание, как внедренный "
символы. Были переданы в качестве \"
.
Однако, есть крайние случаи, связанные с встроенными "
гольцов.:
:: # 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"
Ответ 6
Без полного понимания вашего вопроса мое предложение было бы примерно таким:
@echo off
set MYSCRIPT="some cool powershell code"
powershell -c %MYSCRIPT%
или еще лучше
@echo off
set MYSCRIPTPATH=c:\work\bin\powershellscript.ps1
powershell %MYSCRIPTPATH%
Ответ 7
Это поддерживает аргументы, в отличие от решения, опубликованного Carlos, и не прерывает многострочные команды или использование param
, как решение, отправленное Jay. Единственным недостатком является то, что это решение создает временный файл. Для моего случая использования, который является приемлемым.
@@echo off
@@findstr/v "^@@.*" "%~f0" > "%~f0.ps1" & powershell -ExecutionPolicy ByPass "%~f0.ps1" %* & del "%~f0.ps1" & goto:eof
Ответ 8
В настоящее время я предпочитаю эту задачу - заголовок полиглота, который работает почти так же, как первое решение 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 в виде нескольких строк с одной командой в каждой. Во-первых, я думаю, что легче понять, что происходит: командные строки достаточно короткие, чтобы не выходить за пределы правого окна редактирования, а столбец пунктуации слева помечает его визуально как блок заголовка, на котором написано ужасно злоупотребленное обозначение. первая строка говорит, что это так. Во-вторых, команды del
и goto
находятся в своих собственных строках, поэтому они по-прежнему будут выполняться, даже если в качестве аргумента скрипта будет передано что-то действительно прикольное.
Я предпочел решения, которые создают временный файл .ps1
, а не решения Invoke-Expression
исключительно потому, что непонятные сообщения об ошибках PowerShell будут, по крайней мере, содержать значащие номера строк.
Время, затрачиваемое на создание временного файла, обычно полностью затопляется к тому времени, когда сам PowerShell превращается в лес, в действие, и 128-разрядное значение %RANDOM%
встроенное в имя временного файла, в значительной степени гарантирует, что несколько параллельных сценариев никогда не будут топать друг друга временными файлами. Единственный реальный недостаток подхода временного файла - это возможная потеря информации о каталоге, из которого был вызван исходный сценарий cmd, что является обоснованием для переменной окружения dir
созданной во второй строке.
Очевидно, что PowerShell было бы гораздо менее раздражающим, если бы он не был настолько внимателен к расширениям имен файлов, которые он будет принимать в файлах сценариев, но вы вступаете в войну с имеющейся у вас оболочкой, а не с оболочкой, которая вам нужна.
Кстати говоря, как замечает 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.
Ответ 9
Другой пример пакета + сценарий 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 C command line parser
:# considers these pairs as part of the first and only argument following -c.
:# Cherry on the cake, it 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. (Или, как и большинство других языков сценариев, комментарии на самом деле.)