Search code examples
windowspowershellbatch-filecmd

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, e.g.

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

Users can run this prior to doing work that needs the environment variable, e.g.:

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.

However if a user happens to have a Powershell prompt open, the environment variable is of course not propagated back to 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.

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? This needs to be done without any 3rd party utility.


Solution

  • 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:

    @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 must 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.

    Note that I've added a check for process name pwsh as well, so as to make the solution work with PowerShell Core too.


    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).

    The solution relies on detecting the presence of a user-specific path in PSModulePath, which PowerShell automatically adds when it starts.

    @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 must 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:

    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.

    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.
    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.
    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.