100 Languages Speedrun: Episode 83: PowerShell

PowerShell is Microsoft idea of what shell of the future should look like. It's mostly a Windows thing, but it's possible to run it on OSX, and that's what I'm going to do.

Hello, World!

You can run normal programs in your environment like echo:

> echo "Hello, World!"
Hello, World!

You can also use PowerShell commands like Write-Output. PowerShell uses the kebab convention for its commands:

> Write-Output "Hello, World!"
Hello, World!

Also if you write just a quoted string, it will be printed by default:

> "Hello, World!"
Hello, World!

We can also run PowerShell from Unix scripts:

#!/usr/bin/env pwsh

Write-Output "Hello, World!"
$ ./hello.ps1
Hello, World!

FizzBuzz

There are so many ways to write a FizzBuzz, so let's do it in a weird way:

#!/usr/bin/env pwsh

switch (1..100) {
  {$_ % 15 -eq 0} { "FizzBuzz"; continue }
  {$_ % 5 -eq 0} { "Buzz"; continue }
  {$_ % 3 -eq 0} { "Fizz"; continue }
  default { $_ }
}

Step by step:

  • switch is sort of like the usual switch statements found in many programming languages, but with some twists
  • if we pass a value $_ will be that value inside the switch
  • but if we pass an array to switch, it just loops over it, I don't recall this in any other language
  • it will not just take the first match, it will do all the matches, we need break or continue or such to stop if from printing FizzBuzz Buzz Fizz on every 15th number
  • default only matches if none of the others do, that last one doesn't need continue, I just put it there for clarity

Streams

Most languages separate concepts of values and output (print or such) sends value out of the program.

Unix shells can sort of get such values internally by a pipe foo | bar - things foo writes will be available to bar. But these values get turned into text.

That's not how PowerShell works. It's streams everywhere, and streams can contain any objects, they don't get flattened into text.

#!/usr/bin/env pwsh

function numbers {
  10
  20
  30
}

numbers | ForEach-Object { $_ + 400 }

numbers | Join-String -Separator ","

1..10 | Join-String -Separator ","
$ ./streams.ps1
410
420
430
10,20,30
1,2,3,4,5,6,7,8,9,10
  • pipes in PowerShell don't pass text, they pass objects, so we can pass 1..10
  • we have a bunch of loops, like ForEach-Object that will call a block of code for each element received from previous part of the pipeline
  • commands are case-insensitive, documentation writes them as ForEach-Object, but you can use foreach-object as well
  • since we didn't ask for anything else in numbers, the 10, 20, 30 are just printed to output stream - which would go to screen if we just called numbers, or it gets to the next stage if we | it into it
  • Join-String does the join. Commands in PowerShell are fairly verbose, and follow the Verb-Noun convention.

More About Streams

One of the first questions everyone needs to know about any language is "how do I console.log". And it's a bit more complex in PowerShell, as Write-Output would go to the output stream, not to the console.

#!/usr/bin/env pwsh

function numbers {
  1
  2
  Write-Output 3
  Write-Host 4
  Write-Error 5
  6
}

Write-Output "Loop:"
numbers | ForEach-Object{ Write-Output "Got $_" }

Write-Output "Capturing:"
$x = numbers
Write-Output "We got: [$x]"
$ ./streams2.ps1
Loop:
Got 1
Got 2
Got 3
4
numbers: streams2.ps1:13
Line |
  13 |  numbers | ForEach-Object{ Write-Output "Got $_" }
     |  ~~~~~~~
     | 5

Got 6
Capturing:
4
numbers: streams2.ps1:16
Line |
  16 |  $x = numbers
     |       ~~~~~~~
     | 5

We got: [1 2 3 6]

Step by step:

  • just putting some value normally writes it to the output stream
  • Write-Output writes to output stream
  • Write-Host writes to global STDOUT, so you can use it as console.log
  • Write-Error writes a big error message, but it writes it to global STDOUT not STDERR!
  • | will send all the values to the next pipeline stage
  • assigning function results to value will put all the captured values in it, that's how $x became [1 2 3 6]

