В командном файле CMD я могу определить, был ли он запущен из powershell? - PullRequest
0 голосов
/ 23 ноября 2018

У меня есть пакетный файл Windows, целью которого является установка некоторых переменных среды, например,

=== MyFile.cmd ===
SET MyEnvVariable=MyValue

. Пользователи могут запускать это до выполнения работы, для которой требуется переменная среды, например:

C:\> MyFile.cmd
C:\> echo "%MyEnvVariable%"    <-- outputs "MyValue"
C:\> ... do work that needs the environment variable

Это примерно эквивалентно ярлыкам «Командная строка разработчика», установленным Visual Studio, в которых задаются переменные среды, необходимые для запуска утилит VS.

Однако, если пользователь открывает открытую подсказку Powershell, средапеременная, конечно, не передается обратно в Powershell:

PS C:\> MyFile.cmd
PS C:\> Write-Output "${env:MyEnvVariable}"  # Outputs an empty string

Это может сбивать с толку пользователей, которые переключаются между CMD и PowerShell.

Есть ли способ, который я могу обнаружить в моем пакетном файле MyFile.cmd чтобы он вызывался из PowerShell, чтобы я мог, например, вывести предупреждение пользователю?Это должно быть сделано без какой-либо сторонней утилиты.

Ответы [ 4 ]

0 голосов
/ 26 ноября 2018

Как и ответ от beatcracker Я думаю, что было бы лучше не принимать предположения о внешней оболочке, которая может использоваться для запуска пакетного сценария, например, проблема также может возникнуть при запуске пакетафайл через оболочку bash.

Поскольку он использует исключительно собственные средства CMD и не зависит от какого-либо внешнего инструмента или WMI, время выполнения очень быстрое.

@echo off
call :IsInvokedInternally && (
    echo Script is launched from an interactive CMD shell or from another batch script.
) || (
    echo Script is invoked by an external App. [PowerShell, BASH, Explorer, CMD /C, ...]
)
exit /b

:IsInvokedInternally
setlocal EnableDelayedExpansion
:: Getting substrings from the special variable CMDCMDLINE,
:: will modify the actual Command Line value of the CMD Process!
:: So it should be saved in to another variable before applying substring operations.
:: Removing consecutive double quotes eg. %systemRoot%\system32\cmd.exe /c ""script.bat""
set "SavedCmdLine=!cmdcmdline!"
set "SavedCmdLine=!SavedCmdLine:""="!"
set /a "DoLoop=1, IsExternal=0"
set "IsCommand="
for %%A in (!SavedCmdLine!) do if defined DoLoop (
    if not defined IsCommand (
        REM Searching for /C switch, everything after that, is CMD commands
        if /i "%%A"=="/C" (
            set "IsCommand=1"
        ) else if /i "%%A"=="/K" (
            REM Invoking the script with /K switch creates an interactive CMD session
            REM So it will be considered an internal invocatoin
            set "DoLoop="
        )
    ) else (
        REM Only check the first command token to see if it references this script
        set "DoLoop="

        REM Turning delayed expansion off to prevent corruption of file paths
        REM which may contain the Exclamation Point (!)
        REM It is safe to do a SETLOCAL here because the we have disabled the Loop,
        REM and the routine will be terminated afterwards.
        setlocal DisableDelayedExpansion
        if /i "%%~fA"=="%~f0" (
            set "IsExternal=1"
        ) else if /i "%%~fA"=="%~dpn0" (
            set "IsExternal=1"
        )
    )
)
:: A non-zero ErrorLevel means the script is not launched from within CMD.
exit /b %IsExternal%

Он проверяет командную строку, которая использовалась для запуска оболочки CMD, чтобы определить, был ли скрипт запущен из CMD или внешним приложением, использующим сигнатуру командной строки /C script.bat, которая обычно используется оболочками без CMDзапускать пакетные сценарии.

Если по какой-либо причине необходимо обойти обнаружение внешнего запуска, например, при ручном запуске сценария с дополнительными командами для использования определенных переменных, это можно сделать, добавив @ кпуть к скрипту в командной строке CMD:

cmd /c @MyScript.bat & AdditionalCommands
0 голосов
/ 25 ноября 2018

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

@echo off
setlocal
CALL :GETPARENT PARENT
IF /I "%PARENT%" == "powershell" GOTO :ISPOWERSHELL
IF /I "%PARENT%" == "pwsh" GOTO :ISPOWERSHELL
endlocal

