Как запустить сценарий PowerShell в командном файле Windows - PullRequest
17 голосов
/ 09 апреля 2010

Как мне встроить скрипт PowerShell в тот же файл, что и пакетный скрипт Windows?

Я знаю, что подобные вещи возможны в других сценариях:

  • Встраивание SQL в пакетный скрипт с использованием sqlcmd и продуманное расположение комментариев и комментариев в начале файла
  • В среде * nix с именем программы, с которой вы хотите запустить сценарий в первой строке сценария, закомментированным, например, #!/usr/local/bin/python.

Может не быть способа сделать это - в этом случае мне придется вызывать отдельный сценарий PowerShell из запускающего сценария.

Одним из возможных решений, которое я рассмотрел, является вывод сценария PowerShell и его запуск. Хорошая причина для , а не для этого заключается в том, что одна из причин, по которой можно попытаться это сделать, заключается в использовании преимуществ среды PowerShell без необходимости, например, escape-символов

У меня есть некоторые необычные ограничения, и я хотел бы найти элегантное решение. Я подозреваю, что этот вопрос может вызывать различные ответы: «Почему бы вам не попытаться решить эту другую проблему?» Достаточно сказать, что это мои ограничения, извините за это.

Есть идеи? Есть ли подходящая комбинация умных комментариев и 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.
  • CMD на самом деле тоже не совсем доволен ситуацией - хотя он наткнулся на '#', it is not recognized as an internal or external command, operable program or batch file.

Ответы [ 9 ]

17 голосов
/ 10 апреля 2010

Эта строка передает только правильные строки в PowerShell:

dosps2.cmd * * 1004

@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
11 голосов
/ 10 апреля 2010

Звучит так, будто вы ищете то, что иногда называют «сценарием полиглота». Для 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, но я не смотрел.

3 голосов
/ 11 октября 2015

Здесь тема уже обсуждалась. Основными целями было избежать использования временных файлов для сокращения медленных операций ввода-вывода и запуска сценария без избыточного вывода.

А вот лучшее решение по моему мнению:

<# :
@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.

3 голосов
/ 10 апреля 2010

Это похоже на работу, если вы не возражаете против одной ошибки в PowerShell в начале:

dosps.cmd * * 1004

@powershell -<%~f0&goto:eof
Write-Output "Hello World" 
Write-Output "Hello World again" 
2 голосов
/ 02 марта 2018

Также рассмотрим этот скрипт-обертку "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; так как последняя строка с префиксом @ оканчивается на exit /b, который тут же выходит из пакетного файла, cmd.exe игнорирует остальную часть файла, которая поэтому может содержать не пакетный файл код, т.е. код PowerShell.

  • Строка #> завершает блок комментариев PowerShell, в который входит код пакетного файла.

  • Поскольку файл в целом является действительным файлом PowerShell, для извлечения кода PowerShell не требуется хитрость findstr; однако, поскольку PowerShell выполняет только сценарии с расширением имени файла .ps1, необходимо создать (временную) копию пакетного файла; %TEMP%\%~0n.ps1 создает временную копию в папке %TEMP%, названной для командного файла (%~0n), но с расширением .ps1; временно файл автоматически удаляется по завершении.

  • Обратите внимание, что для передачи кода завершения команды PowerShell требуется 3 отдельных строки из cmd.exe.
    (Использование 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]

Обратите внимание, как вложено " символов. были переданы как \".
Однако существуют крайние случаи , связанные со встроенными " символами. :

:: # 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"
1 голос
/ 06 марта 2018

Моим текущим предпочтением для этой задачи является заголовок полиглота, который работает почти так же, как первое решение 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 начинает работать, а значение %RANDOM% на 128 бит, встроенное в имя временного файла, в значительной степени гарантирует выигрыш нескольких одновременных сценариев. никогда не топайте друг друга временными файлами. Единственный реальный недостаток подхода временных файлов - это возможная потеря информации о каталоге, из которого был вызван исходный сценарий 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-файле.

1 голос
/ 12 июня 2013

Это поддерживает аргументы в отличие от решения, размещенного Карлосом, и не нарушает многострочные команды или использование param, как решение, опубликованное Джей. Единственным недостатком является то, что это решение создает временный файл. Для моего случая использования это приемлемо.

@@echo off
@@findstr/v "^@@.*" "%~f0" > "%~f0.ps1" & powershell -ExecutionPolicy ByPass "%~f0.ps1" %* & del "%~f0.ps1" & goto:eof
1 голос
/ 09 апреля 2010

Без полного понимания вашего вопроса мое предложение будет выглядеть примерно так:

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

или еще лучше

@echo off
set MYSCRIPTPATH=c:\work\bin\powershellscript.ps1
powershell %MYSCRIPTPATH%
0 голосов
/ 18 мая 2018

Еще один пример пакета + сценарий 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. (Или, как и большинство других языков сценариев, комментарии.)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...