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

Popular posts from this blog

Automate Drive Letter Assignment with a DiskPart Script

Mutual Exclusion Techniques for Batch Scripts