简体   繁体   English

在CMD批处理文件中,我可以确定它是否是从PowerShell运行的吗?

[英]In a CMD batch file, can I determine if it was run from powershell?

I have a Windows batch file whose purpose is to set some environment variables, eg 我有一个Windows批处理文件,其目的是设置一些环境变量,例如

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

Users can run this prior to doing work that needs the environment variable, eg: 用户可以在完成需要环境变量的工作之前运行它,例如:

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

This is roughly equivalent to the "Developer command prompt" shortcuts installed by Visual Studio, which set environment variables needed to run VS utilities. 这大致相当于Visual Studio安装的“Developer命令提示符”快捷方式,它设置了运行VS实用程序所需的环境变量。

However if a user happens to have a Powershell prompt open, the environment variable is of course not propagated back to Powershell: 但是,如果用户碰巧打开了Powershell提示符,则环境变量当然不会传播回Powershell:

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

This can be confusing for users who switch between CMD and PowerShell. 对于在CMD和PowerShell之间切换的用户而言,这可能会造成混淆。

Is there a way I can detect in my batch file MyFile.cmd that it was called from PowerShell, so that I can, for example, display a warning to the user? 有没有办法可以在我的批处理文件MyFile.cmd中检测到它是从PowerShell调用的,所以我可以,例如,向用户显示警告? This needs to be done without any 3rd party utility. 这需要在没有任何第三方实用程序的情况下完成。

Your own answer is robust and while it is generally slow due to needing to run a PowerShell process, it can be made significantly faster by optimizing the PowerShell command used to determine the calling shell : 您自己的答案是健壮的 ,虽然由于需要运行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

On my machine, this runs about 3 - 4 times faster (YMMV) - but still takes almost 1 second. 在我的机器上,这个速度大约快3到4倍(YMMV) - 但仍然需要将近1秒。

Note that I've added a check for process name pwsh as well, so as to make the solution work with PowerShell Core too. 请注意,我还添加了对进程名称pwsh的检查,以便使解决方案也适用于PowerShell Core


Much faster alternative - though less robust : 更快的替代方案 - 虽然不那么强大

The solution below relies on the following assumption , which is true in a default installation : 下面的解决方案依赖于以下假设在默认安装中也是如此

Only a system environment variable named PSModulePath is persistently defined in the registry (not also a user-specific one). 只有名为PSModulePath系统环境变量在注册表中持久定义(不是用户特定的 )。

The solution relies on detecting the presence of a user-specific path in PSModulePath , which PowerShell automatically adds when it starts. 该解决方案依赖于检测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

Alternative approach for launching a new cmd.exe console window on demand : 根据需要启动新cmd.exe控制台窗口的替代方法

Building on the previous approach, the following variant simply re-invokes the batch file in a new cmd.exe window on detecting that it is being run from PowerShell . 在以前的方法的基础上,以下变体只是在检测到它是从PowerShell运行时, 在新的cmd.exe窗口中重新调用批处理文件

This is not only more convenient for the user, it also mitigates the problem of the solutions above yielding false positives: When run from an interactive cmd.exe session that was launched from PowerShell , the above solutions will refuse to run, even though they should, as PetSerAl points out. 这不仅对用户来说更方便,还可以缓解上述解决方案产生误报的问题: 从PowerShell启动的交互式cmd.exe会话运行时,上述解决方案将拒绝运行,即使它们应该,正如PetSerAl指出的那样。
While the solution below also doesn't detect this case per se, it still opens a useable - albeit new - window with the environment variables set. 虽然下面的解决方案本身也没有检测到这种情况,但它仍然会打开一个可用的 - 尽管是新的 - 窗口,其中设置了环境变量。

@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

After setting all environment variables, note how the last IF statement, also re-invokes PowerShell, but in the same new window, based on the assumption that the calling user prefers working in PowerShell. 在设置所有环境变量之后,请注意最后一个IF语句如何重新调用PowerShell,但是在同一个新窗口中,基于调用用户更喜欢在PowerShell中工作的假设。
The new PowerShell session will then see newly defined environment variables, though note that you'll need two successive exit calls to close the window. 然后,新的PowerShell会话将看到新定义的环境变量,但请注意,您需要两次连续的exit调用才能关闭窗口。

As Joe Cocker used to say "I get by with a little help from my friends". 正如Joe Cocker曾经说过的那样:“我在朋友的帮助下过得很快”。

In this case from Lieven Keersmaekers , whose comments led me to the following solution: 在这种情况下,来自Lieven Keersmaekers ,他的评论使我得到以下解决方案:

@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

