【问题标题】:In a CMD batch file, can I determine if it was run from powershell?在 CMD 批处理文件中,我可以确定它是否是从 powershell 运行的吗?
【发布时间】:2018-11-23 13:06:30
【问题描述】:

我有一个 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 调用的,以便例如向用户显示警告?这需要在没有任何第三方实用程序的情况下完成。

【问题讨论】:

  • This 可能会为您提供一些帮助,但请注意父进程和子进程之间的联系完全是肤浅的。如果您的父进程死亡(您的用例可能不是问题),内核绝不保证PID不会被重用。
  • @LievenKeersmaekers - 看起来不错:我需要找到当前进程 ID,然后检查祖先名称是否包含字符串“powershell”。
  • 好吧,this 可能会给你一些工作;)
  • 您也可以使用setx command 来永久设置环境变量,以便以后在PowerShell中也可以使用它们...
  • @aschipfl 谢谢,但我特别不希望它们成为永久性的。

标签: windows powershell batch-file cmd


【解决方案1】:

Your own answer is robust 虽然由于需要运行 PowerShell 进程而通常很慢,但通过优化用于确定调用 shell 的 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系统 环境变量被永久定义在注册表中(不是用户特定的)。

该解决方案依赖于检测PSModulePath 中是否存在用户特定路径,PowerShell 在启动时会自动添加该路径。

@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 运行

这不仅对用户更方便,还减轻了上述解决方案产生误报的问题:当从 从 PowerShell 启动的交互式 cmd.exe 会话运行时,上述正如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,但在相同新窗口中,基于调用用户更喜欢在 PowerShell 中工作的假设.
新的 PowerShell 会话将看到新定义的环境变量,但请注意,您需要 两个 连续调用 exit 来关闭窗口。

【讨论】:

  • 谢谢。我的第一个版本并没有像您那样获得 3-4 倍的性能提升,但是正如您所说,您的第二个版本更快,并且对于我的用例来说足够强大。
【解决方案2】:

正如乔·科克 (Joe Cocker) 常说的“我在朋友的帮助下过得很好”。

在这种情况下来自Lieven Keersmaekers,他的 cmets 引导我找到以下解决方案:

@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

【讨论】:

  • 你的解决方案有很大的缺陷。我可以从 PowerShell 运行交互式 CMD 会话。然后从交互式CMD 会话中调用您的脚本,据我所知,这是预期的使用场景。并且您的脚本将拒绝工作,因为交互式 CMD 会话是从 PowerShell 启动的。
  • @PetSerAl - 我的用例并不要求它是 100% 健壮的。对于从 PowerShell 中创建的 CMD 会话,可以在 PowerShell 检测中出现“误报”。我不想要的是“假阴性”,这会导致用户想知道“为什么我的环境变量没有按预期设置”。
【解决方案3】:

我为 Chocolatey 的 RefreshEnv.cmd 脚本做了类似的事情:Make refreshenv.bat error if powershell.exe is being used

由于不相关的原因,我的解决方案没有最终被使用,但它可以在这个 repo 中找到:beatcracker/detect-batch-subshell。这是它的副本,以防万一。


仅当直接从交互式命令处理器会话调用时才会运行的脚本

脚本将检测它是否从非交互式会话 (cmd.exe /c detect-batch-subshell.cmd) 运行并显示相应的错误消息。

非交互式 shell 包括 PowerShell/PowerShell ISE、Explorer 等...基本上任何会尝试通过在单独的 cmd.exe 实例中运行脚本来执行脚本的东西。

但是,从 PowerShell/PowerShell ISE 进入 cmd.exe 会话并在那里执行脚本将起作用。

依赖关系

  • 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

【讨论】:

  • 考虑以下 PowerShell 命令行:cmd /c detect-batch-subshell.cmd `&amp; using updated environment。我将多个命令传递给cmd,以便我更新环境并在同一个cmd 实例中使用它。是否有效?
  • @PetSerAl,您可以通过将@ 添加到脚本路径:cmd /c @detect-batch-subshell.cmd &amp; UsingUpdatedEnvironment 来绕过它。无论如何,这个解决方案有一些缺陷,例如如果脚本的路径包含空格,它会失败,并且它通过使用WMI的缓慢而复杂的方法获取命令行,而CMDCMDLINE 可以很容易地获得它。我将添加一个更简单的答案来解决这些问题。
  • 看起来很有趣,有时间我会去看看,虽然需要一些学习才能理解脚本
  • @sst 感谢您发现空间问题。关于CMDCMDLINE,你说的很对,比WMI好多了,只是我不知道而已。
  • @PetSerAl 修复路径中空格问题的副作用是,如果脚本被传递任何参数,它不会检测到它是在子shell中启动的。所以这应该工作:)。
【解决方案4】:

就像beatcracker 的回答一样,我认为最好不要对可用于启动批处理脚本的外部 shell 进行假设,例如,通过 bash shell 运行批处理文件时也会出现问题。

因为它专门使用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 shell 的命令行,以判断脚本是从CMD 内部启动还是由外部应用程序使用命令行签名/C script.bat 启动,非CMD shell 通常使用该签名启动批处理脚本。

如果出于任何原因需要绕过外部启动检测,例如当使用附加命令手动启动脚本以利用定义的变量时,可以通过在 CMD 中的脚本路径前添加 @ 来完成命令行:

cmd /c @MyScript.bat & AdditionalCommands

【讨论】:

  • prepending @ to the path - 你能详细说明为什么会这样吗?我的批次有点生锈了。
  • @beatcracker,这个解决方案不会通过命令行检测所有可能的批处理脚本调用方法,它只会检查Powershell 或windows shell 组件(例如 Explorer.exe)或其他程序。例如,以这种方式调用批处理文件:cmd /s /c MyScript.bat 会破坏此或您的解决方案所依赖的模式。在批处理文件路径前添加@ 是最简单、最安全的打破模式的方法。
  • @beatcracker,但是如果保留了PathToCMD /C PathToBatchScript 模式,即使将其他参数传递给批处理脚本,它仍然可以检测到外部调用。例如这个工作(检测外部调用):cmd /c ""MyScript.bat" some additional quoted or unquoted parameters"
  • 更新了脚本以涵盖更多形式的调用,例如当 /C 以外的其他开关也传递给 CMD 时。例如cmd /d /s /c "script.cmd"
猜你喜欢
  • 1970-01-01
  • 2019-07-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-01
相关资源
最近更新 更多