Retrieve the Current Command Prompt Process Identifier Using PowerShell
Obtaining the PID of the current Command Prompt process is not straightforward. Unlike PowerShell, Batch scripting does not provide an automatic variable for this information. In this post, I detail the subtle complexities involved in retrieving the current PID due to the limitations of Batch scripting.
1. Parent-Child Relationship Between Processes
Although, I will only describe a method that uses PowerShell to retrieve the PID, but it's important to understand that this capability is not unique to PowerShell. In fact, it is inherent to any console application that can easily access its own PID while running.
This is particularly relevant for console applications built with .NET Framework languages such as C#, VB.NET, or JScript.NET, which — like PowerShell — are typically installed by default on Windows systems. By launching one of these applications from the Command Prompt, you create a parent-child relationship between the Command Prompt (the parent process) and the started console application (the child).
An instance of the Win32_Process
WMI/CIM class, accessible from any .NET Framework application — including PowerShell — exposes the ParentProcessId
property. This allows the child process to determine the PID of the parent Command Prompt process, effectively giving you a way to identify the running Command Prompt process from within a script.
2. Identifying the Parent of a PowerShell Process
In a PowerShell session, you can retrieve the process identifier using the automatic variable $PID
. For example, running the following command from a CMD session returns the value of the PowerShell PID:
> powershell $PID
13800
You can also pass this value back to the Command Prompt by using PowerShell's exit
statement, since the PID is a valid integer and can be returned as an exit code. This allows the CMD environment to capture the PowerShell PID through the built-in %errorlevel%
variable:
> powershell exit $PID
> echo %errorlevel%
3628
To retrieve the PID of the parent process of PowerShell (i.e., the Command Prompt that launched it), you can use PowerShell's Get-CimInstance
cmdlet. By filtering for the process whose ID matches $PID
, you can access its ParentProcessId
property. Again, the result can be returned to CMD via exit
and stored in a variable:
> powershell exit (Get-CimInstance Win32_Process -Filter """ProcessId=$PID""").ParentProcessId
> set PID=%errorlevel%&
> echo %PID%
6392
In the example above, 6392
is the PID of the Command Prompt that launched the PowerShell session.
Note that the use of triple double quotes ("""
) in the CMD example is necessary to escape double quotes inside a PowerShell command passed as an argument to powershell.exe
. This escaping is required due to how CMD parses and passes arguments. The equivalent command written directly in a PowerShell script or executed within an interactive PowerShell session is much simpler:
exit (Get-CimInstance Win32_Process -Filter "ProcessId=$PID").ParentProcessId
3. Windows PowerShell vs. PowerShell Core
Up to this point, "PowerShell" has referred specifically to Windows PowerShell, the legacy version that comes preinstalled on Windows. However, PowerShell Core — the modern, cross-platform version — is available as a separate installation. You can achieve the same result of the previous section with PowerShell Core by replacing powershell
with pwsh -Command
. Although not included by default, it offers some advantages in terms of features and usability.
One such advantage is the ability to more easily retrieve the parent process ID. In PowerShell Core, the Get-Process
cmdlet returns process objects that include a direct reference to their parent through the Parent
property. This makes it simpler and more intuitive to access the parent process's PID compared to the WMI-based approach required in Windows PowerShell.
The example from the previous section can be adapted for PowerShell Core as follows:
> pwsh -Command exit (Get-Process -Id $PID).Parent.Id
> set PID=%errorlevel%&
Here, pwsh
is the command-line executable for PowerShell Core. The command exits with the parent process ID of the current PowerShell session, which is then captured in the %errorlevel%
and assigned to the PID
environment variable in CMD.
This more streamlined approach is one of the reasons PowerShell Core is a compelling choice for scripting.
4. Setting the PID Variable in a for /f
Command
In the previous example, we used PowerShell to retrieve the parent process ID of the current session. Here's the command again, without the exit
statement, run directly from CMD:
> powershell (Get-CimInstance Win32_Process -Filter """ProcessId=$PID""").ParentProcessId
6392
However, when executing this same PowerShell command as input to a for /f
loop in the same CMD session, we get a different result:
> for /f %i in ('powershell ^(Get-CimInstance Win32_Process -Filter """ProcessId=$PID"""^).ParentProcessId') do @echo %i
13196
This discrepancy occurs because the command inside for /f
is executed in a new Command Prompt process that is spawned by the main CMD session (PID 6392
). The PowerShell process becomes a child of this intermediate CMD process, and the parent process ID returned is that of the temporary CMD instance (PID 13196
), not the original one.
4.1 The Intermediate CMD Process
To demonstrate this behavior, let's use the tasklist
command to query all running cmd.exe
processes. Assume the main CMD session (PID 6392
) is the only one initially running. Observe the output when tasklist
is run both directly and within a for /f
loop:
> tasklist /fi "imagename eq cmd.exe"
Image Name PID Session Name Session# Mem Usage
========================= ======== ================ =========== ============
cmd.exe 6392 Console 1 5,832 K
Now compare that to:
> for /f "tokens=*" %i in ('tasklist /fi "imagename eq cmd.exe"') do @echo %i
Image Name PID Session Name Session# Mem Usage
========================= ======== ================ =========== ============
cmd.exe 6392 Console 1 5,832 K
cmd.exe 11008 Console 1 4,704 K
The second cmd.exe
process (PID 11008
) is created specifically to execute the input command of the for /f
loop. This makes it a child of the original CMD session, meaning that the PowerShell process becomes a grandchild of the original CMD session when run this way.
4.2 Resolving the Grandparent PID
To retrieve the PID of the original CMD session (the grandparent), you must follow the process hierarchy two levels up. First, get the PowerShell process's parent (the temporary CMD), then use its PID to query its own parent (the main CMD). Here's how that looks:
> for /f %i in ('powershell ^(Get-CimInstance Win32_Process -Filter """ProcessId=$PID"""^).ParentProcessId ^^^| ForEach-Object { ^(Get-CimInstance Win32_Process -Filter """ProcessId=$_"""^).ParentProcessId }') do @echo %i
6392
Or:
> set gps=ForEach-Object { ^^^(Get-CimInstance Win32_Process -Filter """ProcessId=$_"""^^^).ParentProcessId }&
> for /f %i in ('powershell $PID ^^^| %gps% ^^^| %gps%') do @set PID=%i&
> echo %PID%
6392
4.3 Simplifying with a PowerShell Script File
To streamline the logic for retrieving the main Command Prompt PID, you can offload the PowerShell logic into an external script file (Get-CmdPid.ps1
). This approach avoids complicated escaping and makes your batch script more readable.
Get-CmdPid.ps1
filter Get-ParentPid {
(Get-CimInstance Win32_Process -Filter "ProcessId=$_").ParentProcessId
}
$PID | Get-ParentPid | Get-ParentPid
Then execute:
> for /f %i in ('powershell -ExecutionPolicy Bypass -File Get-CmdPid.ps1') do @set PID=%i&
You can omit -ExecutionPolicy Bypass
if your script is signed or your system policy allows it. This is also compatible with PowerShell Core (pwsh
).
4.4 Equivalent PowerShell Core Script File
Below is the equivalent script (Get-CmdPid.ps1
) written specifically for PowerShell Core:
Get-CmdPid.ps1
#Requires -PSEdition Core
filter Get-ParentPid {
(Get-Process -Id $_).Parent.Id
}
$PID | Get-ParentPid | Get-ParentPid
4.5 Motivation
Using the for /f
loop to retrieve the Command Prompt session's PID avoids side effects that can disrupt script logic. Unlike the PowerShell exit
statement — which sets the %errorlevel%
variable based on the exit code — the for /f
method simply captures the output without changing the error state. This makes it a safer choice when %errorlevel%
must reflect the outcome of previous commands.
5. Conclusion
Retrieving the Process ID of the Command Prompt that launched a script might seem straightforward at first, but in the context of Batch scripting, it involves navigating some subtle process hierarchy behaviors. By leveraging PowerShell — and particularly PowerShell Core — we can work around these limitations effectively.
In the Custom Drive Letter Assigner project, I use the PID of the Command Prompt running the script to uniquely log each execution and to differentiate the active script runner from any additional instances. This mechanism ensures that only one instance performs the drive letter assignment at a time — critical for consistency and correctness.
In the assign.bat
, I used a for /f
loop to retrieve both the PID and the parent PID of the script runner in one command. The PowerShell script get_process.ps1
returns the required process identifiers, which are then parsed within the loop. This made it possible to distinguish between the parent CMD process (which parses the arguments) and the current CMD process (which handles the drive assignments). However, the approach using PowerShell's exit
to assign the PID to the %errorlevel%
pseudo-environment variable proves to be not only more elegant but also simpler and more robust.
Comments
Post a Comment