Every now and then I find myself writing PowerShell
. I do it infrequently enough so that I have the time to forget the syntax and have to spend hours looking for things I've done many times over. This guide contains the common operations I perform in PowerShell
.
- PowerShell 7.2
- Always use a Cmdlet
- Break down long lines
- Splatting
- Arrays
- Commands
- Working with JSON
- Objects
- Filtering
- Require a minimum version of PowerShell
- File operations
- Get stderr from external process
- References
Use PowerShell 7.2 as it is a Long Term Support version. Since PowerShell Core 6, PowerShell is cross-platform (Windows
, macOS
and Linux
) and has better defaults (i.e. Out-File
used to use UTF-16
with the little-endian byte order and now uses UTF8
by default).
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[string]$VpcStackName,
[Parameter(Mandatory=$true)]
[int]$NodeCountMax
)
$ErrorActionPreference = 'Stop'
In this case the Cmdlet
is taking 2
parameters ($VpcStackName
and $NodeCountMax
). You can also create Cmdlets
that don't take any parameters. This approach has the following benefits:
- Support for explicit parameters
- Support Verbose
- Support for ErrorActionPreference
- Support for Get-Help
Parameters can:
- Be strongly-typed
- Be made mandatory
- Be validated against a set
- Have a default value when optional
Let's create the Deploy.ps1
script with the below content:
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)] # This is a mandatory parameter
[ValidateSet('DEV', 'UAT', 'PROD')] # The provided parameter has to match a value from the set
[string]$Environment, # This is a `string` parameter
[Parameter(Mandatory=$true)]
[securestring]$AzureSqlServerAdministratorPassword, # This is a `securestring` parameter
[Parameter()] # This is an optional parameter
[Int16]$SqlDatabaseBackupRetentionDays = 1 # Optional parameters can have a default value when they're not provided by the user
)
The script will throw an error when a parameter does not match the expected set:
> .\Deploy.ps1 -Environment PreProd
Deploy.ps1: Cannot validate argument on parameter 'Environment'. The argument "PreProd" does not belong to the set "DEV,UAT,PROD" specified by the ValidateSet attribute. Supply an argument that is in the set and then try the command again.
When you don't provide a value for a securestring
parameter, PowerShell
will prompt you for the value and obfuscate the characters as you type them. This has the added benefit that the secret will not be recorded in your shell history.
If I create the following PowerShell
script named Trace-Verbose.ps1
:
[CmdletBinding()]
param (
)
Write-Verbose 'Hello from Verbose!'
I can execute it in the following fashion:
> ./Trace-Verbose.ps1
But if I add the the -Verbose
Switch
, I'll get a different output:
> ./Trace-Verbose.ps1 -Verbose
Hello from Verbose!
In most scenario I want scripts to stop at the first encountered error. Setting $ErrorActionPreference
to 'Stop'
does exactly this:
$ErrorActionPreference = 'Stop'
This only impacts the lines below $ErrorActionPreference = 'Stop'
.
Create a script named Get-Help.ps1
:
<#
.SYNOPSIS
A one-liner describing what the script does.
.DESCRIPTION
A longer description. Explain what the script is trying to achieve, call out behaviour that might seem odd at first glance.
.PARAMETER VpcStackName
The name for the CloudFormation stack that deploys the VPC.
.PARAMETER NodeCountMax
The maximum number of nodes (EC2 instances) in your cluster.
.EXAMPLE
./Get-Help.ps1 -VpcStackName nonprod-eks-vpc
.EXAMPLE
./Get-Help.ps1 -VpcStackName nonprod-eks-vpc -NodeCountMax 3
You can include more than one example. Helpful to illustrate optional parameters.
.NOTES
Include things such as pre-requisites. This could be the required PowerShell version, PowerShell modules that need to be installed... Also call out any command the user needs to run before calling the script
- PowerShell 7 (https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-windows?view=powershell-7)
- Azure PowerShell (https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.3)
Before running this script you need to invoke 'Connect-'zAccount` to sign-in and you need to select the subscription you want to operate on.
.LINK
A URI where the user can get more information. In this case it could be:
https://github.com/gabrielweyer/nuggets/blob/main/README.md
#>
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[string]$VpcStackName,
[Parameter()]
[int]$NodeCountMax = 8
)
> Get-Help ./Get-Help.ps1
NAME
C:\t\Get-help.ps1
SYNOPSIS
A one-liner describing what the script does.
SYNTAX
C:\t\Get-Help.ps1 [-VpcStackName] <String> [[-NodeCountMax] <Int32>] [<CommonParameters>]
DESCRIPTION
A longer description. Explain what the script is trying to achieve, call out behaviour that might seem odd at first glance.
RELATED LINKS
A URI where I can get more information. In this case it could be:
https://github.com/gabrielweyer/nuggets/blob/main/README.md
REMARKS
To see the examples, type: "Get-Help C:\t\Get-Help.ps1 -Examples"
For more information, type: "Get-Help C:\t\Get-Help.ps1 -Detailed"
For technical information, type: "Get-Help C:\t\Get-Help.ps1 -Full"
For online help, type: "Get-Help C:\t\Get-Help.ps1 -Online"
You can use the backtick character to split a long line over multiple lines:
Invoke-Executable aws --region $AwsRegion cloudformation create-stack `
--stack-name $VpcStackName `
--role-arn $DeploymentServiceRoleArn `
--template-body file://eks/eks-vpc.yaml
Another way of avoiding long lines is to use splatting. This is more idiomatic than using backticks.
Copy-Item -Path 'test.txt' -Destination 'test2.txt' -WhatIf
# The command above can be rewritten as:
$copyParameters = @{
Path = 'test.txt'
Destination = 'test2.txt'
WhatIf = $true # You need to set the `Switch` parameter to `$true` or `$false`
}
Copy-Item @copyParameters
This example is borrowed from About Splatting in the official PowerShell
documentation.
> @(1, 2, 3)
1
2
3
$a = @()
$a += 'First element in array'
> 'Write me to a file!' | Out-File './output.log'
mkdir tmp | Out-Null
> '{"Greeting": "Hello!"}' | ConvertFrom-Json
Greeting
--------
Hello!
By default PowerShell
serialises only 2
levels deep. The below script:
$model = @{
One = @{
Two = @{
Three = @{
Text = 'Hello'
}
}
}
}
Write-Host "`nWithout -Depth parameter:`n"
ConvertTo-Json $model | Write-Host
Write-Host "`nWith -Depth 3 parameter:`n"
ConvertTo-Json $model -Depth 3 | Write-Host
Will output:
Without -Depth parameter:
WARNING: Resulting JSON is truncated as serialization has exceeded the set depth of 2.
{
"One": {
"Two": {
"Three": "System.Collections.Hashtable"
}
}
}
With -Depth 3 parameter:
{
"One": {
"Two": {
"Three": {
"Text": "Hello"
}
}
}
}
[PSCustomObject]@{
Name = 'Balloon'
Sum = 15
}
> '{"Greeting": "Hello!"}' | ConvertFrom-Json | Select-Object -ExpandProperty Greeting
Hello!
Write-Verbose "Created '$($eksAuthConfigurationMapTemporaryFile.FullName)' to store the EKS authentication configuration map"
$phases = @(
[PSCustomObject]@{
Name = 'Lord'
Sum = 9
}
[PSCustomObject]@{
Name = 'Bucket'
Sum = 3
}
[PSCustomObject]@{
Name = 'Head'
Sum = 6
}
)
$importantPhases = $phases | Where-Object { $_.Sum -gt 5 }
At the top of the script:
#Requires -Version 7.0
Requires also allows to specify modules that should be available on the machine:
#Requires -Modules Az
$eksAuthConfigurationMapTemplate = Get-Content .\eks\eks-auth-configuration-map.yaml -Raw
> 'Write me to a file!' | Out-File './output.log'
Compress a directory that excludes the root directory:
Compress-Archive -Path C:\Api\* -DestinationPath C:\Api\Api.zip
There is not yet a way to do this across platform, you will have to handle Windows
and Unix
differently.
The Unix
command needs to be surrounded by single quotes.
if ($IsWindows) {
$deleteContextOuput = cmd /c kubectl config delete-context $kubectlContext '2>&1'
} else {
$shellCommand = "'{ kubectl config delete-context $kubectlContext; } 2>&1'"
$deleteContextOuput = sh -c $shellCommand
}
if ($LASTEXITCODE -ne 0) {
Write-Warning "Could not delete context '$kubectlContext', error message was: $deleteContextOuput"
}
Based on an answer to the Stack Overflow
question PowerShell: Capture the output from external process that writes to stderr in a variable.
$PSScriptRoot
contains the full path of the executing script's parent directory.
$LASTEXITCODE
contains the exit code of the last native program or PowerShell script that was run. I use it only for native programs.