[英]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-subshell 。 Here is copy of it, just in case.
这是它的副本,以防万一。
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
会话并执行脚本将有效。
cmd.exe
cmd.exe
detect-batch-subshell.cmd
detect-batch-subshell.cmd
> detect-batch-subshell.cmd
Running interactively in cmd.exe session.
powershell.exe
powershell.exe
detect-batch-subshell.cmd
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
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.