echo Not running from Powershell 
SET MyEnvVariable=MyValue

GOTO :EOF

:GETPARENT
SET "PSCMD=$ppid=$pid;while($i++ -lt 3 -and ($ppid=(Get-CimInstance Win32_Process -Filter ('ProcessID='+$ppid)).ParentProcessId)) {}; (Get-Process -EA Ignore -ID $ppid).Name"

for /f "tokens=*" %%i in ('powershell -noprofile -command "%PSCMD%"') do SET %1=%%i

GOTO :EOF

:ISPOWERSHELL
echo. >&2
echo ERROR: This batch file may not be run from a PowerShell prompt >&2
echo. >&2
exit /b 1

На моей машине это работает примерно в 3-4 раза быстрее (YMMV), но все равно занимает почти 1 секунду.

Обратите внимание, что я добавил проверкуимя процесса pwsh, чтобы решение работало с PowerShell Core .


Гораздо более быстрая альтернатива - хотя и менее надежная :

Решение ниже опирается на следующее допущение , которое истинно при установке по умолчанию :

Только система средапеременная с именем PSModulePath постоянно определена в реестре (не также для пользователя one).

Решение основано на обнаружении присутствия пути пользователя в PSModulePath, который PowerShell автоматически добавляет, когдаart.

@echo off
echo %PSModulePath% | findstr %USERPROFILE% >NUL
IF %ERRORLEVEL% EQU 0 goto :ISPOWERSHELL

echo Not running from Powershell 
SET MyEnvVariable=MyValue

GOTO :EOF

:ISPOWERSHELL
echo. >&2
echo ERROR: This batch file may not be run from a PowerShell prompt >&2
echo. >&2
exit /b 1

Альтернативный подход для запуска нового cmd.exe окна консоли по требованию :

Основываясь на предыдущем подходе, следующий вариант просто повторно вызывает командный файл в новом cmd.exe окне при обнаружении его запуска из PowerShell .

Это не только более удобно для пользователяЭто также уменьшает проблему решений, приведенных выше, и дает ложные срабатывания: при запуске из интерактивного cmd.exe сеанса, который был запущен из PowerShell , вышеуказанные решения откажутся запускаться, даже если они должны, как PetSerAl указывает.
Хотя приведенное ниже решение также не обнаруживает этот случай как таковое, оно все же открывает полезное - хотя и новое - окно с установленными переменными среды.

@echo off
REM # Unless already being reinvoked via cmd.exe, see if the batch
REM # file is being run from PowerShell.
IF NOT %1.==_isNew. echo %PSModulePath% | findstr %USERPROFILE% >NUL
REM # If so, RE-INVOKE this batch file in a NEW cmd.exe console WINDOW.
IF NOT %1.==_isNew. IF %ERRORLEVEL% EQU 0 start "With Environment" "%~f0" _isNew & goto :EOF

echo Running from cmd.exe, setting environment variables...

REM # Set environment variables.
SET MyEnvVariable=MyValue

REM # If the batch file had to be reinvoked because it was run from PowerShell,
REM # but you want the user to retain the PowerShell experience,
REM # restart PowerShell now, after definining the env. variables.
IF %1.==_isNew. powershell.exe

GOTO :EOF

После установки всех переменных среды обратите внимание, что последний оператор IF также повторно вызывает PowerShell, но в новом окне с тем же , исходя из предположения, что calПользователь предпочитает работать в PowerShell.
После этого новый сеанс PowerShell увидит вновь определенные переменные среды, хотя учтите, что вам потребуется два последовательных exit вызова для закрытия окна.

0 голосов
/ 26 ноября 2018

Я сделал что-то подобное для сценария Chocolatey's RefreshEnv.cmd : Сделайте ошибку refreshenv.bat, если используется powershell.exe .

Мое решение не помоглоОн не используется по несвязанным причинам, но он доступен в этом репо: beatcracker / detect-batch-subshell .Вот его копия, на всякий случай.


Скрипт, который будет запускаться только при вызове непосредственно из сеанса интерактивного командного процессора

Скрипт обнаружит, запущен ли он из неинтерактивного сеанса (cmd.exe /c detect-batch-subshell.cmd) и выводит сообщение об ошибке approriate.

