I really like powershell
When I went on Windows full time in 2019 I decided to avoid WSL as much as I could, which meant learning the Windows-native ways of doing things, which meant learning PowerShell. It was a pretty rough start! There are some affordances for people used to bash, but anything more complicated than "copy a file" doesn't translate at all.
Once I got more used to it, though, I found that I really like powershell. It pushes the boundaries of what can be "shell scripted" and what needs a "proper" programming language. I don't think I'll convince any Mac or Linux folk to switch to powershell core,1 but I can try convincing Windows people to stop using bash through WSL. And okay, maybe make a few bash users go "damn I want that trick".
The autocomplete is very good
Everybody who's seen Powershell knows how verbose it is. Instead of cat
, you write Get-Content
. That sucks to type! In practice though you're not typing long strings very often. Part of that is because PS comes with a default cat
alias, but it's also because PS has very, very good autocomplete.
You can do the default thing and write Get
and press tab to cycle through things that start with Get
. But you can also write *Content
and press tab to cycle through things that end with Content
. Or you could do *et-*nt
to get everything that ends with nt
and has et-
in the middle. And instead of pressing tab, you can press ctrl-space to navigate through all possible matches with the keyboard.
> *et-*tent {ctrl-space}
Get-PrinterDriver Set-VpnConnectionTriggerDnsConfiguration
Get-PrinterPort Set-VpnConnectionTriggerTrustedNetwork
Get-PrinterProperty
These also work to a lesser extent with flags, though bash does that too. But if you select a flag with ctrl-space
Powershell will also show you the parameter type, which is pretty nice.
Objects are great for pipes
The big difference between bash and powershell is that bash represents all data as text while powershell represents data with objects. And while text may be the "universal interface" it's a really awkward one as you can't distinguish text that's content vs text that's metadata.
As an example, let's say you wanted to know the last-modified timestamps of every file in a directory. In bash that's easy:
ls --full-time
But now let's say you wanted to programmatically use the timestamps. Then you have a problem! The problem is that ls
also returns all this other information to you, too, so you'd have to parse out the timestamps from the rest of the cruft. And you might try
# WRONG
ls --full-time | cut -d' ' -f6,7
But this doesn't work, because ls
conflates information text with presentation text. If two "fields" have different lengths, ls
will add spaces to the name of the smaller one to make the two line up, which will break cut -d' '
. And in fact you can't solve this at all with ls
, you have to write stat -c %w
. Text streams don't compose.
By contrast, here's how you do it in powershell:2
gci | Select LastWriteTime
gci
(Get-ChildItem) returns objects which store information in fields. No parsing needed. Since the returned LastWriteTimes
are date objects, you can use their date methods to get how long ago each file was modified:
gci | select FullName, {((Get-Date) - $_.LastWriteTime).days}
Oh yeah PowerShell has anonymous functions (blocks) and they're objects too.
$timesince = {((Get-Date) - $_.LastWriteTime).days}
gci | select FullName, $timesince
You can map and filter objects
Select-Object maps over a collection of objects. ?
(Where-Object) filters them and %
(Foreach-Object) runs a block on each object.
gci | ? {$_.Length -gt 100kb} | % {mv -WhatIf $_ ..}
These work on all objects: file objects, alias objects, function objects, etc. Want to know all of the methods you can call on files?
gci -File | Get-Member | Where MemberType -EQ Method
Here's one place this is a big improvement over text streams. Normally you process grep
output by assuming that every line is a different match. But if you get line context with the -C
flag, each line represents an exact match, a surrounding line, or the match separator (--
). This makes piping the grep results to another program more difficult. The powershell equivalent, Select-String, instead returns a set of match objects. So you can look for a string, filter by surrounding strings, and pass the matches along the pipeline without worrying about handling the context data passed too.
select-string foo *.md -Context 2 | ? {$_.Context.DisplayPreContext -notmatch "^#"} | select Filename
Providers abstract out data access
How to find all files in the top-level C: drive that start with s:
gci C:S*
How to find all of the functions that start with s:
gci Function:S*
How to find all environmental variables that start with s:
gci Env:S*
You can do similar things with session variables, aliases, certificates, etc. This works through the magic of providers, which adapt powershell commands to specific classes of objects. cp Function:foo Function:bar
creates a new function with identical behavior to foo
. You can also add new providers, which makes it possible to do things like mount a json file as a drive and modify it like it was a filesystem.
Data types actually make sense
I have never been able to figure out how to use arrays and maps in bash. Here's a post about how hard it is to pass and copy arrays. Powershell does much better:
$arr = @(1, 2, 3, 4)
$hash = @{a=1; b=2}
This makes it easy to send json in web requests:
convertTo-Json @{abc=@("efg","hij")} -Compress
# {"abc":["efg","hij"]}
Function parameters are surprisingly powerful
So powershell makes the obvious improvement over bash: functions can have named parameters instead of everything being $1 $2 etc
.
function cp($from, $to) {
# ...
}
Now that's not quite a production-ready cp: I also want to have a force and dryrun flag. Further, positional arguments and flag arguments shouldn't conflict, so cp a b -Force
is the same as cp a -Force b
and cp -Force a b
. And I want to let people specify from
and to
with flags if they want, not just by argument number.
In bash this work is pushed to the body of the function, I think you're supposed to use getopts
? In Powershell it's part of function definition:
# [May have syntax errors - ed]
function cp {
param (
[Parameter(Mandatory, Position = 0)]
[Alias("From")]
[string] $from,
[Parameter(Mandatory, Position = 1)]
[Alias("To")]
[string] $to,
[switch]$force,
[switch]$dryrun
)
# …
}
Okay yeah that's pretty verbose. But it does exactly what we want: $from
and $to
are positional and named, $force
is false unless we pass the -Force
flag, which makes it true.
The cool thing about this is that just as functions are objects, function parameters are objects, too! You can define the separate autocomplete routine for each parameter, or even add custom autocompletes to existing functions. You can set some parameters as being mandatory if other specific parameters are already present. You can even create "parameter mixins" for functions; powershell provides the SupportsShouldProcess which automatically adds -WhatIf
and -Confirm
to the function's logic. You don't even have to implement them yourself!
Other stuff
Some big things I like but haven't used a whole lot:
- Powershell has a module system! Weirdly enough there's no namespacing system, so everything defined in a module is dumped into the global namespace. Even so, it still means you can add new functionality to the shell with just
Install-Module
, which calls the official package registry. Grabbing something like AWS Bindings or Toast Notifications is easy.
- People have told me that Powershell is a light wrapper around the .NET framework, which I know basically nothing about but you can use .NET types and methods in powershell scripts. For example, you can call
[math]::floor($x)
. I was once able to use[System.Drawing.Bitmap]::FromFile
to embed a ton of PNGs into svgs.
- Powershell has different distinct notions of sessions, runspaces, and hosts. This all makes managing remote computers easier, which is sysadmin stuff I never have to think about. But I'm also convinced there's a way to get sessions to send messages to each other, an absolutely great idea I'm sure won't backfire on me horribly.
And some small things I like:
Foreach-Object
takes-begin
and-end
flags, which run before and after the processing. You can do% -begin {$x = 0} -process {stuff} -end {echo $x}
- Forking a process returns a job object tied to the session. So can easily see if a job is complete, stop/resume/kill it, check to see if it spawned its own child jobs, you can easily spawn ten jobs and stop/receive/kill them without having to save the
$!
anywhere.
- Powershell came out after we started running terminals in separate programs and uses that to good effect.
help foo -ShowWindow
opens the reference in a small window with search and scrollbars. Format-Table-autosize
formats the output in a way that takes your viewport width into account. Out-Gridview does this:
Administrative Stuff
First, TLA+ workshop in May; use the code C0MPUT3RTHINGS
for 15% off!
Second, I've been working on an article about analyzing TLA+ state spaces. I'll have the post up next week or the week after, but I also compiled my work into a Github repo to make it more accessible. You can find it here if you want to check it out.
Third, according to my logs I sent nine emails in March, and that's way too many emails! Which makes me ask the question "is six emails also too many emails???" Buttondown just added a "survey your readers" features and let's try it out. You should only see the question below in the email, not the buttondown website. Please let me know!
Update for the Internets
This was sent as part of an email newsletter; you can subscribe here.
If you're reading this on the web, you can subscribe here. Updates are once a week. My main website is here.
My new book, Logic for Programmers, is now in early access! Get it here.