Powershell: Great but Inconsistent
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.