Неинтерактивная оболочка включает в себя PowerShell / PowerShell ISE, Explorer и т. Д. В основном все, что будет пытаться выполнить сценарий, запустив его в отдельном экземпляре cmd.exe.

Hovewer, переход в сеанс cmd.exe из PowerShell / PowerShell ISE и выполнение там сценария будут работать.

Зависимости

  • wmic.exe - поставляется с Windows XP Professional и выше.

Пример:

  1. Открыть cmd.exe
  2. Тип detect-batch-subshell.cmd

Выход:

> detect-batch-subshell.cmd

Running interactively in cmd.exe session.

Пример:

  1. Открыть powershell.exe
  2. Тип detect-batch-subshell.cmd

Выход:

PS > detect-batch-subshell.cmd

detect-batch-subshell.cmd only works if run directly from cmd.exe!

Код

  • detect-batch-subshell.cmd
@echo off

setlocal EnableDelayedExpansion

:: Dequote path to command processor and this script path
set ScriptPath=%~0
set CmdPath=%COMSPEC:"=%

:: Get command processor filename and filename with extension
for %%c in (!CmdPath!) do (
    set CmdExeName=%%~nxc
    set CmdName=%%~nc
)

:: Get this process' PID
:: Adapted from: http://www.dostips.com/forum/viewtopic.php?p=22675#p22675
set "uid="
for /l %%i in (1 1 128) do (
    set /a "bit=!random!&1"
    set "uid=!uid!!bit!"
)

for /f "tokens=2 delims==" %%i in (
    'wmic Process WHERE "Name='!CmdExeName!' AND CommandLine LIKE '%%!uid!%%'" GET ParentProcessID /value'
) do (
    rem Get commandline of parent
    for /f "tokens=1,2,*" %%j in (
        'wmic Process WHERE "Handle='%%i'" GET CommandLine /value'
    ) do (

        rem Strip extra CR's from wmic output
        rem http://www.dostips.com/forum/viewtopic.php?t=4266
        for /f "delims=" %%x in ("%%l") do (
            rem Dequote path to batch file, if any (3rd argument)
            set ParentScriptPath=%%x
            set ParentScriptPath=!ParentScriptPath:"=!
        )

        rem Get parent process path
        for /f "tokens=2 delims==" %%y in ("%%j") do (
            rem Dequote parent path
            set ParentPath=%%y
            set ParentPath=!ParentPath:"=!

            rem Handle different invocations: C:\Windows\system32\cmd.exe , cmd.exe , cmd
            for %%p in (!CmdPath! !CmdExeName! !CmdName!) do (
                if !ParentPath!==%%p set IsCmdParent=1
            )

            rem Check if we're running in cmd.exe with /c switch and this script path as argument
            if !IsCmdParent!==1 if %%k==/c if "!ParentScriptPath!"=="%ScriptPath%" set IsExternal=1
        )
    )
)

if !IsExternal!==1 (
    echo %~nx0 only works if run directly from !CmdExeName!^^!
    exit 1
) else (
     echo Running interactively in !CmdExeName! session.
 )

endlocal
0 голосов
/ 24 ноября 2018

Как говорил Джо Кокер: «Я получаю немного помощи от моих друзей».

В данном случае от Ливена Керсмейкера , чьи комментарии привели меня к следующему решению:

@echo off
setlocal
CALL :GETPARENT PARENT
IF /I "%PARENT%" == "powershell.exe" GOTO :ISPOWERSHELL
endlocal

echo Not running from Powershell 
SET MyEnvVariable=MyValue

GOTO :EOF

:GETPARENT
SET CMD=$processes = gwmi win32_process; $me = $processes ^| where {$_.ProcessId -eq $pid}; $parent = $processes ^| where {$_.ProcessId -eq $me.ParentProcessId} ; $grandParent = $processes ^| where {$_.ProcessId -eq $parent.ParentProcessId}; $greatGrandParent = $processes ^| where {$_.ProcessId -eq $grandParent.ParentProcessId}; Write-Output $greatGrandParent.Name

for /f "tokens=*" %%i in ('powershell -command "%CMD%"') do SET %1=%%i

GOTO :EOF

:ISPOWERSHELL
echo.
echo ERROR: This batch file may not be run from a PowerShell prompt
echo.
cmd /c "exit 1"
GOTO :EOF
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...