Skip to content

PowerShell Primer for Linux Ops Engineers

You run Linux. You think in bash. But your org has Windows servers, Active Directory, Azure, or Exchange -- and the GUI people are too slow. PowerShell is how you script that world.


Why You Need This

Who made it: PowerShell was created by Jeffrey Snover at Microsoft, first released in 2006 as "Windows PowerShell 1.0." Snover wrote the influential "Monad Manifesto" in 2002, arguing that Windows needed a command-line shell built on .NET objects rather than text streams. The name "Monad" came from Leibniz's philosophy; it was renamed to PowerShell before release. PowerShell Core (cross-platform, open-source) was released in 2016, and runs on Linux and macOS as pwsh.

  • Windows Server administration is PowerShell-native. No ssh + bash equivalent.
  • Azure, M365, and Active Directory tooling is PowerShell-first.
  • pwsh runs on Linux. You can automate Azure from your terminal.
  • Mixed environments are the norm. Refusing to learn PowerShell is a career limiter.

Mental Model: PowerShell vs Bash

Concept Bash PowerShell
Commands lowercase utilities (ls, grep) Verb-Noun cmdlets (Get-ChildItem, Select-String)
Pipeline passes text (bytes) passes .NET objects
Parsing output awk, cut, sed .Property access, Select-Object
Typing everything is a string strong types (int, string, array, hashtable)

Remember: The PowerShell naming convention: every cmdlet is Verb-Noun. Common verbs: Get (read), Set (modify), New (create), Remove (delete), Start/Stop (control), Invoke (execute). If you can guess the verb and noun, you can guess the cmdlet. Get-Command *Service* confirms your guess.

The object pipeline is the single biggest difference. You don't parse text -- you access properties on structured objects.

# Bash: ps aux | grep nginx | awk '{print $2}'
Get-Process nginx | Select-Object -Property Id

Essential Discovery Cmdlets

Get-Command *Service*          # find cmdlets by name pattern
Get-Help Get-Service -Full     # full docs for a cmdlet
Get-Service | Get-Member       # inspect what properties/methods an object has

Get-Member is your | head + man combined. Use it constantly.


Variables

$name = "web01"           # simple variable
$env:PATH                 # environment variable
$_                        # current pipeline object (like awk's $0)
$?                        # last command success (bool, not exit code)
$LASTEXITCODE             # actual exit code from native commands

Pipeline and Filtering

Get-Process | Select-Object Name, CPU, Id                              # select properties
Get-Service | Where-Object { $_.Status -eq "Running" }                 # filter
Get-Process | ForEach-Object { "PID: $($_.Id) Name: $($_.Name)" }     # iterate
Get-Process | Sort-Object CPU -Descending | Select-Object -First 10   # sort + limit

String Handling

"Connecting to $host"                          # double quotes interpolate
'Connecting to $host'                          # single quotes are literal
"CPU count: $($env:NUMBER_OF_PROCESSORS)"      # subexpression inside double quotes

File Operations

Get-Content /etc/hosts                       # cat
Set-Content -Path ./out.txt -Value "data"    # echo > file
Add-Content -Path ./out.txt -Value "more"    # echo >> file
Get-ChildItem -Recurse -Filter *.log         # find . -name '*.log'
Test-Path ./somefile                         # test -f / test -d

Process and Service Management

Get-Process -Name nginx                      # ps aux | grep nginx
Stop-Process -Name nginx -Force              # kill -9 $(pgrep nginx)
Get-Service -Name sshd                       # systemctl status sshd
Restart-Service -Name W3SVC                  # systemctl restart apache2

Remote Execution

Invoke-Command -ComputerName web01 -ScriptBlock { Get-Service W3SVC }   # ssh host 'cmd'
Enter-PSSession -ComputerName web01                                      # ssh
Invoke-Command -ComputerName web01,web02,web03 -ScriptBlock { hostname } # fan out

PSRemoting uses WinRM (port 5985/5986). Enable on target: Enable-PSRemoting -Force.

Gotcha: WinRM uses port 5985 (HTTP) and 5986 (HTTPS). In production, always use HTTPS (5986) with proper certificates. The HTTP transport sends credentials in a way that is vulnerable to replay attacks on untrusted networks. Also, PSRemoting defaults to a 5-connection limit per user per machine — fan-out to 50 servers from a single session requires raising MaxShellsPerUser via winrm set.


PowerShell on Linux (pwsh)

sudo apt-get install -y powershell    # Ubuntu/Debian
sudo yum install -y powershell        # RHEL/CentOS
pwsh                                   # launch

Cross-platform modules work. Windows-specific cmdlets (Get-Service, Get-EventLog) do not. Azure, REST, and file cmdlets work fine on Linux.


REST APIs and JSON

# GET request (like curl -s | jq)
$resp = Invoke-RestMethod -Uri "https://api.github.com/repos/torvalds/linux"
$resp.stargazers_count

# POST request
$body = @{ name = "new-repo" } | ConvertTo-Json
Invoke-RestMethod -Uri "https://api.example.com" -Method Post -Body $body `
    -ContentType "application/json"

# JSON parsing (like jq .)
$data = '{"name":"web01","cpu":4}' | ConvertFrom-Json
$data.name

# Emit JSON
@{ name = "web01"; cpu = 4 } | ConvertTo-Json

Active Directory Basics

Requires the ActiveDirectory module (RSAT on Windows, or run from a DC).

Get-ADUser -Identity jdoe -Properties *               # look up a user
Get-ADUser -Filter {Enabled -eq $false}                # find disabled accounts
Get-ADGroup -Identity "DevOps" | Get-ADGroupMember      # group membership
Get-ADComputer -Filter * -SearchBase "OU=Servers,DC=corp,DC=com"
Unlock-ADAccount -Identity jdoe                         # unlock locked account

Azure PowerShell

Install-Module Az -Scope CurrentUser       # install (once)
Connect-AzAccount                           # login

Get-AzResourceGroup                         # list resource groups
Get-AzVM | Select-Object Name, ResourceGroupName, Location
Get-AzStorageAccount | Select-Object StorageAccountName, Location

Works from pwsh on Linux. This is why you install PowerShell on your jumpbox.


Scripting Basics

function Get-DiskReport {
    param(
        [string[]]$ComputerName = @("localhost"),
        [int]$ThresholdPercent = 90
    )
    foreach ($computer in $ComputerName) {
        Invoke-Command -ComputerName $computer -ScriptBlock {
            Get-PSDrive -PSProvider FileSystem |
                Where-Object { ($_.Used / ($_.Used + $_.Free)) * 100 -gt $using:ThresholdPercent }
        }
    }
}

try {
    Stop-Service -Name "CriticalApp" -ErrorAction Stop
} catch {
    Write-Error "Failed to stop service: $_"
}

-ErrorAction Stop converts non-terminating errors into catchable exceptions. Without it, try/catch won't catch most cmdlet failures.

Default trap: PowerShell has two kinds of errors: terminating (thrown exceptions) and non-terminating (written to the error stream but execution continues). Most cmdlet failures are non-terminating — meaning try/catch silently ignores them. Always use -ErrorAction Stop on cmdlets inside try blocks. This is the #1 PowerShell scripting bug for bash users who expect errors to stop execution.

One-liner: Quick Azure VM inventory from Linux: pwsh -c 'Connect-AzAccount; Get-AzVM | Select Name, ResourceGroupName, Location | Format-Table' — run this from your jumpbox to get a fleet summary without touching the Azure portal.