Fibonacci

#!/usr/bin/env pwsh

function fib($n) {
  if ($n -le 2) {
    1
  } else {
    $a = fib($n-1)
    $b = fib($n-2)
    $a + $b
  }
}

1..20 | ForEach-Object {
  Write-Output "fib($_)=$(fib($_))"
}
$ ./fib.ps1
fib(1)=1
fib(2)=1
fib(3)=2
fib(4)=3
fib(5)=5
fib(6)=8
fib(7)=13
fib(8)=21
fib(9)=34
fib(10)=55
fib(11)=89
fib(12)=144
fib(13)=233
fib(14)=377
fib(15)=610
fib(16)=987
fib(17)=1597
fib(18)=2584
fib(19)=4181
fib(20)=6765

There are some interesting things going on here:

  • like all modern languages, there's string interpolation - and obviously with a different syntax
  • we need to capture $a = fib($n-1) and $b = fib($n-2), it doesn't really work to do fib($n-1) + fib($n-2), it would just have them write to the same output stream
  • PowerShell uses comparisons like -lt, -eq etc. because characters in shells like =, <, > are so overloaded by other meanings, they just can't also serve as comparison operators; a lot of shells do something similar
  • this looks like a fairly conventional fib($n), but it really isn't, we're writing 1 or $a + $b to output stream, not "returning" anything

Working with Objects

Let's use PowerShell for its intended purpose a bit. Because we switched from working with text to working with objects, we can do a lot of things which are quite awkward with Unix shells. Like this:

> Get-ChildItem | Select-Object -Property Name, Size | Sort-Object Size

Name         Size
----         ----
hello.ps1      50
streams.ps1   170
fizzbuzz.ps1  177
fib.ps1       195
streams2.ps1  242
> Get-Process -Name 'pwsh'

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
      0     0.00      67.81      23.63   8132033 pwsh
  • Get-ChildItem is like ls, except it writes objects to its output stream, not just text
  • Select-Object does a few things, one of them is filtering which properties we want to keep
  • Sort-Object can sort by whatever we ask it to
  • Get-Process is like ps, except it writes objects not text to its output stream

This design, dealing with objects not text, lets PowerShell commands to be far more modular than Unix commands. There's a reason Unix started with "make each program do one thing", and then every command ended up with 100 switches. That reason is all programs forced to talk to each other over text.

If we started all over, we might end up with something more like PowerShell.

JSON

However, there's already a structured data format a lot of Unix commands support - JSON streams. Can PowerShell do JSON? At least the basic kind yes:

> Get-ChildItem | Select-Object -Property Name, Size | Sort-Object Size | ConvertTo-Json
[
  {
    "Name": "hello.ps1",
    "Size": 50
  },
  {
    "Name": "streams.ps1",
    "Size": 170
  },
  {
    "Name": "fizzbuzz.ps1",
    "Size": 177
  },
  {
    "Name": "fib.ps1",
    "Size": 195
  },
  {
    "Name": "streams2.ps1",
    "Size": 242
  }
]

There's no direct support for JSON streaming, no of jq integration, and PowerShell's data streams (structured table based) are not really close to JSON data streams (unstructured objects) in philosophy. So in practice JSON streaming and what PowerShell is doing are very different approaches to solve the same problem of going beyond text only communication between programs.

Should you use PowerShell?

PowerShell is an interesting reimagining of what shell could be, unshackled by all the history.

If you need to administer some Windows machines, and don't want to use WSL or such for it, then PowerShell is a totally reasonable choice.

As for a Unix shell replacement, it lacks a lot of convenient features like high quality tab completion (at least out of the box), doesn't follow Unix conventions like STDERR very well, and has really steep learning curve due to differences from Unix shells and other programming languages. It's very interesting as a concept, but I don't think it really works as implementation.

If you're unhappy with existing Unix shells, I'd recommend first investigating something like Elvish or Xonsh, where you can get similar power with more compatibility.

Code

All code examples for the series will be in this repository.

Code for the PowerShell episode is available here.