I have just transitioned from a Linux/Mac work environment, to a Windows one and have now been using PowerShell for the last 4ish month. At first I refused, trying to get along with WSL (which is great, btw, but still just a hassle occasionally). Went back to old trusted cygwin and bash for windows - also didn’t feel great - not as native as I would have liked. So eventually, I cracked and gave PowerShell a go.
This page is what I would have loved for someone to give me 4 month ago.
This is not a “How to write PowerShell” guide, but more of a ‘If you are used to working on the CLI a lot, how can you transition from BASH to PowerShell?’ cheat-sheet. There are enough code examples below to get a good feeling for how to write PowerShell scripts, too, if that’s what you’re after - but you have to go digging through my examples.
Lets start with some basics
You want pwsh (version 7.3.x +), aka Powershell Core.
You also want chocolatey (also have a look at winget).
You then want to choco install
fzf
yq
jq
tbh, force of habit - you don’t actually need jq anymore. In PowerShell, you can deal with json natively - I’ve not yet encountered a situation where I had to go back to jq
vim or nano (who doesn’t love to edit a text file on the cli quickly?)
.bashrc
$HOME/Documents/PowerShell/Microsoft.PowerShell_profile.ps1
There may well be other profiles in the same place, like Microsoft.VSCode_profile.ps1
Luckily, sourcing files is a thing in Powershell, too. So you can simply add this to the other profiles (assuming you want the same shell behavior in your VSCode terminal as in your normal windows terminal):
. $HOME\Documents\PowerShell\Microsoft.PowerShell_profile.ps1
What do I have in my profile?
foreach ($script in Get-ChildItem "$PSScriptRoot/shell_functions") {
. $script
}
Haha! Got ya.. I have a subdirectory with various ps1 files in it.. like k8s.ps1
and prompt.ps1
- the above loop sources them all. Keeps an otherwise overwhelmingly large $profile neat and tidy. I do the same in my .bashrc for the same reason.
aliases
A number of useful aliases exist:
ls
(akadir
, aka Get-ChildItem)cat
(Get-Content)echo
(Write-Output) - although you’re more likely to use Write-Host, I suspect.popd/pushd
(Pop-Location and Push-Location respectively)those surprised me, tbh - in a nice way.
Notice how the nix ones are nice and short, and the powershell ones are.. not? That’s really annoying and something I haven’t gotten over yet.
Get all of them:
Get-Alias
Adding your own:
New-Alias tf terraform -force
-force so you can do this in your profile without it complaining at you that the alias already exists.
Environment
This will solve 90% of your day to day need:
listing it (note the missing $):
ls env:
adding to it:
$env:FOO = "Bar"
There are details here, about global and local scope etc. If and when this becomes an issue, you can google it.
Now for some more specific stuff
base64
If you, like me, do a lot with kubernetes, you probably use base64 (-d)
quite frequently.
In powershell, we say:
[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String(“FooBar”))
Well that’s just stupid.
Put this in your profile instead
function base64 { | |
param ( | |
[Parameter(Mandatory = $false)] | |
[switch]$d, | |
[Parameter(Mandatory = $false)] | |
[string]$value, | |
[Parameter(ValueFromPipeline=$true)] | |
[string[]]$in | |
) | |
$value = $in ? $in : $value | |
if (Test-Path -Path $value -PathType Leaf) { | |
$value = Get-Content $value | |
} | |
switch ($d) { | |
$true { | |
return [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($value)) | |
} | |
Default { | |
return [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($value)) | |
} | |
} | |
} |
and use it like a normal person:
PowerShell> echo "FooBar" | base64
Rm9vQmFy
PowerShell> echo "Rm9vQmFy" | base64 -d
FooBar
PowerShell> cat Foo.txt
Bar
PowerShell> base64 Foo.txt | base64 -d
Bar
PowerShell> base64 FooBar
Rm9vQmFy
And for those wondering .. GNU echo trails a newline (unless used with -n), PowerShell doesn’t. Hence the lack of Cg==
which you may have come used to seeing.
Prompt
Ok, this is not going to be “Oh my zsh” .. but I really got used to having KubePS1 and a git branch as part of my CLI prompt, so here is how you do that in powershell.
Colours
You may have this in your .bashrc already somewhere - notice the difference in the ESC character.
# enable colours | |
$ESC=[char]27 | |
$RED="$ESC[31m" | |
$GREEN="$ESC[32m" | |
$YELLOW="$ESC[33m" | |
$BLUE="$ESC[34m" | |
$MAGENTA="$ESC[35m" | |
$CYAN="$ESC[36m" | |
$END="$ESC[0m" | |
# enable special text formating | |
$BOLD="$ESC[1m" | |
$LOW="$ESC[2m" | |
$UNDERLINE="$ESC[4m" | |
$INVERT="$ESC[7m" | |
$PASSWORD="$ESC[8m" | |
$BLINK="$ESC[5m" | |
$NORMAL="$ESC[0m" |
actual prompt
There is a default prompt function
, which you can overwrite, like so:
(the first and last line are part of the default prompt, the rest I’ve changed)
function prompt { | |
$(if (Test-Path variable:/PSDebugContext) { '[DBG]: ' } else { '' }) + | |
$(Get-KubePS1) + | |
$(Get-GitBranch) + | |
$(Split-Path -Path (Get-Location) -Leaf) + | |
$(if ($NestedPromptLevel -ge 1) { '>>' }) + '> ' | |
} | |
function Get-GitBranch { | |
$branch = git branch --show-current 2>$null | |
switch ($branch) { | |
"main" { $COLOUR = $RED } | |
"master" { $COLOUR = $RED } | |
"develop" { $COLOUR = $GREEN } | |
Default { $COLOUR = $YELLOW } | |
} | |
$prompt = "[" + $COLOUR + $($branch) + $END + "] " | |
return $branch ? $prompt : '' | |
} | |
function Get-KubePS1 { | |
if ($env:KUBEPS1 -eq 1) { | |
if (!$env:KUBECONTEXT -or !$env:KUBENS) { | |
$env:KUBECONTEXT = kubectl config current-context | |
$env:KUBENS = kubectl config view --minify -o jsonpath='{..namespace}' | |
} | |
return '(' + $BLUE + [char]0x2388 + $RED + " $env:KUBECONTEXT" + $END + ':' + $CYAN + "$env:KUBENS" + $END + ') ' | |
} else { | |
return '' | |
} | |
} |
Doesn’t that look pretty:
whoami
I like a good whoami
function whoami {
Write-Host "> User :" (C:/Windows/System32/whoami)
Write-Host "> IP :" (Invoke-WebRequest ifconfig.me/ip)
Write-Host "> Azure:" (az account show | ConvertFrom-Json).Name
}
Invoke-WebRequest is basically curl. I do have curl, and it executes like .. 20ms faster. But I can’t remember if I installed that myself or it comes out of the box now.
Also, notice the “ConvertFrom-Json
” cmdlet - no jq
required.
grep
Muscle memory is strong, what can I say.
New-Alias grep Select-String
Initially I thought I had to get really complicated with this, but Select-String actually works pretty much like grep out of the box, so aliasing it was enough to get me through the day.
Other Kubernetes shenanigans
I have written some stuff on this a while ago here (when I still had short hair, that’s how long ago that was!)
So this is my k8s.ps1 in the aforementioned ./shell_functions
directory.
I hope that most of this is self-explanatory. If you want to compare to bash or get some more details, read the old article linked above.
New-Alias -Name k -Value kubectl -force | |
kubectl completion powershell | Out-Null | |
# turn kubeps1 prompt on or off | |
function kps { | |
if ($env:KUBEPS1 -eq 1) { | |
$env:KUBEPS1 = 0 | |
} else { | |
$env:KUBEPS1 = 1 | |
} | |
} | |
$env:KUBEPS1 = 1 | |
# load all kubeconfigs into KUBECONFIG env var, so I don't have to maintain one big kubeconfig file | |
function Get-kubeconfigs { | |
$kubeConfigDir = "$HOME/.kube/configs" | |
$env:KUBECONFIG = "" | |
foreach ($config in (Get-ChildItem $kubeConfigDir)) { | |
$env:KUBECONFIG += ";$config" | |
} | |
} | |
Get-kubeconfigs | |
# this is my short version of the official ktx | |
# the official ktx doesn't support multiple kubeconfigs | |
# in the KUBECONFIG env var - hence I've created my own | |
function ktx { | |
$context = kubectl config get-contexts -o name | fzf | |
kubectl config use-context $context | |
$env:KUBECONTEXT = kubectl config current-context | |
$env:KUBENS = kubectl config view --minify -o jsonpath='{..namespace}' | |
} | |
# as above, my short, hacky kubens alternative | |
function ksn { | |
param ( | |
[Parameter(Mandatory = $false)] | |
[string]$ns | |
) | |
if (!$ns) { | |
$ns = kubectl get namespace -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}' | fzf | |
} | |
kubectl config set-context --current --namespace=$ns | |
$env:KUBENS = $ns | |
} | |
# helper function | |
# to enable selecting and pre-filtering of resource names | |
function Get-K8sResource { | |
param ( | |
[Parameter(Mandatory = $false)] | |
$preFilter, | |
[Parameter(Mandatory = $true)] | |
$type | |
) | |
if ($preFilter) { | |
return ([string[]](kubectl get $type --no-headers -o custom-columns=:.metadata.name | Select-String $preFilter ) | fzf -1 --header="Select $($type):" ) | |
} else { | |
return ((kubectl get $type --no-headers -o wide | fzf --header="Select $($type):") -split "\s+")[0] | |
} | |
} | |
# helper function | |
# to execute certain commands against a container inside a pod | |
# returns straight away if only one container exists | |
function Get-ContainerName { | |
param ( | |
[Parameter(Mandatory = $true)] | |
[string]$pod | |
) | |
return (kubectl get pod $pod -o jsonpath='{range .spec.containers[*]}{@.name}{"\n"}{end}' | fzf -1 --header="Select container:") | |
} | |
# quick shell into a running pod/container | |
function kssh { | |
param ( | |
[Parameter(Mandatory = $false)] | |
[string]$preFilter | |
) | |
$pod = Get-K8sResource $preFilter -type "pod" | |
$container = Get-ContainerName $pod | |
Write-Host "kubectl exec -it $pod -c $container -- /bin/bash || /bin/sh || /bin/ash/" | |
kubectl exec -it $pod -c $container -- /bin/bash || /bin/sh || /bin/ash/ | |
} | |
# quick logs from a running pod/container | |
function kl { | |
param ( | |
[Parameter(Mandatory = $false)] | |
[string]$preFilter | |
) | |
$pod = Get-K8sResource $preFilter -type "pod" | |
$container = Get-ContainerName $pod | |
Write-Host "$RED>>$YELLOW $pod $RED>$GREEN $container$END" | |
kubectl logs $pod -c $container | |
} | |
# quickly get secrets and their decoded values | |
function ks { | |
param ( | |
[Parameter(Mandatory = $false)] | |
[string]$preFilter, | |
[Parameter(Mandatory = $false)] | |
[switch]$all | |
) | |
$secret = Get-K8sResource $preFilter -type "secret" | |
Write-Host $MAGENTA"> "$secret | |
$secret_obj = (kubectl get secret $secret -o json | ConvertFrom-Json) | |
if ($all) { | |
foreach ($item in $secret_obj.data.PSObject.Properties) { | |
Write-Host $YELLOW">> "$($item.Name)": "$RED$(base64 -d $item.Value)$END | |
} | |
} else { | |
$data = ($secret_obj.data.PSObject.Properties.Name | fzf -1 --header="Select Data to decode:") | |
Write-Host $YELLOW">> "$($data)": "$RED$(base64 -d $secret_obj.data.$data) | |
} | |
} | |
# quickly describe a k8s resource | |
function kd { | |
param ( | |
[Parameter(Mandatory = $false)] | |
[string]$type, | |
[Parameter(Mandatory = $false)] | |
[string]$preFilter | |
) | |
if ($type) { | |
$resourceType = $type | |
} else { | |
$resourceType = (kubectl api-resources -o name | fzf --header="Select resource type:") | |
} | |
$resource = Get-K8sResource -preFilter $preFilter -type $resourceType | |
kubectl describe $resourceType $resource | |
} |
Some more useful things
Azure
function azsub {
$subscription = (Get-AzSubscription).Name | fzf
Set-AzContext -Subscription $subscription
az account set -s $subscription
}
Terraform
New-Alias tf terraform -force
function tfw {
$workspace = terraform workspace list | fzf
$workspace = $workspace -replace '[ *]', ''
Write-Host $GREEN"Switching to workspace `"$workspace`"."$END
terraform workspace select $workspace
}
Git
# git get | |
function gg { | |
git pull | |
} | |
# git branch - start a new feature | |
function New-GitBranch { | |
param ( | |
[Parameter(Mandatory = $true)] | |
[string]$branch | |
) | |
Write-Host $BOLD$MAGENTA"[ Git Branch ]"$END | |
Write-Host $YELLOW"> Pull"$END | |
git pull | |
Write-Host $YELLOW"> Creating new branch"$END | |
git checkout -b $branch | |
Write-Host $YELLOW"> Publishing new branch"$END | |
git push -u origin $branch | |
} | |
# you can obly run this alias once - need to figure out a better way. | |
# New-Alias gb New-GitBranch -force -option 'Constant','AllScope' | |
# git lazy - add all files, commit all files and publish all changes | |
function New-GitCommit { | |
Write-Host $BOLD$MAGENTA"[ Git Lazy ]"$END | |
$branch = Get-GitBranch | |
Write-Host $BLUE"On branch:"$END $branch | |
Write-Host $YELLOW"> Pull"$END | |
git pull | |
Write-Host $YELLOW"> Adding files"$END | |
git status -s | |
git add --all | |
$commitMsg = $args | |
Write-Host $YELLOW"> Commit"$LOW $commitMsg$END | |
git commit --all -m "$commitMsg" | |
Write-Host $YELLOW"> Push"$END | |
git push | |
} | |
# you can obly run this alias once - need to figure out a better way. | |
# New-Alias gl New-GitCommit -force -option 'Constant','AllScope' |