If you are used to Unix shells like bash or zsh for many years and suddenly have to work with PowerShell – as I recently did – this may seem very awkward and restricted at first glance. The style of the terminal still reminds to MS-DOS and some familiar UNIX commands seem to be available but only work halfway.
To illustrate this, let’s have a first look at ls and ps.
UNIX ls will list a directory this way:
-rw-r--r-- 1 otigges staff 20923 May 9 08:08 README.md -rw-r--r-- 1 otigges staff 117186 May 9 08:08 build.psm1 drwxr-xr-x 14 otigges staff 448 May 9 08:08 docs drwxr-xr-x 29 otigges staff 928 May 9 08:08 src
PowerShell ls displays:
Directory: /tmp/pwsh Mode LastWriteTime Length Name ---- ------------- ------ ---- d---- 05/09/2020 08:08 docs d---- 05/09/2020 08:08 src ----- 05/09/2020 08:08 117186 build.psm1 ----- 05/09/2020 08:08 20923 README.md
The familiar UNIX ps looks something like:
UID PID PPID C STIME TTY TIME CMD
0 1 0 0 Fri06AM ?? 4:30.08 /sbin/launchd
0 109 1 0 Fri06AM ?? 0:23.14 /usr/sbin/syslogd
0 114 1 0 Fri06AM ?? 0:16.00 /usr/libexec/kextd
0 120 1 0 Fri06AM ?? 2:40.12 /usr/libexec/configd
...
While PowerShell ps yields:
NPM(K) PM(M) WS(M) CPU(s) Id SI ProcessName
------ ----- ----- ------ -- -- -----------
0 0.00 19.95 4.59 2484 1 AMPLibraryAgent
0 0.00 112.71 1,094.23 9316 1 Atom
0 0.00 17.09 19.93 9327 1 Atom Helper
0 0.00 4.50 0.12 5607 1 AudioComponentR
0 0.00 4.19 0.13 380 1 backgroundtaskm
...
The two PowerShell commands provide slightly different information about directories and processes and use another formatting than we are used to. Not a big problem so far. The real challenge arises, when we try to process the textual output of a command, e.g. searching and filtering:
cat app.log | grep -i "error" | cut -d ' ' -f 5
Besides the fact, that there are no “grep” and “cut” commands in PowerShell, this text-oriented processing would be the wrong approach. While in Unix shells everything is text, in PowerShell everything is an object. If we try to work with PowerShell the same way as we work with Unix shells, we make life very difficult for ourselves.
Let’s see, what that “object orientation” means.
Cmd-lets
At the core of PowerShell are so called cmd-lets. They can be implemented as .NET classes or as scripts. For UNIX users they have a rather unfamiliar naming schema of Verb-SomeCamelCaseDescription, e.g. Get-Process, Select-String or Get-AzureADGroupMember.
Get-Process will give us a list of running processes. But this list is not plain text but a list of objects. We can assign this list to a variable and then inspect elements.
$all_processes = Get-Process
Now the list of processes is assigned to the variable $all_processes. We can check the number of processes in the list with $all_processes.length and can access an item in the list by it’s index:
$ps = $all_processes[42]
The properties of this process can be accessed by meaningful names like $ps.Id, $ps.Name , $ps.TotalProcessorTime.
As we have a structured list of process objects we can also conveniently iterate and filter all processes. For example we can output all processes consuming more than 1% CPU:
For($ps in Get-Process)
{
if ($ps.CPU -gt 1.0) {
echo "$($ps.NAME) -> $($ps.CPU)"
}
}
Another syntax to achieve the same with pipes looks like this:
Get-Process | Where-Object CPU -gt 1.0 | \
ForEach-Object { echo "$($_.Name) -> $($_.CPU)" }
With Where-Object we can filter the list of processes and ForEach-Object allows to execute a piece of code for all items in this list, while the current element is bound to the implicit variable $_.
Aliases
As these CamelCase-names of the cmd-lets are long and unwieldy, you will probably use a lot of aliases. Analogous to the UNIX alias command there are Get-Alias and Set-Alias in PowerShell.
In fact, the ps and ls commands that were used in the introduction, are aliases for the cmd-lets Get-Process and Get-ChildItem:
> Get-Alias ps, ls
Alias ps -> Get-Process
Alias ls -> Get-ChildItem
Using builtin aliases we can shorten the above code snippet to:
ps | where CPU -gt 1.0 | foreach { echo "$($_.Name) -> $($_.CPU)" }
Writing PowerShell scripts
This focus on objects with defined types and structures will become a real advantage when writing functions and scripts. Compared to bash and zsh, PowerShell offers a sophisticated declaration of parameters. Therefore, you can place a params-block at the beginning of a script or a function to check parameters and bind them to variables.
param (
[Parameter(Mandatory=$true)][String] $id,
[Parameter(Mandatory=$false)][System.Diagnostics.Process] $ps
)
In this example we expect one mandatory string parameter and an optional second parameter of type Process. PowerShell supports parameter passing either by position or by name. So we could call a script with above parameters in two way:
# by position
some_script.ps1 "some_id" $some_process
# by name
some_script.ps1 -ps $some_process -id "some_id"
Another helpful feature is the possibility to return objects instead of only exit codes like in UNIX scripts.
function getUsersOfGroup() {
param (
[Parameter(Mandatory=$true)][String] $groupId
)
# load from active directory
$group = ...
$users = ...
return $users
}
We can use the result of this function for example in a pipeline:
getUsersOfGroup("developers") | where Status -eq "active" | sort
PowerShell scripts can do a lot more than I could show in this brief introduction. I can really recommend to try it out. Even if you will most probably continue to use bash or zsh you might get some inspiration and a look outside the box.
Conclusion
Once you got familiar with the basic concepts of PowerShell, it offers you a powerful and consistent approach to retrieve, process, and display information.
- Don’t treat PowerShell as it was a UNIX shell
- PowerShell is object-oriented, while classic UNIX commands deal with formatted text
- You can bind objects to variables and pass them through pipelines
- Flexible and yet easy to use parameter declarations for scripts and functions
Want to try out PowerShell on Mac?
> brew cask install powershell ... > pwsh