PowerShell Crash Course
Tags: devops, powershell, scripting, windows
Published: 2019-03-15 07:13:50

---
title: PowerShell Crash Course
published: true
tags: devops,powershell,scripting,windows
series: pwsh_crash_course
canonical_url: 
---

Let’s be honest, `cmd.exe` sucks. I have fond memories of MS-DOS 6.0/6.22, but after getting over the learning curve of Linux/Unix it’s tough to go back. When PowerShell 1.0 was announced I was excited; finally a “real” CLI for Windows. But I couldn’t really be asked to learn it and kept installing [cygwin](https://www.cygwin.com/) or [msys](http://mingw.org/wiki/msys).

[After discovering](/2018/09/26/new-microsoft.html) [PowerShell Core](https://github.com/PowerShell/PowerShell) is multi-platform, it’s back on my radar because of a few use cases:

- [Windows automation](./reusable-windows-vms-with-vagrant-2h5c)
- CI/CD    
  - Our [JenkinsFile](https://jenkins.io/doc/book/pipeline/) is bloated with inline script (some was moved to C#, but that initiative was unsuccessful)
  - [Some AppVeyor](./a-potpourri-of-net-core-cicd-lite-24ba#appveyor) where `pwsh:` [works on both Linux/Windows](https://www.appveyor.com/docs/getting-started-with-appveyor-for-linux/#running-windows-and-linux-builds-side-by-side)
- Windows OEM    
  - If you customize/automate Windows OS setup as a Windows OEM licensee, device OEM/ODM, or enterprise IT you pretty much have to use PowerShell

This is more in the vain of cheat-sheet/quick reference/cookbook than tutorial. If you’re not comfortable picking up new (scripting) languages, this may be unhelpful.

## Shell Keyboard Shortcuts

Display list of all shortcuts:

```powershell
Get-PSReadlineKeyHandler
```

On macOS/Linux it defaults to `emacs` “edit mode”. If you’ve used emacs (or [bash’s emacs mode](https://www.gnu.org/software/bash/manual/html_node/Command-Line-Editing.html)) you’ll feel right at home. On Windows, the default is `Windows` but you can:

```powershell
Set-PSReadLineOption -EditMode Emacs
```

If you’re new to PowerShell, I highly recommend switching to emacs mode. If for no other reason you’ll also familiarize yourself with Bash- should you ever find yourself at a Linux terminal.

Commands for every day use:

```powershell
# Movement
Ctrl+a # Beginning of line
Ctrl+e # End of line
Ctrl+f # Forward one character
Ctrl+b # Back one character
Alt+f # Forward one word
Alt+b # Back one word

# Editing
Alt+. # Insert last argument of previous command
Ctrl+d # Delete character
Alt+d # Delete word
Ctrl+u # Delete to beginning of line
Ctrl+k # Delete to end of line

# Command History
Ctrl+p # Previous command
Ctrl+n # Next command
Ctrl+o # Execute command and advance to next
Ctrl+r <text> # Search command history for <text>
```

`Alt+.` is one I wish I had learned the first day I installed Linux. Tip: you can press it repeatedly to cycle through the history of last arguments.

Same with `Ctrl+o`. If you need to redo a sequence of commands: `Ctrl+p` back to the start, `Ctrl+o` `Ctrl+o`… throw in `Ctrl+n` if you need to skip one, etc.

If you’re on macOS/OSX and using the default terminal `Alt` is `Esc`. Or, you can use `Option` (recommended) via **Terminal > Preferences** :  
 ![](https://rendered-obsolete.github.io/assets/osx_term_option.png)

## Basic Syntax

Literals and variables:

```powershell
$boolean = $true # or `$false`
$string = "string"
$int = 42
$array = 1, 2, 3
$array2 = @(1, 2, 3)
$array[0] = $null # Remove first item
$hash = @{first = 1
    "second" = 2; third = 3
}
# Add to hashtable
$hash += @{4 = "fourth"}
$hash["fifth"] = 5 # Key has to be quoted here
$hash[4] = $null # Remove value (but not key) from hash

# String with `PATH` environment variable
"$env:PATH ${env:PATH}: safer"
# Multi-line "here string"
@"
"Here-string" with value $env:PATH
"@
# Escape character
"literal `$ or `" within double-quotes"
# Evaluate expression
"Hello $(echo world)"

# Casting locks variable type
[int[]]$ints = "1", "2", "3"
$ints = "string" # Throws exception
# Destructuring
$first, $rest = $ints # first = 1; $rest = 2,3
```

Control-flow:

```powershell
$value = 42
if ($value -eq 0) {
    # Code
} elseif ($value -gt 1) {
} else {
}

$value = "value"
# Match against each string/int/variable/expression case
switch ($value)
{
    "x" { echo "matched string" }
    1 { echo "matched int" }
    $var { echo "matched variable" }
    { $_ -gt 42 }{ echo "matched expression" }
    default { }
}
$collection = 1,2,3,4
# Matched against each element of collection. `$_` is current item. `Break` applies to entire collection
switch ($collection)
{
    1 { echo $_ 1 }
    { $_ -gt 1 } { echo "$_ Greater than 1" }
    3 { echo $_ 3; break }
}
# Output is (NB: there's no 4):
#1 1
#2 Greater than 1
#3 Greater than 1
#3 3

foreach ($val in $collection) {
}

while ($value -gt 0) {
    $value--
}
```

- **Assignment** : `+=` `-=` `*=` `/=` `++` `--` (e.g. `++$int` or `$int++` or `$int += 1`)
- **Equality** : `-eq` `-ne` `-gt` `-ge` `-lt` `-le`
- **Matching** : `-like` `-notlike` (wildcard), `-match` `-notmatch` (regex; `$matches` contains matching strings)
- **Containment** : `-contains` `-notcontains` `-in` `-notin`
- **Type** : `-is` `-isnot`
- **Logic** : `-and` `-or` `-xor` `-not` or `!` (e.g. `$a -and $b` or `-not $a` or `!$a`)
- **Replacement** : `-replace` (replaces a string pattern)
- Other than the last, all return `$true` or `$false`
- All are case-insensitive. For case-sensitive prefix with `c` (e.g. `-clike`)
- If input is collection, output is a collection of matches

- [Comparison operators](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_comparison_operators)

## Essentials

```powershell
# List commands containing "Path"
Get-Command -Name *path*

# Get help for `Get-Command`
Get-Help Get-Command

# List properties/methods of object
Get-Command | Get-Member

cd output/Debug
Set-Location output/Debug

# Current file/module's directory
$PSScriptRoot

ls
dir # also works which is freaky/helpful for migration
Get-ChildItem
# Pattern glob
ls *.jpg
Get-ChildItem *.jpg
# Just files
Get-ChildItem -File
# Just directories
Get-ChildItem -Directory
Get-ChildItem | ForEach-Object { $_.Name }
Get-ChildItem | Where-Object {$_.Length -gt 1024}

md tmp/
New-Item -ItemType Directory -Name tmp/ -Force | Out-Null

# Add to PATH
$env:PATH += ";$(env:USERPROFILE)" # `;` for Windows, `:` for *nix
$env:PATH += [IO.Path]::PathSeparator + $(pwd) # Any platform

# Check environment variable `GITHUB_TOKEN` is set
Test-Path Env:\GITHUB_TOKEN
# Test for file/directory
Test-Path subdir/child -PathType Leaf # `Container` for directory

pushd tmp/
popd
Push-Location tmp/
Pop-Location
cd - # Go back to previous directory

# Write to stdout, redirect stderr to stdout, send stdout to /dev/null
Write-Output "echo" 2>&1 > $null
&{
    Write-Warning "warning"
    Write-Output "stdout"
# Append warnings to tmp.txt, rest to /dev/null
} 3>> ./tmp.txt | Out-Null
# Write to stderr, redirect all, append to file
Write-Warning "oops" *>> ./tmp.txt

# Execute string
$ls = "ls"
& $ls
& $ls -l # with args
# Execute string with args
$ls_l = "ls -l"
Invoke-Expression $ls_l
# Execute file `script.ps1`
& ./script
$file = "./script.ps1"
& $file

# Execute command looking for failure text
$res = Invoke-Expression "& $cmd 2>&1"
if ($LASTEXITCODE -and ($res -match "0x800700C1")) {
    # Do something
}
```

- [Automatic variables](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_automatic_variables)
- [Providers](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_providers) (like `Env:\`)
- [Redirection](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_redirection)
- [Call operator `&`](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_operators#call-operator-)

## Error Handling

Powershell has terminating (i.e. exceptions) and non-terminating errors.

```powershell
# Delete PathToDelete/ folder recursively ignoring all errors
Remove-Item -Force -Recurse -ErrorAction Ignore PathToDelete

# Make terminating error
Write-Error "fail" -ErrorAction Stop
throw "fail"

# Non-terminating errors are terminating
$ErrorActionPreference = "Stop"

# Handle terminating error
try {
    throw "fail"
} catch [System.Management.Automation.RuntimeException] {
    Write-Output "Throw'd: $_"
] catch [Microsoft.PowerShell.Commands.WriteErrorException] {
    Write-Output "Write-Error'd"
} catch {
    # Any error
} finally {
    # Always executes
}

# Handling non-terminating errors
if ($LastExitCode > 0) {
    # Exit code of last program >0, which might mean it failed
}
if ($?) {
    # Last operation succeeded
} else {
    # Last operation failed
}
```

- [Common parameters](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_commonparameters) (like `-ErrorAction`)
- [Preference variables](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_preference_variables) (like `$ErrorActionPreference`)

## Jobs

```powershell
# Start job in background (sleeps for 200 seconds)
$job = Start-Job { param($secs) Start-Sleep $secs } -ArgumentList 200
# Or
$job = Start-Sleep 200 &
# Wait for it with a timeout
Wait-Job $job -Timeout 4

# Jobs run in their own session, use -ArgumentList
$value = "hi"
Start-Job { Write-Output "value=$value" } | Wait-Job | Receive-Job
# Output: value=
Start-Job { Write-Output "value=$args" } -ArgumentList $value | Wait-Job | Receive-Job
# Output: value=hi

# Start a bunch of work in parallel
Get-Job | Remove-Job # Remove existing jobs
$MaxThreads = 2 # Limit concurrency
foreach ($_ in 0..10) {
    # Wait for one of the jobs to finish
    while ($(Get-Job -State Running).count -ge $MaxThreads) {
        Start-Sleep 1
    }
    Start-Job -ScriptBlock { Start-Sleep 2 } # Random work
}
# Wait for them all to finish
while ($(Get-Job -State Running)){
    Start-Sleep 1
}
```

- [Jobs](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_jobs)
- Be careful if you use relative paths: jobs start from `$HOME` on macOS/Linux and `Documents/` on Windows
- Need `Receive-Job` to see stdout/stderr
- Parallel work snippet from [this SO](https://stackoverflow.com/questions/43685522/running-tasks-parallel-in-powershell)

## Parameters and Functions

Command-line arguments to a script are handled as `param()` placed at the top of file.

```powershell
function Hello { echo Hi }
# Call the function
Hello
# Output: Hi

# Function with two named params. First with type and default (both optional)
function HelloWithParams {
    param([string]$name = "<unknown>", $greeting)
    echo "hello $name! $greeting"
}
HelloWithParams 1 2
# Output: hello 1! 2
HelloWithParams -greeting 40
# Output: hello <unknown>! 40

# Function with switch and positional parameters
function Greeting {
    param([switch]$flag)
    echo "hello $flag $args"
}
Greeting more stuff
# Output: hello False more stuff
Greeting -flag more stuff
# Output: hello True more stuff
Greeting -flag:$false more stuff
# Output: hello False more stuff


function PositionalParams {
    param(
        [parameter(Position=0)]
        $greeting,
        [string]$name,
        [parameter(Position=1)]
        $tail
        )
    echo "$greeting $name$tail"
}
PositionalParams hi "!"
# Output: hi !
PositionalParams hi -name jake "!"
# Output: hi jake!
PositionalParams hi "!" -name jake
# Output: hi jake!

function FormalHello {
    param(
        # Params default to optional
        [parameter(Mandatory=$true, HelpMessage="Initial greeting")]
        [string]$greeting,
        # If have multiple values have to use @()
        [string[]]$name = @("Sir", "<unknown>")
        )
    echo "$greeting $name $suffix"
}
FormalHello "Greetings"
# Output: Greetings Sir <unknown>
FormalHello -name sir,jake,3rd -greeting welcome
# Output: welcome sir jake 3rd
```

## Misc

**Visual Studio Code** :

- Use [the extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.PowerShell).
- On Windows, install PowerShell Core and in Visual Studio Code click the “PowerShell Session Menu”:  
 ![](https://rendered-obsolete.github.io/assets/vscode_pwsh_switch.png)

From command pallete that appears pick **Switch to PowerShell Core 6 (x64)**.

**Jenkins** :

- Inline powershell or call a script:    

```groovy
  powershell '''
      & ./script.ps1
      '''
```

- Call script and handle exit code:    

```groovy
  def res = powershell returnStatus: true, script: '''
      & ./script.ps1
      '''
  if (res != 0) {
      currentBuild.result = 'UNSTABLE'
  }
```