100 Languages Speedrun: Episode 83: PowerShell
Tags: powershell, windows, unix
Published: 2022-02-09 17:30:24

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   81320 …33 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"](https://en.wikipedia.org/wiki/Unix_philosophy), and then [every command ended up with 100 switches](https://danluu.com/cli-complexity/). 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](https://dev.to/taw/100-languages-speedrun-episode-48-elvish-3k6l) or [Xonsh](https://dev.to/taw/100-languages-speedrun-episode-66-xonsh-5ade), where you can get similar power with more compatibility.

### Code

[All code examples for the series will be in this repository](https://github.com/taw/100-languages-speedrun).

[Code for the PowerShell episode is available here](https://github.com/taw/100-languages-speedrun/tree/master/episode-83-powershell).