100 Languages Speedrun: Episode 83: PowerShell
Tomasz Wegrzanowski
Posted on February 9, 2022
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 usualswitch
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
orcontinue
or such to stop if from printingFizzBuzz
Buzz
Fizz
on every 15th number -
default
only matches if none of the others do, that last one doesn't needcontinue
, 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 useforeach-object
as well - since we didn't ask for anything else in
numbers
, the10
,20
,30
are just printed to output stream - which would go to screen if we just callednumbers
, 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 theVerb-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 asconsole.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 dofib($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 writing1
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 likels
, 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 likeps
, 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.
Posted on February 9, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.