Search code examples
pythonpowershellsubprocess

Python subprocess communicating with Powershell and Pipeline Operators


I am looking to find the starttime of all chrome instances on my windows computer. In powershell, I can do this via get-process chrome | Format-Table StartTime.

I want to do this in a python script and use the output of this. My code is below:

import subprocess
call = "powershell get-process chrome | powershell Format-Table ProcessName, StartTime"
process = subprocess.Popen(call, stdout=subprocess.PIPE, stderr=None, shell=True)
outputs = process.communicate()
print(outputs)

The output for this command is [''], even with chrome open.

Observations

If I change call to

call = "powershell get-process chrome"

this outputs the table, as expected. I think the error has to do with the pipeline operator.


Solution

  • Your own helpful answer is the quickest way to solve your problem based on your approach of implicitly calling via cmd.exe, due to shell=True (^-escaping the | protects it from up-front interpretation by cmd.exe)

    As also implied by the linked answer, don't use powershell twice: pass a single command that is a PowerShell pipeline to a single call to powershell.exe, the Windows PowerShell CLI, which implies use of the -Command (-c) parameter.[1]

    While shell=True is convenient in terms of syntax (providing a the entire command line as a single string), it isn't necessary, and causes overhead due to the extra cmd.exe process that must be created - let alone the need for careful escaping, to prevent unwanted up-front interpretation of parts of the command line by cmd.exe, as in the case at hand.

    Therefore, consider calling powershell.exe directly, in which case no escaping of | is needed, but it is then best to pass the command as an array, whose first element is the target executable, and whose subsequent elements are the arguments, specified individually:[2]

    # ...
    # Construct the PowerShell CLI call as an *array*:
    # The target executable, followed by the arguments to pass to it.
    call = "powershell.exe", "-c", "Get-Process chrome | Format-Table ProcessName, StartTime"
    
    # Make the call, but do NOT use `shell=True`
    process = subprocess.Popen(call, stdout=subprocess.PIPE, stderr=None)
    # ...
    

    As an aside:

    • The above applies analogously applies to the higher-level subprocess.run() function, which - except for advanced use cases such as asynchronous execution and providing stdin input dynamically, in response to the running process' output - is preferable to the lower-level subprocess.Popen interface it is based on.

    [1] Note that, by contrast, pwsh, the PowerShell (Core) CLI, now requires use of -Command / -c.

    [2] On Windows, you may still pass a single string that is the entire command line (using only "..." for embedded quoting), but on Unix-like platforms using an array is a must in order to pass arguments. (However, in an argument-less invocation, the executable name or path may be passed as a string rather than as a one-element array on both platforms. Also, with shell=True passing a single string even with embedded arguments works on both platforms, as that string becomes a single argument that is passed to the shell executable.)