At my previous job, we all used Windows. At first I insisted on doing everything in Windows Subsystem for Linux (WSL) or Linux-based Docker containers, but eventually I gave in and went native.

I read Learn Powershell in a Month of Lunches so I’d actually understand things instead of cobbling together snippets from search or LLMs.

Then, at a subsequent job, I went back to Linux. I thought I’d feel relieved to be back in my comfort zone of not developing on Windows. But instead, as I went back to the standard Linux shells (bash/zsh), I found myself missing some Powershell features.

These are some features that I miss.

Tab completion for free

I still have not bothered to add tab completion to my shell scripts because of the extra steps involved.

Powershell gives you this for free when you write a “cmdlet” (like a shell function). Here’s an example cmdlet:

function Test-MyCmdlet {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Argument
    )

    Write-Output $Argument
}

Save it to Scripts.ps1, then:

. Scripts.ps1  # Source it
Test-MyCmdlet -Ar  # Now press tab
Test-MyCmdlet -Argument  # Boom

A debugger

Eventually your spicy one-liner in the CLI will grow out of control and you need to turn it into a script. Sometimes, you might need to debug that script.

vscode with the Powershell extension does this with no extra steps.

Add a breakpoint:

and hit run:

Editor support

vscode with the Powershell extension has decent autocomplete and linting.

I haven’t seen a bash/zsh script editor with this kind of autocomplete and I’m not sure how such a thing would work considering that those completions reflect the system they run on, not just the source code. For example:

systemctl is-active Netwo#TAB

might give

systemctl is-active NetworkManager.service

which would only make sense on systems where NetworkManager is installed.

I will say shellcheck is quite good for linting.

A package manager

Install-Module 👌

For example, NTFSSecurity eases the pain of dealing with NTFS permissions:

Install-Module NTFSSecurity -Scope CurrentUser
Get-NTFSOwner .

I do not miss dealing with NTFS permissions on Windows. I do not miss inherited permissions.

Automatic short commands

Instead of

Test-MyCmdlet -Argument

you can do any of

Test-MyCmdlet -A
Test-MyCmdlet -Ar
Test-MyCmdlet -Arg

assuming there is no conflicting argument that would match.

Easy man pages

In Powershell, it’s all baked in to the file, and vscode’s autocomplete helps you write it.

function Test-MyCmdlet {
    <#
    .SYNOPSIS
    This is just a demo cmdlet to show off Powershell. 

    .PARAMETER Argument
    A piece of text to demonstrate a commandline arg, e.g. "hello world". 
    
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Argument
    )

    Write-Output $Argument
}
> Get-Help Test-MyCmdlet -Full 

NAME
    Test-MyCmdlet
    
SYNOPSIS
    This is just a demo cmdlet to show off Powershell.
    
    
SYNTAX
    Test-MyCmdlet [-Argument] <String> [<CommonParameters>]
    
    
DESCRIPTION
    

PARAMETERS
    -Argument <String>
        A piece of text to demonstrate a commandline arg, e.g. "hello world".
        
        Required?                    true
        Position?                    1
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    <CommonParameters>
        This cmdlet supports the common parameters: Verbose, Debug,
        ErrorAction, ErrorVariable, WarningAction, WarningVariable,
        OutBuffer, PipelineVariable, and OutVariable. For more information, see
        about_CommonParameters (https://go.microsoft.com/fwlink/?LinkID=113216). 
    

Command naming consistency

At first this seemed annoying and verbose. Get-ChildItem instead of ls?

But then I noticed I could find the command I wanted faster than asking Google/ChatGPT.

For example, I knew of Get-Process, which is like ps and shows all running processes, but how would I stop a process? Easy, find all commands whose verb operates on the noun Process:

Get-Command -Noun Process

which returns:

Debug-Process
Get-Process
Start-Process
Stop-Process
Wait-Process

Stop-Process it is.

Objects, not plaintext

Everything is an object with named properties, which means you rarely need to use the usual awk, tr, cut, sed, etc to parse plaintext.

For example, to see how much CPU each chrome process is using, then sort by most-to-least, we use the fact that Get-Process objects have ProcessName, CPU properties:

Get-Process | where ProcessName -Match 'chrome'  | sort CPU -Descending

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
     53    46.45     152.30       5.02    9084   1 chrome
     23    15.75      37.97       0.81    7980   1 chrome
     22    33.06     108.33       0.81    9556   1 chrome
     22    30.45      76.10       0.53    8496   1 chrome
     22    21.66      61.98       0.30    6808   1 chrome
     19    13.48      28.31       0.06    8824   1 chrome
     13     7.88      18.79       0.03    9188   1 chrome
     10     6.40       8.62       0.02    9984   1 chrome

I personally find this easier to memorize than the bash alternative of bespoke ps arguments (which aren’t portable):

ps -eo pid,pcpu,pmem,comm --sort=-pcpu | grep '[c]hrome'

or sorting on the column index which breaks if you shuffle the columns around and %cpu isn’t in the 2nd column anymore:

ps -axo pid,%cpu,%mem,command | grep '[c]hrome' | sort -k2 -nr

(Also, note the grep [c]hrome hack to avoid searching for the grep process itself).

This is amazing for pipelines. Because all inputs and outputs are objects, Powershell knows the types of things going through a pipeline before running anything. For example, suppose I want to filter processes by their commandline arguments but I momentarily forgot the property.

Get-Process | Where-Object Com # Command? Commandline? idk, let's press tab

results in:

Get-Process | Where-Object CommandLine
CommandLine  Comments     Company      CompanyName

Ah, right, it’s CommandLine. Now I can finish what I was doing, finding all running processes that Google might be responsible for:

Get-Process | Where-Object CommandLine -Match 'google' 

Type checking

This is a nice sanity check to prevent you from accidentally switching the type of a variable:

> [int]$x = 10
> $x="foo"
MetadataError: Cannot convert value "foo" to type "System.Int32". Error: "The input string 'foo' was not in a correct format."

Things I don’t miss

The Powershell 5.1 and 7 distinction. Windows ships with Powershell 5.1, but the latest features are in Powershell 7 (currently 7.5.1). This means you either need to get everyone on your team to install 7 to share scripts, or you don’t use the newer features in 7.

No universal ‘fail fast’ mode. In bash, exit code 0 is success, anything else is fail. set -e tells your script to fail fast and exit when it sees a nonzero code which is great for cutting down on debugging time. Powershell sort of has this with

$ErrorActionPreference = 'Stop'

but it only works on programs that use Powershell’s exception mechanisms, like Write-Error and not on programs that use exit codes.

The rest of Windows. NTFS permissions, path separators are backslashes, there are too many ways to do things (batch, vbscript, Powershell, control panel GUIs), Windows Docker containers don’t work. I’d add “vendor lock-in” here but we were already stuck in the walled garden and Powershell happened to be in it.

Conclusion

Powershell just seemed more intuitive and consistent than bash or zsh and I found I could do a lot more without asking Google or ChatGPT to remind me how to do things.

There are newer shells like fish which share these design goals. Maybe I should try them.