1. Context

Powershell, at least in its latest 7.4 version, is great due to two primary reasons:

  • Most objects are indeed objects, leading to very little need for parsing command-line interfaces (CLIs) when attempting automation;

  • It functions consistently across Windows, Linux, and macOS.

However, Powershell’s many ways of accomplishing the same tasks often confuse new users. Let’s examine an example using Get-Process.

2. Get-Process

Get-Process retrieves the list of running processes on the machine, but you can also ask it for a specific process:

Get-Process -Id 1

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
      0     0.00      10.05       0.72       1   1 systemd

If you want to retrieve more than one process, use the following command:

Get-Process -Id 1,2,3

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
      0     0.00       0.00       2.19       2   0 kthreadd
      0     0.00       0.00       0.00       3   0 rcu_gp
      0     0.00      10.05       0.72       1   1 systemd

3. Issues Arise

The first issue is that not all cmdlets (Powershell commands) accept a list as input like -Id. Instead, you must iterate over an array using the pipeline with ForEach-Object or directly within the command:

@(1, 2, 3) | ForEach-Object {Get-Process -Id $_}

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
      0     0.00       0.00       2.19       2   0 kthreadd
      0     0.00       0.00       0.00       3   0 rcu_gp
      0     0.00      10.05       0.72       1   1 systemd

or

@(1, 2, 3).ForEach({Get-Process -Id $_})

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
      0     0.00       0.00       2.19       2   0 kthreadd
      0     0.00       0.00       0.00       3   0 rcu_gp
      0     0.00      10.05       0.72       1   1 systemd

While there are 2 (edit: found a third one) ways to iterate on an array, it’s not clear when learning Powershell why they exist.

4. Using the Pipeline

You write a script where you first gather the list of ids, and then you send those in a pipeline.

But since Get-Process accepts a list of ints as -Id, then maybe you can feed it the list of ints directly:

@(1, 2, 3) | Get-Process

Get-Process: The input object cannot be bound to any parameters for the command either because the command does not take pipeline input or the input and its properties do not match any of the parameters that take pipeline input.
Get-Process: The input object cannot be bound to any parameters for the command either because the command does not take pipeline input or the input and its properties do not match any of the parameters that take pipeline input.
Get-Process: The input object cannot be bound to any parameters for the command either because the command does not take pipeline input or the input and its properties do not match any of the parameters that take pipeline input.

Well, ok, maybe it doesn’t know it’s for -Id, so let’s try:

@(1, 2, 3) | Get-Process -Id

Get-Process: Missing an argument for parameter 'Id'.
Specify a parameter of type 'System.Int32[]' and try again.

That doesn’t work either.

The thing is, in powershell, everything is an object, right? You look online, and you realize that the set of parameters expected by a command is itself an object, and it can be provided as a PSObject or PSCustomObject.

Fair enough:

@(1, 2, 3) |
  ForEach-Object {New-Object PSObject -property @{ Id = $_ }} |
  Get-Process

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
      0     0.00      10.05       0.75       1   1 systemd
      0     0.00       0.00       3.17       2   0 kthreadd
      0     0.00       0.00       0.00       3   0 rcu_gp

It works, but this ForEach-Object is complicated, and you could have stuck Get-Process in there directly.

Fiddling around, you find another way:

@(@{Id=1}, @{Id=2}, @{Id=3}) |
  ForEach-Object {[PSCustomObject]$_} |
  Get-Process

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
      0     0.00      10.05       0.75       1   1 systemd
      0     0.00       0.00       3.17       2   0 kthreadd
      0     0.00       0.00       0.00       3   0 rcu_gp

Better, but still not great.

After longer, you eventually reach this:

@([PSCustomObject]@{Id=1}, [PSCustomObject]@{Id=2}, [PSCustomObject]@{Id=3}) |
  Get-Process

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
      0     0.00      10.05       0.75       1   1 systemd
      0     0.00       0.00       3.17       2   0 kthreadd
      0     0.00       0.00       0.00       3   0 rcu_gp

5. Working with JSON

Another topic involves parsing JSON and using Powershell commands to modify it. ConvertFrom-Json seems like a good starting point:

ConvertFrom-Json ./Dev/java_statics/airports.json

ConvertFrom-Json: Conversion from JSON failed with error: Input string '.' is not a valid number. Path '', line 1, position 1.

Turns out, ConvertFrom-Json does not read files at all, but wants its input to a string!

So, you’re forced to Get-Content first, which is a bit annoying, but it works:

Get-Content ./Dev/java_statics/airports.json |
  ConvertFrom-Json |
  Where-Object {$_.continent -eq 'EU'} |
  Select-Object -Last 10

And it’s a bit slow! airports.json is 1GiB, true, but it takes 233 seconds to finish.

While using jq and bash is much faster at 64 seconds:

jq -c 'map(select(.continent = "EU")) | .[]' ./Dev/java_statics/airports.json | tail -n 10

How about dumping some json then? Like dumping info about process id 1.

Get-Process -Id 1 | ConvertTo-Json

WARNING: Resulting JSON is truncated as serialization has exceeded the set depth of 2.

# a json follows

Wait, what? By default, it won’t output a json with depth level higher than 2? And because you can’t always know how deep your json is, you basically have to always run ConvertTo-Json -Depth 10000 and hope it’s enough.

Seeing that makes you wonder if ConvertFrom-Json suffers the same issue, so you look up Get-Help ConvertFrom-Json -Detailed, and you see this:

-Depth <System.Int32>
    Gets or sets the maximum depth the JSON input is allowed to have. The
    default is 1024.

So, both ConvertFrom-Json and ConvertTo-Json have default depth limits, but they aren’t even the same (2 vs 1024).

On top of that, the UX of many commands have awful defaults (just like the json ones) that just trip you up, and some other commands are just too difficult to use (such as Select-Xml and its xmlnamespace handling).

6. Conclusion

Powershell can be more pleasant for quick scripts, thanks to passing objects in the pipeline, and good manuals and auto-completion. All commands follow the Verb-Noun pattern which make commands more discoberable than ls or ps aux or pwd on Linux systems.

However, finding the recommended way to accomplish tasks is often unclear because there are multiple ways to achieve the same result.

Additionally, some commands have awkward defaults or complex usage patterns, making alternative shells like nushell an appealing option.