﻿# PowerShell: objects, cmdlets and scripts

> PowerShell from the first script to file processing and pipelines: variables and types, comparisons, if and switch, loops, parameters, functions, arrays and hashtables. The idea that sets it apart gets the most room – a command returns objects with named properties, not text – and a cheat sheet maps everything to its bash form.

Source: https://laszlonemes.com/university/computer-systems/powershell-basics · Author: László Nemes

Session 8 · Computer Systems

PowerShell from the first script to file processing and pipelines: variables and types, comparisons, if and switch, loops, parameters, functions, arrays and hashtables. The idea that sets it apart gets the most room – a command returns objects with named properties, not text – and a cheat sheet maps everything to its bash form.

27 min read `Windows PowerShell 5.1 vs PowerShell 7` `Objects, properties, Get-Member` `Cmdlets and aliases` `param(), functions and their output` `Get-Content and Set-Content` `Where-Object, ForEach-Object, Group-Object` `bash → PowerShell cheat sheet`

**PowerShell** is a shell and a scripting language. You type commands into it, or you put them into a file and run the file – the same two uses a Unix shell has. It comes with Windows, and its current version runs on Linux and macOS too.

If you have written bash scripts, most of the ideas here are old friends: variables, conditions, loops, functions, pipes. They are spelled differently, and one thing is truly new. A bash command prints text, and the next command picks that text apart. A PowerShell command returns **objects**, and the next command asks them for what it needs by name. This session covers the language from the first script to file processing and pipelines.

After this session you can

- tell Windows PowerShell 5.1 from PowerShell 7, start `pwsh` and run a `.ps1` script
- explain what it means that a command returns objects, and read a property instead of counting columns
- write variables, conditions, `switch`, loops and functions, and know the traps of each
- declare script parameters with `param()`, with names, types and default values
- read a file with `Get-Content` and write one with `Set-Content`
- build a pipeline from `Where-Object`, `ForEach-Object`, `Sort-Object` and `Group-Object`

> **The one thing to remember:** PowerShell is a language of *objects*. A command returns values that have named properties; a pipe hands those values on unchanged; a filter, a sort or a comparison asks for a property by name. Text only appears at the very end, when something is shown on the screen.

Downloads

**Example scripts**

One commented script per topic, in the order of the sections below, and the two data files the last two read. Each script also notes the bash form of what it shows. Read them next to the text, run them, change them – inside PowerShell, from the folder you saved them to: `./1_first_script.ps1`, `./8_read_file.ps1 numbers_mixed.txt`.

[1_first_script.ps1](https://laszlonemes.com/files/computer-systems/powershell-basics/1_first_script.ps1) · [2_script_with_parameters.ps1](https://laszlonemes.com/files/computer-systems/powershell-basics/2_script_with_parameters.ps1) · [3_if_statement.ps1](https://laszlonemes.com/files/computer-systems/powershell-basics/3_if_statement.ps1) · [4_strings.ps1](https://laszlonemes.com/files/computer-systems/powershell-basics/4_strings.ps1) · [5_loops.ps1](https://laszlonemes.com/files/computer-systems/powershell-basics/5_loops.ps1) · [6_functions.ps1](https://laszlonemes.com/files/computer-systems/powershell-basics/6_functions.ps1) · [7_arrays_hashtables.ps1](https://laszlonemes.com/files/computer-systems/powershell-basics/7_arrays_hashtables.ps1) · [8_read_file.ps1](https://laszlonemes.com/files/computer-systems/powershell-basics/8_read_file.ps1) · [9_pipeline.ps1](https://laszlonemes.com/files/computer-systems/powershell-basics/9_pipeline.ps1) · [numbers_mixed.txt](https://laszlonemes.com/files/computer-systems/powershell-basics/numbers_mixed.txt) · [patients.txt](https://laszlonemes.com/files/computer-systems/powershell-basics/patients.txt)

Or all at once, from the terminal – the files land in the current directory:

Linux, macOS, the course server

```shell
curl -O "https://laszlonemes.com/files/computer-systems/powershell-basics/{1_first_script.ps1,2_script_with_parameters.ps1,3_if_statement.ps1,4_strings.ps1,5_loops.ps1,6_functions.ps1,7_arrays_hashtables.ps1,8_read_file.ps1,9_pipeline.ps1,numbers_mixed.txt,patients.txt}"
```

Windows (PowerShell, cmd)

```powershell
curl.exe -O "https://laszlonemes.com/files/computer-systems/powershell-basics/{1_first_script.ps1,2_script_with_parameters.ps1,3_if_statement.ps1,4_strings.ps1,5_loops.ps1,6_functions.ps1,7_arrays_hashtables.ps1,8_read_file.ps1,9_pipeline.ps1,numbers_mixed.txt,patients.txt}"
```

Look at the data files freely, but don't save them from an editor: their line endings and last bytes are part of the exercise, and an editor may "fix" them.

## Two programs called PowerShell

There are two programs with this name, and it matters which one you start (Fig. 1). **Windows PowerShell 5.1** is part of Windows and no longer gets new features. **PowerShell 7** is the one being developed: it is open source, runs on Windows, Linux and macOS, and reads and writes files as UTF-8 by default.

> **Fig. 1** · Two programs, one name. They live side by side on Windows – installing version 7 doesn't replace 5.1.
>
> *Diagram:* Two different programs are called PowerShell. Windows PowerShell 5.1, started as powershell, exists only on Windows, ships with it and gets no new features. PowerShell 7, started as pwsh, runs on Windows, Linux and macOS, has to be installed separately and is the one being developed. This course uses PowerShell 7.

|  | Windows PowerShell | PowerShell |
| --- | --- | --- |
| Version | 5.1 – the last one | 7.x |
| Started with | `powershell` | `pwsh` |
| Runs on | Windows only | Windows, Linux, macOS |
| Installation | part of Windows | separate – on Windows too |
| Icon and window title | blue, "Windows PowerShell" | black, "PowerShell 7" |
| `$PSEdition` | `Desktop` | `Core` |

**Use PowerShell 7 – `pwsh` – on every system, Windows included.** A script written for it behaves the same everywhere. Everything on these pages is written for version 7; the example scripts also run in 5.1, but a few things behave differently there (see encoding).

installing PowerShell 7

```shell
winget install --id Microsoft.PowerShell --source winget    # Windows
brew install powershell/tap/powershell                      # macOS
sudo snap install powershell --classic                      # Linux (Ubuntu)
```

The [official install guide](https://learn.microsoft.com/powershell/scripting/install/installing-powershell) covers every other system. On Linux and macOS, PowerShell is just another program: you enter it from your usual terminal with `pwsh` and leave it with `exit`.

```powershell
pwsh                            # enter PowerShell (the prompt starts with PS)

$PSVersionTable.PSVersion       # the version: 7.x
$PSEdition                      # Core = PowerShell 7,  Desktop = Windows PowerShell 5.1
$IsWindows; $IsLinux; $IsMacOS  # which system is this? (these exist only in version 7)

exit                            # back to the shell you came from
```

Common mistake

**On Windows, "PowerShell" in the Start menu is the old one**

The first hit for "PowerShell" is the blue Windows PowerShell 5.1. Look for `pwsh` or the entry called "PowerShell 7". If `$PSEdition` prints `Desktop`, you are in the wrong one.

## Your first script

A PowerShell script is a text file whose name ends in `.ps1`. This is [1_first_script.ps1](https://laszlonemes.com/files/computer-systems/powershell-basics/1_first_script.ps1), without its comments:

1_first_script.ps1

```powershell
Write-Output "Hello PowerShell!"

$name = "Anna"                      # the $ is there in the assignment too; spaces around = are fine
$age = 21

Write-Output "Name: $name, age: $age"     # Name: Anna, age: 21
Write-Output 'Name: $name, age: $age'     # Name: $name, age: $age

$nextYear = $age + 1                      # a number is a number: arithmetic needs no special syntax
Write-Output "In ten years: $($age + 10)" # $( ) puts any expression into a string
Write-Output "Uppercase: $($name.ToUpper()), length: $($name.Length)"
```

- **`Write-Output`** – Prints its argument to the output. `echo` is another name for the same command.
- **`$name = "Anna"`** – A variable. The `$` is part of its name everywhere – in the assignment too – and spaces around `=` are fine.
- **`"…"`** – Double quotes: variables inside are replaced by their value.
- **`'…'`** – Single quotes: everything stays as written.
- **`$( … )`** – Inside a string: evaluate this expression and insert the result.
- **`# …`** – A comment. Several lines at once go between `<#` and `#>`.

To run a script, give its path. For a script in the current directory that means `./` in front – PowerShell doesn't look for commands in the current directory by itself.

```powershell
./1_first_script.ps1            # inside PowerShell
pwsh ./1_first_script.ps1       # from bash or zsh: hand the file to the pwsh program
```

- The extension `.ps1` is required. Execute permission is not – inside PowerShell every `.ps1` can be run.
- No first line is needed. The examples still begin with `#!/usr/bin/env pwsh`: on Linux and macOS this shebang lets you start them straight from bash after `chmod +x`. For PowerShell it is only a comment.
- Write paths with `/`. PowerShell's own commands accept `\` too, but external programs on Linux and macOS don't.
- Commands, variables and operators ignore upper and lower case: `write-output`, `$NAME` and `-EQ` all work.

Common mistake

**Commands ignore case – file names on Linux don't**

`Get-Content Numbers.txt` finds `numbers.txt` on Windows and fails on Linux, where the file system is case-sensitive. Write file names exactly as they are.

### The execution policy (Windows only)

Windows PowerShell 5.1 **refuses to run scripts by default**: `./script.ps1` answers *running scripts is disabled on this system*. This is the execution policy, a guard against starting a downloaded script by accident. PowerShell 7 on Windows normally installs with a policy that lets your own scripts run. If you do meet the error, change the policy for your own user – in the PowerShell you actually use, because the two keep separate settings:

```powershell
Get-ExecutionPolicy                                     # Windows PowerShell 5.1 default: Restricted
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser     # your own scripts may run
```

| Value | Meaning |
| --- | --- |
| `Restricted` | no script may run, only typed commands |
| `RemoteSigned` | scripts written on this machine run; downloaded ones only if signed (recommended) |
| `Unrestricted` | everything runs, with a warning for downloaded scripts |

A script you downloaded – the examples above, say – is unblocked with `Unblock-File -Path script.ps1`, after you have read what it does. Linux and macOS have no execution policy.

## Commands return objects

An **object** is a value that carries named pieces of data – its **properties** – and things it can do – its **methods**. Both are reached with a dot. `Get-ChildItem`, the command that lists a directory, doesn't produce lines of text: it returns one object per file, and each of them knows its `Name`, its size (`Length`) and the time it was last changed (`LastWriteTime`).

> **Fig. 2** · Same question, two kinds of pipe. In bash you describe where the value sits in the text; in PowerShell you name it.
>
> *Diagram:* In bash a pipe carries text: ls -l prints a line of characters, and the next command has to know that the size is the fifth column. In PowerShell a pipe carries objects: Get-ChildItem returns a file object with named properties such as Name, Length and LastWriteTime, and the next command asks for a property by name.

That is the whole difference from a Unix shell (Fig. 2). There, "the size of a file" is the fifth column of a line that `ls -l` printed, and a file name with a space in it shifts the columns. Here it is a property with a name, and the next command can use it directly:

```powershell
Get-ChildItem | Select-Object Name, Length
Get-ChildItem | Where-Object Length -gt 1000 | Sort-Object Length -Descending
```

Every value is an object – a string and a number as well:

```powershell
$name = "Anna"
$name.Length            # 4     – a property
$name.ToUpper()         # ANNA  – a method

$file = Get-Item ./numbers_mixed.txt
$file.Name              # numbers_mixed.txt
$file.Length            # 34 – the size in bytes
$file.LastWriteTime     # the time of the last change – a date object, not text
```

You don't have to remember what an object offers. Pipe it into `Get-Member` and it lists its properties and methods:

```powershell
"some text" | Get-Member                          # every property and method of a string
Get-ChildItem | Get-Member -MemberType Property    # only the properties of a file object
```

### Cmdlets: Verb-Noun

PowerShell's built-in commands are called **cmdlets** ("command-lets"). Every name is a verb, a dash and a noun – `Get-Content`, `Set-Content`, `Get-ChildItem`, `Copy-Item`, `Remove-Item`, `Test-Path` – so a name you haven't seen yet can often be guessed. Options are whole words with one dash: `Get-ChildItem -Recurse -Force`. And the shell documents itself:

```powershell
Get-Help Get-ChildItem -Examples      # how a command is used, with examples (-Online opens the browser)
Get-Command *Content*                 # every command with "Content" in its name
Get-Command -Verb Get -Noun *Item*    # search by verb and by noun
Get-Alias dir                         # which cmdlet is behind an alias
```

Good to know

**Tab completes everything**

`Tab` completes command names, options and file names: type `Get-Ch`, press `Tab`, then `-` and `Tab` again to step through the options.

## Aliases, and where they stop working

Long names are easy to read and slow to type, so many cmdlets have a short second name, an **alias**: `gci` and `dir` for `Get-ChildItem`, `gc` for `Get-Content`, `echo` for `Write-Output`, `cd` for `Set-Location`. Use them freely at the prompt. **In a script, write the full name**: it is easier to read, and it means the same on every system – which is not true of every alias.

> **Fig. 3** · The same line gives two different answers, because ls is two different commands.
>
> *Diagram:* The command ls | Where-Object Length -gt 10 means two different things. On Windows ls is an alias of Get-ChildItem and returns file objects, so Length is the size in bytes. On Linux and macOS the same name starts the real ls program, which prints text, so Length is the number of characters in each name. Full cmdlet names behave the same everywhere.

Common mistake

**The aliases named after Unix commands exist only on Windows**

On Windows, `ls`, `cat`, `cp`, `mv`, `rm`, `sort` and a few more are aliases of cmdlets. On Linux and macOS those names start the **real Unix programs**, which print text and take different options (Fig. 3). The cmdlet names and the aliases that aren't borrowed from Unix – `dir`, `gci`, `gc`, `echo`, `select`, `where` – behave the same everywhere. The cheat sheet below lists which is which.

Good to know

**curl and curl.exe**

In Windows PowerShell 5.1, `curl` is an alias of the cmdlet `Invoke-WebRequest`, which takes entirely different options than the real curl. Writing `curl.exe` calls the real program. That is why the Windows download commands on these pages are spelled that way.

## Variables, types and quoting

A variable holds an object, and so it has a **type**: text, a whole number, a decimal number, true/false, an array, a file. PowerShell picks the type from the value. You can also state it; the value is then converted, or the assignment fails:

```powershell
[int]$count = "42"          # the text becomes a number
[int]$count = "abc"         # ERROR: Cannot convert value "abc" to type "System.Int32"
$number = [int]"42"         # a conversion inside an expression
$number.GetType().Name      # Int32
```

Because a number really is a number, arithmetic needs no special syntax. One thing to know: division keeps the fraction.

```powershell
7 + 2 * 3                   # 13
7 / 2                       # 3.5 – division doesn't drop the fraction
[math]::Truncate(7 / 2)     # 3   – this does
[int](7 / 2)                # 4   – a conversion rounds
7 % 2                       # 1   – the remainder
```

- **`$null`** – The value "nothing" – what an unset variable or a missing hashtable key gives.
- **`$true`, `$false`** – The two logical values. Comparisons and yes/no functions return them.
- **`$env:PATH`** – An environment variable. `$HOME` exists as an ordinary variable too.
- **`` `n ``, `` `t ``, `` `$ ``** – Special characters in a double-quoted string. The escape character is the **backtick**, not the backslash.
- **`${name}`** – The variable name in braces, for when a letter or a colon follows it directly.

### Putting values into a string

Inside double quotes, `$name` is replaced by the value of the variable. That replacement covers the variable only – not a property after it (Fig. 4).

> **Fig. 4** · A property inside a string needs `$( )`. Without it you get the object, then the leftover text.
>
> *Diagram:* Inside a double-quoted string PowerShell replaces only the variable itself. In Size: $file.Length the whole $file object is inserted and .Length stays plain text, so the path is printed followed by .Length. Wrapped as $($file.Length), the expression is evaluated first and the string contains 34.

Common mistake

**A property in a string needs $( )**

`"Name: $file.Name"` prints the **whole** `$file` object and then the text `.Name`. Write `"Name: $($file.Name)"`. And when a colon or a letter follows the variable name directly, put the name in braces: `"${key}: $value"`, `"${fruit}s"`.

Good to know

**3,5 or 3.5?**

With Hungarian regional settings, `7 / 2` typed at the prompt is shown as `3,5`, with a decimal comma. Inside a string (`"$(7 / 2)"`) and when converting from text (`[double]"3.5"`) the decimal sign is always a point, so a script prints the same on every machine.

## Working with strings

A string is an object with methods of its own, and PowerShell adds a set of text operators on top. Between them they cover what takes `tr`, `cut` and `sed` elsewhere. The script [4_strings.ps1](https://laszlonemes.com/files/computer-systems/powershell-basics/4_strings.ps1) runs through all of these:

```powershell
$word = "PowerShell"
$word.Length                    # 10            the number of characters
$word.Substring(0, 5)           # Power         five characters from position 0
$word[0]; $word[-1]             # P  l          one character; a negative index counts from the end
"  Hello World  ".Trim()        # Hello World   spaces and tabs cut from both ends
$word.ToUpper()                 # POWERSHELL    .ToLower() is its pair
$word -replace 'Shell', 'Point' # PowerPoint    every occurrence is replaced

$name, $visits, $date = "Kiss Lajos, 12, 2023-11-13" -split ','
$name.Trim()                    # Kiss Lajos    each field in its own variable

"one", "two", "three" -join '-' # one-two-three
```

Two operators test what a string looks like, and they are easy to mix up:

| Operator | Pattern language | Example |
| --- | --- | --- |
| `-like` | wildcards: `*` any text, `?` one character. The whole string must fit. | `$name -like '*.txt'` |
| `-match` | a regular expression. Matches anywhere unless anchored with `^` and `$`. | `$line -match '^[0-9]+$'` |
| `-notlike`, `-notmatch` | the negated forms | `$line -notmatch '^#'` |
| `-replace` | a regular expression and a replacement; every match is replaced | `$s -replace '[0-9]+', ''` |
| `-split` | a regular expression that marks where to cut; the result is an array | `$line -split ','` |

After a successful `-match`, the automatic variable `$Matches` holds what matched. The parts in parentheses are available by number:

```powershell
if ("2023-11-13" -match '^([0-9]{4})-([0-9]{2})-([0-9]{2})$') {
    Write-Output "Year: $($Matches[1]), month: $($Matches[2])"    # Year: 2023, month: 11
}
```

### Regular expressions here

- The syntax is the familiar one: `^`, `$`, `[0-9]`, `+`, `*`, `?`, `{2}`, `( | )`, `\.`.
- Write the pattern **in quotes**, preferably single ones, so nothing in it is replaced.
- Matching **ignores case**. The case-sensitive forms have a `c` in front: `-cmatch`, `-clike`, `-ceq`.
- The shorthand classes are `\d` (a digit), `\s` (whitespace) and `\p{L}` (a letter, accented ones included).

## Comparing and deciding

Comparison operators are words with a dash in front, and the same ones work for numbers, text and every other type. They return `$true` or `$false`:

```powershell
5 -gt 3                       # True
"apple" -eq "APPLE"           # True  – comparing text ignores case
"apple" -ceq "APPLE"          # False – the c prefix makes it case-sensitive
"report.txt" -like '*.txt'    # True  – a wildcard pattern
"2026" -match '^[0-9]+$'      # True  – a regular expression
3 -in 1, 2, 3                 # True
(5 -gt 3) -and (2 -gt 3)      # False
-not (Test-Path ./nope)       # True
```

| Operators | Meaning |
| --- | --- |
| `-eq`, `-ne` | equal, not equal |
| `-lt`, `-le`, `-gt`, `-ge` | less, less or equal, greater, greater or equal |
| `-and`, `-or`, `-not` (or `!`) | combine and negate conditions |
| `-like`, `-match` | wildcard pattern, regular expression |
| `-contains`, `-in` | is a value in an array: `$array -contains $x`, `$x -in $array` |

Common mistake

**> and = are not comparisons**

`>` redirects output into a file, here as in every shell. `if ($a > 5)` compares nothing: it creates a file called `5`, and the condition is false. Likewise `=` always assigns, and there is no `==` operator. Compare with `-gt`, `-lt`, `-eq`.

Common mistake

**The left side decides the type**

When the two sides have different types, the right one is converted to the type of the left one. This matters for everything that arrives as text – a parameter declared as `[string]`, what the user typed, a line of a file. Convert it before you compare or calculate.

```powershell
"10" -gt "9"          # False – two strings, compared as text: "1" comes before "9"
[int]"10" -gt 9       # True  – the left side is a number, so the right side becomes one too
"5" + 1               # 51    – the left side is text, so + glues
5 + "1"               # 6     – the left side is a number, so + adds
```

### if

The condition goes between parentheses, each branch between braces. The script [3_if_statement.ps1](https://laszlonemes.com/files/computer-systems/powershell-basics/3_if_statement.ps1) has the longer version.

```powershell
if ($number -gt 0) {
    Write-Output "positive"
} elseif ($number -lt 0) {
    Write-Output "negative"
} else {
    Write-Output "zero"
}
```

Questions about files are answered by a cmdlet, `Test-Path`. It returns true or false, so it goes straight into the condition: `-PathType Leaf` asks for a file, `-PathType Container` for a directory, and without `-PathType` anything that exists will do.

```powershell
if (Test-Path -Path $Value -PathType Leaf) {
    Write-Output "'$Value' is a file"
} elseif (Test-Path -Path $Value -PathType Container) {
    Write-Output "'$Value' is a directory"
}
```

### switch

When one value is compared with several fixed alternatives, `switch` is clearer than a chain of `elseif`s. The branch called `default` catches everything else.

```powershell
switch ($option) {
    '-a'    { Write-Output "add" }
    '-d'    { Write-Output "delete" }
    default { Write-Output "unknown" }
}
```

With `-Wildcard` the alternatives are wildcard patterns, with `-Regex` regular expressions. Patterns can overlap – and then `switch` does something a `case` statement doesn't (Fig. 5):

> **Fig. 5** · `case` stops at the first matching branch; `switch` runs every branch that matches.
>
> *Diagram:* For the value apple.txt, bash's case runs the first matching branch, *.txt, and never looks at the apple* pattern after it. PowerShell's switch checks every pattern and runs every branch that matches, so both branches run unless the first one ends with break.

```powershell
switch -Wildcard ('apple.txt') {
    '*.txt'  { Write-Output "text file" }           # runs
    'apple*' { Write-Output "starts with apple" }   # runs too – add break above to stop at the first
}
```

Common mistake

**switch runs every matching branch**

If only the first match should run, end the branch with `break`. With exact values – `'-a'`, `'-d'`, `'+'` – it makes no difference, because only one branch can match.

## Loops

The loops are the classic ones; the body always goes between braces. [5_loops.ps1](https://laszlonemes.com/files/computer-systems/powershell-basics/5_loops.ps1) shows each of them running.

- **`while (…) { }`** – Repeats as long as the condition is true. The condition is checked first.
- **`do { } until (…)`** – Checks at the end, so the body runs at least once; repeats until the condition becomes true. `do { } while (…)` is its mirror image.
- **`for (…; …; …) { }`** – The counting loop: start; condition; step.
- **`foreach ($x in …) { }`** – Walks the elements of an array – or of anything a command returns.
- **`1..5`** – A range: the numbers from 1 to 5, ready for `foreach`.
- **`break`, `continue`** – Leave the loop; skip to the next round.

```powershell
$i = 1
while ($i -le 3) {
    Write-Output "i = $i"
    $i++                                    # or: $i += 1
}

foreach ($n in 1..10) {                     # the numbers 1 to 10
    if ($n % 2 -eq 0) { continue }          # skip the even numbers
    if ($n -gt 7)     { break }             # stop above 7
    Write-Output $n                         # 1 3 5 7
}

foreach ($file in Get-ChildItem -Path . -Filter *.txt) {      # every .txt file here
    Write-Output "$($file.Name): $($file.Length) bytes"       # file objects, not names
}

1..3 | ForEach-Object { Write-Output "$_ doubled is $($_ * 2)" }   # a loop inside a pipeline
```

Good to know

**foreach and ForEach-Object are two things**

`foreach ($x in …) { }` is a **statement**. `ForEach-Object { }` is a **cmdlet inside a pipeline**, where the current item is always called `$_`. In a script the statement reads better, and `break` and `continue` work in it the way you expect.

## Script parameters

A script declares what it expects in a `param()` block at its top. Every parameter has a **name** and, if you give one, a **type** and a default value. This is [2_script_with_parameters.ps1](https://laszlonemes.com/files/computer-systems/powershell-basics/2_script_with_parameters.ps1) in short:

```powershell
param(
    [string]$Name,
    [int]$Times = 1         # the default when it isn't given
)

if (-not $Name) {
    Write-Error "Usage: $($MyInvocation.MyCommand.Name) <name> [times]"
    exit 1
}

for ($i = 1; $i -le $Times; $i++) {
    Write-Output "$i. Hello $Name!"
}
```

The caller can pass the values in order, or by name – then the order doesn't matter:

calling it

```powershell
./2_script_with_parameters.ps1 Anna 3                 # by position
./2_script_with_parameters.ps1 -Name Anna -Times 3    # by name
./2_script_with_parameters.ps1 -Times 3 -Name Anna    # by name the order doesn't matter
./2_script_with_parameters.ps1 Anna                   # Times is 1
./2_script_with_parameters.ps1 Anna abc               # stopped before it starts: "abc" is not an [int]
```

The last line shows that a type is a check as well: the text `abc` can't become an `[int]`, so PowerShell stops with an error before the first line of the script runs. If you want the error message to be your own, declare the parameter as `[string]` and test it with a regular expression – [Parameters, error handling and text files](https://laszlonemes.com/university/computer-systems/powershell-practice) compares the two ways.

- **`$PSBoundParameters`** – The parameters that were actually passed. `.Count` says how many; `.ContainsKey('Times')` whether one was given.
- **`$args`** – Extra values that match no declared parameter. In a script without a `param()` block, *all* parameters: `$args[0]`, `$args[1]`, `$args.Count`.
- **`$MyInvocation.MyCommand.Name`** – The name of the running script, for usage messages.
- **`$?`, `$LASTEXITCODE`** – Whether the last command succeeded (true/false); the exit code of the last external program.

Common mistake

**param() comes first, and arrays take commas**

The `param()` block must be the **first statement** of the script; only comments may stand before it. An array parameter (`[int[]]$Numbers`) is passed with **commas**: `./sum.ps1 1,2,3`. Separated by spaces, they would be three separate parameters.

Common mistake

**Start scripts with parameters from inside PowerShell**

When you start a `.ps1` from bash or zsh, *that* shell processes the parameters first and hands them over as plain text: `1,2,3` arrives as one string, a `*` is replaced by file names, and a value starting with a dash (`-7`) may be taken for a parameter name. Type `pwsh` first, then run the script.

## Functions

A function is declared like a small script: a name – by convention Verb-Noun again – and a `param()` block. [6_functions.ps1](https://laszlonemes.com/files/computer-systems/powershell-basics/6_functions.ps1) has the examples of this section.

```powershell
function Get-Sum {
    param(
        [int]$A,
        [int]$B
    )
    return $A + $B
}
$result = Get-Sum 3 4
```

Common mistake

**No parentheses, no commas**

A function is called like a command: `Get-Sum 3 4`, or `Get-Sum -A 3 -B 4`. The form you know from other languages, `Get-Sum(3, 4)`, passes **one** parameter – the array `(3, 4)` – and fails with *Cannot convert the "System.Object[]" value … to type "System.Int32"*.

The second trap is the result. A function doesn't return only what stands after `return`: its result is **everything it writes to the output**, and `return` merely adds one last value and ends the function (Fig. 6).

> **Fig. 6** · Whatever a function writes to the output is part of its result. `Write-Host` bypasses the output.
>
> *Diagram:* Everything a PowerShell function writes to the output becomes part of its result, not only the value after return. A Write-Output message inside the function ends up in the variable next to the number, turning the result into an array of two. Write-Host writes straight to the screen and stays out of the result.

```powershell
function Get-Square {
    param([int]$N)

    Write-Host "(calculating the square of $N)"   # to the screen only – not part of the result
    return $N * $N
}

$square = Get-Square 6      # prints the note; $square is 36
# with Write-Output instead of Write-Host, $square would be an array: the text AND 36
```

A function that answers a yes/no question returns `$true` or `$false` and goes straight into an `if`. Such functions are usually named `Test-…`:

```powershell
function Test-Even {
    param([int]$N)
    return ($N % 2 -eq 0)       # the comparison is already $true or $false
}

if (Test-Even 4) { Write-Output "4 is even" }
```

Good to know

**Scope**

A variable created inside a function is local to it. The function can *read* the variables of the script, but assigning to one creates a local copy and leaves the outer one unchanged. Hand data in through parameters and out through the result.

## Output, input and errors

PowerShell keeps several output channels – *streams* – apart. What a script writes to the output stream is its result: it can be piped on or stored in a variable. Messages for the person at the keyboard go elsewhere.

| Cmdlet | Goes to | Use it for |
| --- | --- | --- |
| `Write-Output` | the output stream | results. A bare value does the same: `"text"` on its own line is printed. |
| `Write-Host` | the screen only | progress notes that must not end up in a pipeline or a variable |
| `Write-Warning` | the warning stream | something was skipped or looks wrong, but the script goes on |
| `Write-Error` | the error stream | the reason the script can't do its job – followed by `exit 1` |
| `Read-Host` | – | asking the user for a line of input |

```powershell
$name = Read-Host "Name"              # prints "Name: " and waits – it adds the colon itself
[int]$number = Read-Host "Number"     # Read-Host returns text; convert before you calculate
```

A script ends with an exit code like any other program: `exit 0` (or reaching the end) for success, `exit 1` for failure. To throw output away, redirect it to `$null` or pipe it into `Out-Null`.

### try and catch

An error can be *caught*: when something fails inside the `try` block, execution jumps to the `catch` block, where the error is in the variable `$_`.

```powershell
try {
    $result = $a / $b                               # dividing integers by zero is an error
    $lines = Get-Content -Path $file -ErrorAction Stop
} catch {
    Write-Error "Something went wrong: $($_.Exception.Message)"
    exit 1
}
```

Common mistake

**catch doesn't see a cmdlet's error without -ErrorAction Stop**

By default the errors of cmdlets – a missing file, for example – **don't stop** the script and are not caught: the message is printed and the script goes on. Only with `-ErrorAction Stop` does the error become catchable. For files, the simplest is to check first with `Test-Path`.

In your assignment

**What a checker tries first**

Your PowerShell assignment is run with wrong parameters and a missing file before anything else. Usage and error messages go through `Write-Error`, the script ends with `exit 1`, and only results go to the output – then `./script.ps1 … | Set-Content out.txt` produces a clean file.

## Arrays and hashtables

An **array** is a numbered list of values. Many commands return one – the lines of a file, the files of a directory – and you make your own with commas. [7_arrays_hashtables.ps1](https://laszlonemes.com/files/computer-systems/powershell-basics/7_arrays_hashtables.ps1) is the example script.

```powershell
$fruits = "apple", "pear", "apple"      # an array: values separated by commas
$empty = @()                            # an empty array

$fruits.Count                           # 3
$fruits[0]                              # apple
$fruits[-1]                             # apple – a negative index counts from the end
$fruits += "plum"                       # append an element
$fruits -contains "plum"                # True
1..5                                    # the numbers 1 2 3 4 5
"one two  three" -split '\s+'           # the words of a line, as an array
```

A **hashtable** stores values under names instead of numbers: each key – any text – has one value. It is the tool for "how many of each" and "the total per …" questions. A key that isn't in the table yet gives `$null`, and `$null + 1` is `1`, so counting needs no special case for the first occurrence:

```powershell
$counts = @{}

foreach ($word in "apple", "pear", "apple") {
    $counts[$word] = $counts[$word] + 1         # a missing key is $null, and $null + 1 is 1
}

foreach ($key in ($counts.Keys | Sort-Object)) {    # the order of the keys isn't defined – sort them
    Write-Output "${key}: $($counts[$key])"         # apple: 2, pear: 1
}
```

- **`$counts = @{}`** – An empty hashtable.
- **`$counts['apple']`** – The value stored under a key – to read it or to assign it.
- **`$counts.Keys`** – All the keys, in no particular order. Sort them before printing.
- **`$counts.Count`** – The number of keys.
- **`$counts.ContainsKey($k)`** – Whether a key is in the table.

[Files as objects and hashtables](https://laszlonemes.com/university/computer-systems/powershell-files-hashtables) uses hashtables on real data, with a maximum search over the keys.

## Reading and writing files

### Reading: Get-Content

`Get-Content` returns the lines of a file, **one string per line**. Pipe them on, walk them with a loop, or keep them in a variable as an array:

```powershell
Get-Content -Path numbers_mixed.txt                 # prints the file
$lines = Get-Content -Path numbers_mixed.txt        # an array: $lines[0] is the first line
$lines.Count                                        # 10 – wc -l would say 9: there is no final newline
```

Real files are rarely tidy. [numbers_mixed.txt](https://laszlonemes.com/files/computer-systems/powershell-basics/numbers_mixed.txt) is a deliberately bad one; this is what `cat -et` shows of it – `$` marks a newline, `^M` a carriage return (the extra character of a Windows line ending), `^I` a tab – and the last line has no newline at all:

cat -et numbers_mixed.txt

```text
  15$
-4$
$
7   ^M$
12^M$
abc$
^I3$
1.5$
$
100
```

[Reading and writing files; line endings](https://laszlonemes.com/university/computer-systems/files-line-endings) shows what each of these does to a line-by-line loop in bash. `Get-Content` takes two of the four problems off your hands (Fig. 7): it cuts off either kind of line ending, and it returns the last line whether or not a newline follows it.

> **Fig. 7** · The four file traps: `Get-Content` solves the two about line endings; the two about content are yours.
>
> *Diagram:* Of the four classic traps of reading a text file line by line, Get-Content handles two by itself: a last line without a newline still arrives, and Windows CRLF line endings are cut off so no carriage return stays in the line. Skipping empty lines and trimming spaces and tabs remain the script's job.

### The pattern to start from

What remains is to trim each line and to skip the empty ones. Start every script that reads a file from this loop:

```powershell
foreach ($line in Get-Content -Path $File -Encoding UTF8) {
    $line = $line.Trim()            # cut spaces and tabs from both ends

    if ($line -eq '') {             # skip empty lines
        continue
    }

    # --- process the line ---
    Write-Output $line
}
```

Before the loop come two guards – the parameter was given, and the file exists:

```powershell
param(
    [string]$File
)

if (-not $File) {
    Write-Error "Usage: $($MyInvocation.MyCommand.Name) <file>"
    exit 1
}

if (-not (Test-Path -Path $File -PathType Leaf)) {
    Write-Error "File '$File' does not exist"
    exit 1
}
```

[8_read_file.ps1](https://laszlonemes.com/files/computer-systems/powershell-basics/8_read_file.ps1) is this skeleton with a few lines of processing: it adds up the whole numbers of a file and warns about everything else.

```text
PS> ./8_read_file.ps1 numbers_mixed.txt
WARNING: Line 6: 'abc' is not an integer, skipped
WARNING: Line 8: '1.5' is not an integer, skipped
6 valid number(s), sum: 133
Report written to 'numbers_report.txt'
```

**Splitting a line into fields.** `-split` cuts the line at a separator and returns an array; assigning that array to several variables at once gives each field a name. Trim the fields, and convert the ones you calculate with:

```powershell
$name, $count, $date = $line -split ','     # "Kiss Lajos, 12, 2023-11-13"
$name = $name.Trim()                        # "Kiss Lajos" – the space inside the name stays
[int]$count = $count.Trim()                 # 12, as a number
```

Common mistake

**A one-line file is not an array**

If the file has a **single line**, `Get-Content` returns a string, not an array, and `$lines[0]` is the *first character* of that string. When you index the lines or use `.Count`, force an array: `$lines = @(Get-Content -Path $File)`. A `foreach` loop is not affected.

### Writing

```powershell
Set-Content -Path out.txt -Value "first line"       # creates the file, or overwrites it
Add-Content -Path out.txt -Value "second line"      # appends to its end
Set-Content -Path out.txt -Value $lines             # an array: one element per line
Get-Content in.txt | Sort-Object | Set-Content -Path sorted.txt    # the output of a pipeline
```

When a script builds its output line by line, the simplest way is to **collect the lines in an array** and write the array once at the end. Nothing has to be emptied first, and running the script twice gives the same file:

```powershell
$result = @()

foreach ($line in Get-Content -Path $File -Encoding UTF8) {
    $result += $line.ToUpper()
}

Set-Content -Path $OutFile -Value $result -Encoding UTF8     # one element per line, newline after the last
```

**Writing back to the file you read.** Put the reading in parentheses. `( )` makes `Get-Content` read the whole file into memory first; only then does the writing start. Without them, the file is opened for writing while it is still being read.

```powershell
(Get-Content -Path $File) -replace 'apple', 'peach' | Set-Content -Path $File
```

### Encoding and line endings

|  | PowerShell 7 | Windows PowerShell 5.1 |
| --- | --- | --- |
| reading, by default | UTF-8 | the system's legacy code page – accented letters **break** |
| `Set-Content`, `Add-Content` by default | UTF-8 | the system's legacy code page |
| `>`, `>>`, `Out-File` by default | UTF-8 | UTF-16 – Unix tools show it as garbage |
| with `-Encoding UTF8` | UTF-8 (same as the default) | UTF-8, with a marker (BOM) at the start of the file |

That is why the example scripts write `-Encoding UTF8` on every read and write: in PowerShell 7 it changes nothing, and in the old Windows PowerShell it keeps accented text intact. For the same reason they don't write files with `>`. A file PowerShell writes gets the line endings of the system it runs on – CRLF on Windows, LF on Linux and macOS; when reading, either is fine.

## Pipelines

The `|` sends the output of one command to the next, one object at a time. Inside the braces of a pipeline cmdlet, the object that is just passing through is called **`$_`**. Six cmdlets do most of the work:

| Cmdlet | What it does |
| --- | --- |
| `Where-Object { … }` | lets through the objects for which the condition is true |
| `ForEach-Object { … }` | replaces each object by whatever the block outputs |
| `Sort-Object` | sorts – by a property (`-Property Length`) or a computed value, `-Descending` for largest first, `-Unique` to drop repeats |
| `Select-Object` | keeps the first or last few (`-First 3`, `-Last 3`), skips some (`-Skip 1`), or keeps only certain properties |
| `Group-Object` | puts equal values into groups; each group has a `Name` and a `Count` |
| `Measure-Object` | counts, and with `-Sum`, `-Average`, `-Maximum`, `-Minimum` does the arithmetic |

```powershell
Get-Content patients.txt | Where-Object { $_ -match ',2023-10-' }      # keep the lines that match
Get-Content patients.txt | ForEach-Object { ($_ -split ',')[0] }       # turn every line into its first field
Get-Content patients.txt | Select-Object -First 3                      # the first three lines
Get-ChildItem | Sort-Object -Property Length -Descending               # file objects, largest first
```

### A worked example: the most common value

[patients.txt](https://laszlonemes.com/files/computer-systems/powershell-basics/patients.txt) has one patient per line: name, date of the visit, illness. **Which illness was the most common in October?** Keep the October lines, cut out the third field, group equal illnesses, sort the groups by size, keep the first:

```powershell
Get-Content patients.txt |
    Where-Object { $_ -match ',2023-10-' } |
    ForEach-Object { ($_ -split ',')[2] } |
    Group-Object |
    Sort-Object -Property Count -Descending |
    Select-Object -First 1
```

```text
Count Name                      Group
----- ----                      -----
    3 headache                  {headache, headache, headache}
```

> **Fig. 8** · Lines of text go in; from `Group-Object` on, objects with a `Name` and a `Count` travel down the pipe.
>
> *Diagram:* The most common illness of October as a PowerShell pipeline. Get-Content emits 14 lines, Where-Object keeps the 5 October lines, ForEach-Object cuts out the third field, Group-Object folds the five strings into two objects with a Name and a Count, and sorting by Count and taking the first leaves one object: headache, 3.

The result is not a line of text to take apart again but one object, whose two values already have names (Fig. 8):

```powershell
$top = Get-Content patients.txt | … | Select-Object -First 1

Write-Output "Most common illness in October: $($top.Name) ($($top.Count) patient(s))"
```

[9_pipeline.ps1](https://laszlonemes.com/files/computer-systems/powershell-basics/9_pipeline.ps1) builds this up step by step. Typed at the prompt, with aliases, the same pipeline shrinks to one line – fine for trying things out, too terse for a script:

```powershell
gc patients.txt | ? { $_ -match ',2023-10-' } | % { ($_ -split ',')[2] } | group | Sort-Object Count -Descending | select -First 1
```

And when the question is a number – how many, how much in total, the largest – end the pipeline with `Measure-Object`:

```powershell
$stats = 4, 8, 15, 16, 23, 42 | Measure-Object -Sum -Average -Maximum -Minimum
$stats.Sum          # 108
$stats.Maximum      # 42
```

Good to know

**Select-String**

`Select-String -Path file -Pattern 'pattern'` searches files the way `grep` does, but it returns match objects, not strings: the text of the line is `.Line`, its number `.LineNumber`. When you only need the lines, `Where-Object { $_ -match … }` is simpler.

Good to know

**Breaking a long pipeline**

A pipeline may continue on the next line **after** a `|`, as in the example above. Build it the way you would any pipeline: one stage at a time, looking at the output after each.

## Coming from bash: a cheat sheet

If you already write bash, this is the quickest way in: the same things, side by side. The bash column is explained in [Shell scripts](https://laszlonemes.com/university/computer-systems/shell-scripts), [Shell scripting in practice](https://laszlonemes.com/university/computer-systems/shell-practice) and [Text processing](https://laszlonemes.com/university/computer-systems/text-processing).

### The language

|  | bash | PowerShell |
| --- | --- | --- |
| assignment | `name="Anna"` | `$name = "Anna"` |
| arithmetic | `$(( a + b ))` | `$a + $b` |
| length of a string | `${#s}` | `$s.Length` |
| substring | `${s:0:5}` | `$s.Substring(0, 5)` |
| replace | `${s//a/b}`, `sed 's/a/b/g'` | `$s -replace 'a', 'b'` |
| split into fields | `IFS=',' read -r a b <<< "$s"` | `$a, $b = $s -split ','` |
| wildcard test | `[[ $s == *apple* ]]` | `$s -like '*apple*'` |
| regex test | `[[ $s =~ ^[0-9]+$ ]]` | `$s -match '^[0-9]+$'` |
| and, or, not | `&&`, `\|\|`, `!` | `-and`, `-or`, `-not` |
| file / directory exists | `[ -f "$p" ]`, `[ -d "$p" ]` | `Test-Path $p -PathType Leaf` / `Container` |
| if | `if [ … ]; then … elif … else … fi` | `if (…) { … } elseif (…) { … } else { … }` |
| case | `case "$x" in a) … ;; *) … ;; esac` | `switch ($x) { 'a' { … } default { … } }` |
| counting loop | `for (( i=0; i<3; i++ )); do … done` | `for ($i = 0; $i -lt 3; $i++) { … }` |
| loop over a list | `for x in a b c; do … done` | `foreach ($x in 'a', 'b', 'c') { … }` |
| arguments | `$1`, `$#`, `"$@"`, `$0` | `param()`, `$PSBoundParameters.Count`, `$args`, `$MyInvocation.MyCommand.Name` |
| function result | printed with `echo`, captured with `$( )` | written to the output or `return`ed, assigned directly |
| local variable | `local x=…` | automatic inside a function |
| error message | `echo "…" >&2` | `Write-Error "…"` |
| read from the keyboard | `read -r -p "Name: " name` | `$name = Read-Host "Name"` |
| array | `a=(x y z)`, `${#a[@]}`, `${a[i]}` | `$a = 'x', 'y', 'z'`, `$a.Count`, `$a[$i]` |
| key–value store | `declare -A c`, `${c[$k]}`, `"${!c[@]}"` | `$c = @{}`, `$c[$k]`, `$c.Keys` |
| discard output | `> /dev/null` | `> $null` or `\| Out-Null` |

### Commands and their aliases

| bash | Cmdlet | Alias everywhere | Alias on Windows only |
| --- | --- | --- | --- |
| `ls` | `Get-ChildItem` | `dir`, `gci` | `ls` |
| `cat` | `Get-Content` | `gc`, `type` | `cat` |
| `cd` | `Set-Location` | `cd`, `chdir` |  |
| `pwd` | `Get-Location` | `pwd` |  |
| `cp` | `Copy-Item` | `copy`, `cpi` | `cp` |
| `mv` | `Move-Item` | `move`, `mi` | `mv` |
| `rm` | `Remove-Item` | `del`, `ri`, `rd` | `rm`, `rmdir` |
| `mkdir` | `New-Item -ItemType Directory` | `md` |  |
| `touch` | `New-Item` | `ni` |  |
| `echo` | `Write-Output` | `echo` |  |
| `man` | `Get-Help` | `help` | `man` |
| `which` | `Get-Command` | `gcm` |  |
| `ps` | `Get-Process` | `gps` | `ps` |
| `kill` | `Stop-Process` | `spps` | `kill` |
| `clear` | `Clear-Host` | `cls` | `clear` |
| `curl` | `Invoke-WebRequest` | `iwr` | `curl`, `wget` (5.1 only) |

### Filters

| bash | PowerShell |
| --- | --- |
| `grep 'pattern' file` | `Get-Content file \| Where-Object { $_ -match 'pattern' }` |
| `grep -v 'pattern'` | `Where-Object { $_ -notmatch 'pattern' }` |
| `grep -c 'pattern' file` | `@(Get-Content file \| Where-Object { $_ -match 'pattern' }).Count` |
| `wc -l < file` | `@(Get-Content file).Count` |
| `head -n 3`, `tail -n 3`, `tail -n +2` | `Select-Object -First 3`, `-Last 3`, `-Skip 1` |
| `cut -d ',' -f 1` | `ForEach-Object { ($_ -split ',')[0] }` |
| `sort`, `sort -n`, `sort -r`, `sort -u` | `Sort-Object`, `Sort-Object { [int]$_ }`, `-Descending`, `-Unique` |
| `sort \| uniq -c` | `Group-Object` |
| `tr '[:lower:]' '[:upper:]'` | `ForEach-Object { $_.ToUpper() }` |
| `sed 's/a/b/g'` | `ForEach-Object { $_ -replace 'a', 'b' }` |
| a loop or `awk` for a sum | `Measure-Object -Sum` |
| `find . -name '*.txt'` | `Get-ChildItem -Recurse -Filter *.txt` |
| `command \| tee out.txt` | `command \| Tee-Object -FilePath out.txt` |

And the loop that every file-processing script is built on, in both languages – the two lines bash needs for the line endings are simply gone:

bash

```shell
while IFS= read -r line || [ -n "$line" ]; do
    line=${line%$'\r'}             # drop a Windows \r
    if [ -z "$line" ]; then         # skip empty lines
        continue
    fi

    # --- process the line ---
    echo "$line"
done < "$file"
```

PowerShell

```powershell
foreach ($line in Get-Content -Path $File -Encoding UTF8) {
    $line = $line.Trim()            # cut spaces and tabs from both ends

    if ($line -eq '') {             # skip empty lines
        continue
    }

    # --- process the line ---
    Write-Output $line
}
```

## Common mistakes

| Wrong | Right | Why |
| --- | --- | --- |
| `name = "Anna"` | `$name = "Anna"` | The `$` is part of the name in the assignment too. |
| `if ($a > 5)` | `if ($a -gt 5)` | `>` is a redirection: a file called `5` appears and the condition is false. |
| `if ($a = 5)`, `if ($a == 5)` | `if ($a -eq 5)` | `=` assigns; there is no `==` operator. |
| `Get-Sum(3, 4)` | `Get-Sum 3 4` | With parentheses you pass one array parameter. |
| `"Size: $file.Length"` | `"Size: $($file.Length)"` | A property inside a string needs `$( )`. |
| `"$key: $value"` | `"${key}: $value"` | The colon makes PowerShell read the variable name differently. |
| `'Hello $name'` | `"Hello $name"` | Nothing is replaced inside single quotes. |
| `$line -match '[0-9]+'` for "only a number" | `$line -match '^[0-9]+$'` | Without anchors `asd2` matches too. |
| `$word -match 'apple'`, expecting case to matter | `-cmatch` | `-match`, `-like` and `-eq` ignore case. |
| `"10" -gt "9"` | `[int]"10" -gt 9` | Two strings are compared as text; the left side decides the type. |
| `$n = Read-Host "Number"`, then `$n + 1` | `[int]$n = Read-Host "Number"` | `Read-Host` returns text: `"5" + 1` is `51`. |
| `7 / 2`, expecting a whole number | `[math]::Truncate(7 / 2)` | The result is `3.5`; `[int](7 / 2)` rounds, to `4`. |
| `$lines = Get-Content $f`, then `$lines[0]` | `$lines = @(Get-Content $f)` | For a one-line file you get a string, and [0] is its first character. |
| `Get-Content $f \| … \| Set-Content $f` | `(Get-Content $f) \| … \| Set-Content $f` | Without the parentheses the writing starts while the file is still being read: an error on Windows, a damaged file elsewhere. |
| `try { Get-Content $f } catch { … }` | `Get-Content $f -ErrorAction Stop` | `catch` only sees a cmdlet's error with `-ErrorAction Stop`. |
| `Write-Output "…"` for a progress note in a function | `Write-Host "…"` | Everything a function writes to the output becomes part of its result. |
| `ls \| Where-Object Length -gt 1000` on Linux | `Get-ChildItem \| Where-Object Length -gt 1000` | `ls` is an alias only on Windows; elsewhere the real `ls` runs and returns text. |
| `param()` in the middle of the script | `param()` as the first statement | Only comments may stand before it. |
| `./script.ps1` on Windows: *running scripts is disabled* | `Set-ExecutionPolicy RemoteSigned -Scope CurrentUser` | The execution policy of Windows PowerShell 5.1 forbids scripts by default. |

## Practice

Seven small scripts, one or two per topic. Work inside `pwsh`, in the folder with the files of this page. For every task:

- parameters come through a `param()` block; a missing one gets a `Usage: …` message
- error messages go through `Write-Error`, and the script stops with `exit 1`
- a missing file or directory is an error too – check with `Test-Path`
- full cmdlet names in the script, no aliases

Task 1

**Greeting**

The script takes a name as a parameter and greets that person: `Hello <name>!`. Without a parameter it uses the name of the logged-in user.

```text
PS> ./greeting.ps1 Anna
Hello Anna!
PS> ./greeting.ps1
Hello student!
```

**Approach:**

One `[string]` parameter; `-not $Name` is true when it wasn't given. `[Environment]::UserName` works on every system – `$env:USERNAME` exists only on Windows, `$env:USER` only on Linux and macOS.

```powershell
if (-not $Name) {
    $Name = [Environment]::UserName
}
```

Task 2

**Calculator**

The script takes two numbers with an operator between them – `./calc.ps1 6 '*' 7` – and prints the result. Choose the operation with `switch`. An unknown operator and a division by zero end with an error message.

```text
PS> ./calc.ps1 6 '*' 7
6 * 7 = 42
PS> ./calc.ps1 7 / 2
7 / 2 = 3.5
PS> ./calc.ps1 7 / 0
Write-Error: Division by zero
PS> ./calc.ps1 7 x 2
Write-Error: Unknown operator 'x' (use +, -, * or /)
```

**Approach:**

Take all three as `[string]`, check `$PSBoundParameters.Count -ne 3`, validate the two numbers with the pattern `'^[+-]?[0-9]*\.?[0-9]+$'`, then convert them to `[double]`. Test the divisor yourself: dividing a decimal number by zero is not an error in PowerShell – the result is infinity.

```powershell
[double]$a = $First
[double]$b = $Second

switch ($Operator) {
    '+' { $result = $a + $b }
    '/' {
        if ($b -eq 0) {
            Write-Error "Division by zero"
            exit 1
        }
        $result = $a / $b
    }
    default {
        Write-Error "Unknown operator '$Operator' (use +, -, * or /)"
        exit 1
    }
}
```

Task 3

**File, directory, or nothing?**

The script decides whether the path it gets is a file, a directory, or doesn't exist. For a file it prints the size, for a directory the number of entries in it.

```text
PS> ./path_type.ps1 numbers_mixed.txt
'numbers_mixed.txt' is a file (34 bytes)
PS> ./path_type.ps1 .
'.' is a directory (11 entries)
PS> ./path_type.ps1 nope
'nope' does not exist
```

**Approach:**

Two `Test-Path` tests – `-PathType Leaf`, then `-PathType Container` – and an `else`. The size is a property of the file object, not a separate command. Put the expressions into the output string with `$( )`.

```powershell
$file = Get-Item -Path $Path                  # a file object
$file.Length                                  # its size in bytes

$entries = @(Get-ChildItem -Path $Path)       # @( ) keeps it an array, even with 0 or 1 entry
$entries.Count
```

Task 4

**Even numbers up to a limit**

The script prints the even numbers from 1 to the number it gets as a parameter, one per line – for 10: 2, 4, 6, 8, 10. Check with a regular expression that the parameter is a positive whole number.

**Approach:**

Take the parameter as `[string]` so the error message is yours. `^[1-9][0-9]*$` rules out `0`, `-3`, `abc` and `1.5` in one go.

```powershell
if ($Limit -notmatch '^[1-9][0-9]*$') {
    Write-Error "'$Limit' is not a positive integer"
    exit 1
}

for ([int]$i = 1; $i -le $Limit; $i++) { … }      # or: 1..$Limit | Where-Object { $_ % 2 -eq 0 }
```

Task 5

**Sum until zero**

The script keeps asking the user for numbers until a `0` arrives, then prints the sum of the numbers and how many there were. For input that isn't a number it warns and asks again.

```text
Enter a number (0 to stop): 5
Enter a number (0 to stop): abc
WARNING: 'abc' is not an integer, try again
Enter a number (0 to stop): -2
Enter a number (0 to stop): 0
Sum of the 2 number(s): 3
```

**Approach:**

`while ($true)` with a `break` on zero and a `continue` on bad input. `Read-Host` returns text – validate it as text, then convert. Don't call your variable `$input`: that name is an automatic variable of PowerShell.

```powershell
while ($true) {
    $text = Read-Host "Enter a number (0 to stop)"

    if ($text -notmatch '^-?[0-9]+$') {
        Write-Warning "'$text' is not an integer, try again"
        continue
    }

    [int]$number = $text
    if ($number -eq 0) {
        break
    }
    …
}
```

Task 6

**Patients of a month**

The script takes the patient file and a month as two digits (`01`–`12`), prints the names of that month's patients and then how many there were. Anything that isn't a valid month is refused; a month without patients gets its own message. Input: [patients.txt](https://laszlonemes.com/files/computer-systems/powershell-basics/patients.txt).

```text
PS> ./patients_of_month.ps1 patients.txt 10
Anna Kovacs
Peter Szabo
Eva Toth
Mark Varga
Julia Balogh
5 patient(s) in month 10
PS> ./patients_of_month.ps1 patients.txt 02
No patients in month 02
PS> ./patients_of_month.ps1 patients.txt 13
Write-Error: '13' is not a month (01-12)
```

**Approach:**

Two `[string]` parameters – as text, `02` keeps its leading zero. Check the month with a pattern of two alternatives, then the two guards and the reading loop from above. Split each line into its three fields and cut the month out of the date.

```powershell
if ($Month -notmatch '^(0[1-9]|1[0-2])$') {
    Write-Error "'$Month' is not a month (01-12)"
    exit 1
}
…
    $name, $date, $illness = $line -split ','

    if ($date.Substring(5, 2) -eq $Month) {       # "2023-10-13" -> "10"
        Write-Output $name
        $count++
    }
```

Task 7

**Directory statistics**

For a directory given as a parameter (default: the current one), print how many files it has, their total size, and which one is the largest. Freshly downloaded, the folder of this page gives:

```text
PS> ./dir_stats.ps1
Directory:     .
Files:         11
Total size:    24719 bytes
Largest file:  9_pipeline.ps1 (3583 bytes)
```

**Approach:**

A default value in the `param()` block: `[string]$Directory = "."`. `Get-ChildItem -File` returns only files, as objects; `Measure-Object` sums a property, and sorting by `Length` puts the largest first. Handle the empty directory before measuring.

```powershell
$files = @(Get-ChildItem -Path $Directory -File)

$stats = $files | Measure-Object -Property Length -Sum          # $stats.Count, $stats.Sum
$largest = $files | Sort-Object -Property Length -Descending | Select-Object -First 1
```

## Summary

- **pwsh** – PowerShell 7: cross-platform, UTF-8 by default – the one to use
- **cmdlet** – a built-in PowerShell command, named Verb-Noun: `Get-Content`, `Test-Path`
- **object** – a value with named properties and methods; what commands return and pipes carry
- **property** – a named piece of data on an object: `$file.Length`, `$name.Length`
- **alias** – a short second name of a cmdlet; the ones named after Unix commands exist only on Windows
- **`$_`** – the object currently passing through a pipeline
- **hashtable** – `@{}` – values stored under keys
- **execution policy** – the Windows setting that decides whether scripts may run

- Use PowerShell 7 (`pwsh`) everywhere, and start scripts with parameters from inside it.
- Commands return objects: ask for a property by name instead of cutting a column out of text.
- Comparisons are `-eq`, `-gt`, `-like`, `-match` … and work on every type; `>` is a redirection, `=` an assignment.
- Text operators ignore case, and a regular expression is written in quotes.
- Parameters get names and types in `param()`, which must be the first statement.
- Call functions without parentheses and commas; everything they write to the output is their result.
- `Get-Content` handles either line ending and a missing final newline; trimming and skipping empty lines are yours.
- `Where-Object` filters, `ForEach-Object` transforms, `Group-Object` counts – and what comes out has named properties.

### Further reading

- [The official PowerShell documentation](https://learn.microsoft.com/powershell/)
- [PowerShell 101](https://learn.microsoft.com/powershell/scripting/learn/ps101/00-introduction) – a free introductory book
- [What differs between PowerShell 7 and Windows PowerShell 5.1](https://learn.microsoft.com/powershell/scripting/whats-new/differences-from-windows-powershell)
- Built into the terminal: `Get-Help <command> -Examples`, `Get-Command`, `Get-Member`
- [regex101](https://regex101.com/) – try regular expressions; choose the ".NET (C#)" flavour for PowerShell
