Beginners

Enough PowerShell to Be Useful: Six Commands and One Idea

You do not need to learn PowerShell to run PowerShell. Most SharePoint admin scripting is six commands and one idea, and once you have those the rest is looking things up as you go.

This is those six, shown on the kind of data you actually handle: list items with a title, a status, a budget and an owner. Everything below was run on PowerShell 7.

The one idea: the pipeline moves objects, not text

This is the thing that makes PowerShell different from every shell before it, and it is the thing that makes the six commands work.

When you pipe something with |, you are not passing text along. You are passing objects, and each object has properties you can name:

$items | Get-Member -MemberType NoteProperty
Budget
Owner
Status
Title

Get-Member is how you find out what an object has. When a script somewhere returns something and you do not know what you can do with it, pipe it into Get-Member. It is the most useful command for learning that PowerShell has.

1. Where-Object: keep the rows you want

$items | Where-Object { $_.Status -eq 'Complete' }
Project 01
Project 04

$_ is “the current object”. Inside the braces it means the one item Where-Object is looking at right now.

Since PowerShell 3 there is a shorter form for simple checks:

$items | Where-Object Status -eq 'Complete'

Same result, no braces, no $_. Use the short form for one condition and the brace form when you need more than one.

The operators are words, not symbols

This trips up everyone coming from another language:

-eq  -ne  -gt  -lt  -ge  -le      not  ==  !=  >  <

The reason is that > already means "redirect output to a file" in a shell. So PowerShell spells its comparisons out. Type $a > $b expecting a comparison and you will silently create a file called $b.

Two more you will use constantly:

$items | Where-Object { $_.Title -like 'Project*' }    # wildcard
$items | Where-Object { $_.Title -match 'Q[0-9]' }     # regex

-like takes wildcards (* and ?). -match takes a regular expression. Reach for -like first; it is simpler and it is usually enough.

And one that causes real bugs, -eq is case-insensitive by default:

'Complete' -eq 'complete'    # True
'Complete' -ceq 'complete'   # False - the c prefix makes it case-sensitive

Usually the case-insensitive default is what you want. Just know it is the default, so a comparison you expected to fail on case will quietly pass.

2. Select-Object: choose columns, or take the first few

$items | Select-Object Title, Budget -First 3
Title      Budget
-----      ------
Project 01   1000
Project 02   2000
Project 03   3000

Two jobs in one command: pick which properties you want, and -First / -Last to take a slice.

The one worth knowing separately is -ExpandProperty. Plain Select gives you objects with one property. ExpandProperty gives you the raw values:

$items | Select-Object -ExpandProperty Owner -Unique
alice@contoso.com, bob@contoso.com, carol@contoso.com

When you want a plain list of email addresses to loop over rather than a table to look at, that is the one.

3. Sort-Object

$items | Sort-Object Budget -Descending | Select-Object -First 2 Title, Budget
Title      Budget
-----      ------
Project 05   5000
Project 04   4000

Sort, then take the first two, and you have "the two biggest", which is most of what "top N" reporting ever is.

4. Measure-Object: the sums you would otherwise export to Excel for

$items | Measure-Object Budget -Sum -Average -Maximum
Count   : 5
Sum     : 15000
Average : 3000
Maximum : 5000

Count on its own is the everyday one. "How many items match" is just (... | Measure-Object).Count, or on most objects simply .Count.

5. Group-Object: counts per category, in one line

$items | Group-Object Status | Select-Object Count, Name
Count Name
----- ----
    1 Blocked
    2 Complete
    2 In Progress

This is a pivot table in one command. "How many sites per template", "how many items per status", "how many files per owner", all Group-Object. It is the report people are most surprised PowerShell writes for free.

6. ForEach-Object: do a thing to each one

$items | ForEach-Object { "$($_.Title) = $($_.Budget)" }

Same $_ as Where-Object, but instead of keeping or dropping the item you do something with it. This is where the actual work happens, updating each item or building each line of a report.

ForEach-Object is not foreach

There are two, they look similar, and the difference matters:

$items | ForEach-Object { ... }     # a cmdlet, works IN a pipeline, uses $_

foreach ($item in $items) { ... }   # a statement, its own variable, NOT pipeable

ForEach-Object sits in a pipeline. foreach is a language keyword and cannot be piped into. Put a pipe after its closing brace and you get "An empty pipe element is not allowed", which does not obviously point at the mistake.

Use ForEach-Object in a pipeline, foreach when you are already writing a block of script. For large collections foreach is faster, but you will not notice until you are into tens of thousands of items.

One trap that is worth ten minutes now

When a filter returns exactly one item, older PowerShell gave you that single object, not a collection of one, so .Count was $null instead of 1:

$one = $items | Where-Object { $_.Status -eq 'Blocked' }
$one.Count        # 1 in PowerShell 7, was $null in 5.1

PowerShell 7 fixed this, but plenty of tenants still run 5.1, and a script that checks if ($result.Count -gt 0) will silently do the wrong thing there. Wrap anything you are going to count:

$one = @($items | Where-Object { $_.Status -eq 'Blocked' })
$one.Count        # 1 - reliable on every version

The @() forces an array even around a single result. It is the cheapest habit in PowerShell and it saves a specific, maddening bug.

That is the toolkit

Filter with Where-Object, pick with Select-Object, order with Sort-Object, total with Measure-Object, pivot with Group-Object, act with ForEach-Object. Six commands, one pipeline, and Get-Member to tell you what you are holding when you get lost.

Nearly everything in my SharePoint PowerShell posts is those six pointed at real data. Learn them on made-up objects like these, where nothing can break, and the SharePoint version is just the same commands with a live list on the front 🙂