我有一个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调用的,因此可以向用户显示警告?这需要在没有任何第三方工具的情况下完成。
答案 0 :(得分:5)
Your own answer is robust,并且由于需要运行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 一起使用。
更快的替代方法-尽管不那么可靠:
下面的解决方案基于以下假设,在默认安装中为 true :
注册表中仅持久定义了一个名为PSModulePath
的 system 环境变量(不是 user-specific 一个)。
该解决方案依赖于检测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
控制台窗口的替代方法:
在以前的方法的基础上,以下变体只需 在检测到它正在从PowerShell运行时在新的cmd.exe
窗口中重新调用批处理文件 >。
这不仅为用户提供了便利,还减轻了上述解决方案产生误报的问题:当从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
调用 来关闭窗口。
答案 1 :(得分:3)
乔·科克(Joe Cocker)曾经说过:“我在朋友的帮助下获得了成功”。
在这种情况下,来自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
答案 2 :(得分:2)
我为Chocolatey的RefreshEnv.cmd脚本Make refreshenv.bat error if powershell.exe is being used做过这样的事情。
出于不相关的原因,我的解决方案没有停止使用,但是可以在此仓库中使用:beatcracker/detect-batch-subshell。这是它的副本,以防万一。
脚本将检测它是否从非交互式会话(cmd.exe /c detect-batch-subshell.cmd
)运行,并显示适当的错误消息。
非交互式外壳包括PowerShell / PowerShell ISE,资源管理器等...基本上任何会尝试通过在单独的cmd.exe
实例中运行脚本来执行脚本的东西。
但是,从PowerShell / PowerShell ISE进入cmd.exe
会话并在那里执行脚本即可。
cmd.exe
detect-batch-subshell.cmd
> detect-batch-subshell.cmd
Running interactively in cmd.exe session.
powershell.exe
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
答案 3 :(得分:1)
像beatcracker的答案一样,我认为最好不要假设可用于启动批处理脚本的外部外壳,例如,通过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用于启动批处理脚本。
如果出于某种原因需要绕过外部启动检测,例如,当使用其他命令手动启动脚本以利用已定义的变量时,可以通过在@
之前添加{ {1}}命令行:
CMD