I did something like this for Chocolatey's RefreshEnv.cmd script: Make refreshenv.bat error if powershell.exe is being used . 我为Chocolatey的RefreshEnv.cmd脚本执行了类似的操作如果正在使用powershell.exe,请 生成refreshenv.bat 错误

My solution didn't end being used, for unrelated reasons, but it's available in this repo: beatcracker/detect-batch-subshell . 由于无关的原因,我的解决方案没有结束使用,但它可以在这个回购中使用: beatcracker / detect-batch-subshel​​l Here is copy of it, just in case. 这是它的副本,以防万一。


Script that will only run if called directly from interactive command processor session 仅在交互式命令处理器会话中直接调用时才会运行的脚本

Script will detect if it's run from non-interactive session ( cmd.exe /c detect-batch-subshell.cmd ) and show approriate error message. 脚本将检测它是否从非交互式会话( cmd.exe /c detect-batch-subshell.cmd )运行并显示approriate错误消息。

Non-interactive shell includes PowerShell/PowerShell ISE, Explorer, etc... Basically anything that will try to execute script by running it in the separate cmd.exe instance. 非交互式shell包括PowerShell / PowerShell ISE,Explorer等...基本上任何尝试通过在单独的cmd.exe实例中运行脚本来执行脚本的东西。

Hovewer, dropping into the cmd.exe session from PowerShell/PowerShell ISE and executing script there will work. Hovewer,从PowerShell / PowerShell ISE进入cmd.exe会话并执行脚本将有效。

Dependencies 依赖

  • wmic.exe - comes with Windows XP Professional and up. wmic.exe - 随Windows XP Professional一起提供。

Example: 例:

  1. Open cmd.exe 打开cmd.exe
  2. Type detect-batch-subshell.cmd 输入detect-batch-subshell.cmd

Output: 输出:

> detect-batch-subshell.cmd

Running interactively in cmd.exe session.

Example: 例:

  1. Open powershell.exe 打开powershell.exe
  2. Type detect-batch-subshell.cmd 输入detect-batch-subshell.cmd

Output: 输出:

PS > detect-batch-subshell.cmd

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

Code

  • 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

Like the answer from beatcracker I think it would be better to not take assumptions about the external shell that can be used to launch the batch script, for instance, the issue can also arise when running the batch file through the bash shell. 就像beatcracker的回答一样,我认为最好不要对可用于启动批处理脚本的外壳进行假设,例如,当通过bash shell运行批处理文件时也会出现问题。

Because it exclusively uses the native facilities of CMD and has no dependency on any external tool or the WMI , the execution time is very fast. 因为它专门使用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%

It checks the command line that used to launch the CMD shell to tell if script have been launched from within CMD or by an external app using the command line signature /C script.bat which is typically used by non CMD shells to launch batch scripts. 它检查用于启动CMD shell的命令行,以判断是否已从CMD内部或使用命令行signature /C script.bat从外部应用程序启动脚本,该脚本通常由非CMD shell用于启动批处理脚本。

If for any reason the external launch detection needs to bypasses, for instance when manually launching the script with additional commands to take advantage the defined variables, it can done by prepending @ to the path of the script in CMD command line: 如果出于任何原因需要绕过外部启动检测,例如当使用其他命令手动启动脚本以利用已定义的变量时,可以通过在CMD命令行中将@前置到脚本的路径来完成:

cmd /c @MyScript.bat & AdditionalCommands

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 Windows 10:如何确定是否正在从网络映射的驱动器运行批处理文件 - Windows 10: How can I determine whether a batch file is being run from network mapped drive 从cmd运行命令,但不在批处理文件中运行 - command is run from cmd but not in batch file 如何以管理员身份从批处理文件运行 powershell 脚本以加入域? - How can i run a powershell script from a batch file as admin to join a domain? 我可以运行带有隐藏cmd窗口的Windows批处理文件但是看到批处理启动的程序吗? - Can I run a windows batch file with hidden cmd window but see the programs started by the batch? 给定一个包含cmd命令的文本文件,我如何使用批处理文件循环并运行它们? - Given a text file containing cmd commands, how can I use a batch file to loop and run them? 确定程序是在 Powershell 还是 CMD 上运行 - Determine whether programs run on Powershell or CMD 我有一个将在cmd中运行但不会作为批处理文件运行的命令 - I have a command that will run in cmd but wont run as batch file 运行cmd命令工作,但从批处理文件运行不 - Run cmd command work but run from batch file doesn't 如何从批处理文件运行 PowerShell 脚本 - How to run a PowerShell script from a batch file CMD批处理文件以运行R代码。 如何选择不同版本的R? - CMD Batch file to run R code. How can I choose different version of R?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM