Assigning variables

A variable is a name for a value. The two shells write and treat them differently. In Bash, a shell variable is assigned with name=value, without spaces around the =, and read with $name. Its value is always text: Bash calculates only where you ask for it.

server=web01
port=443
echo "Checking $server on port $port"
total="5"
total+=3
echo "String append: $total"
declare -i count=5
count+=3
echo "Integer add:   $count"
echo "Arithmetic:    $(( 5 + 3 ))"
Checking web01 on port 443
String append: 53
Integer add:   8
Arithmetic:    8

+= appends text to a normal variable, so 5 and 3 become 53. Only a variable declared as an integer (declare -i) or arithmetic expansion $(( … )) adds numbers, and only whole numbers.

PowerShell variables always carry the $, also when you assign them, spaces around = are fine, and the value keeps its .NET type. Mixing types follows one rule: the type of the left operand wins.

$server = 'web01'
$port = 443
"Checking $server on port $port"
$port.GetType().Name
$server.GetType().Name
"5" + 5
5 + "5"
Checking web01 on port 443
Int32
String
55
10

"5" + 5 starts with a string, so the 5 is turned into text and appended; 5 + "5" starts with a number, so the string is converted and added. Values that come from files, CSV exports or Read-Host are strings, so convert them before you calculate: [int]$kwh. Variable names in PowerShell are case-insensitive ($Server and $server are the same variable); in Bash they are not.

Quoting in Bash

Quoting tells Bash which characters to take literally. Single quotes take everything literally; double quotes still expand $variables, $(commands) and $((arithmetic)), and a backslash inside them escapes only $, ", \, the backtick and a newline. $'…' (ANSI-C quoting) understands escape sequences such as \t and \n.

echo 'Single: $HOME `date` \n'
echo "Double: \$HOME \"quoted\" \\"
printf '%s\n' $'Tab:\there'
Single: $HOME `date` \n
Double: $HOME "quoted" \
Tab:	here

Word splitting

The rule that breaks more Bash scripts than any other: word splitting is the step in which Bash cuts the result of an unquoted expansion, such as $file or $(command), into separate words at spaces, tabs and newlines (the characters in the special variable IFS, the internal field separator). Each piece becomes a separate argument. A file name with a space becomes two names:

file="meter readings.csv"
touch "$file"
ls $file
echo "---"
ls "$file"
ls: cannot access 'meter': No such file or directory
ls: cannot access 'readings.csv': No such file or directory
---
meter readings.csv
The variable file holds meter readings.csv. Unquoted, ls $file receives two arguments, meter and readings.csv. Quoted, ls "$file" receives one argument, meter readings.csv. file="meter readings.csv" ls $file ls meter readings.csv 2 arguments ls "$file" ls meter readings.csv 1 argument Each box is one argument that ls receives.
Word splitting happens after the variable is replaced by its value. Double quotes keep the value together as one argument.

After splitting comes pathname expansion: any unquoted *, ? or […] in the result is treated as a wildcard and replaced by matching file names. An alert text with an asterisk suddenly lists the folder:

touch billing.log portal.log
alert="Disk full on db01 *"
echo $alert
echo "$alert"
Disk full on db01 billing.log portal.log
Disk full on db01 *

An empty unquoted variable is worse still: it disappears altogether, and a test can then give the wrong answer without any error. [ -n $answer ] becomes [ -n ], which is a test of the non-empty string -n and therefore true:

answer=""
if [ -n $answer ]; then echo "unquoted: answer is not empty?!"; fi
if [ -n "$answer" ]; then echo "quoted: not empty"; else echo "quoted: empty"; fi
unquoted: answer is not empty?!
quoted: empty

The fix is always the same: write "$var", "$(command)" and "$@" in double quotes. The few places that need no quotes, such as the right side of an assignment and the inside of [[ … ]], are not worth remembering as exceptions. ShellCheck, the Bash linter (see Testing, Linting and Style), finds unquoted variables for you:

cat > cleanup.sh <<'X'
#!/bin/bash
file=$1
ls $file
X
shellcheck cleanup.sh || echo "shellcheck exit status: $?"

In cleanup.sh line 3:
ls $file
   ^---^ SC2086 (info): Double quote to prevent globbing and word splitting.

Did you mean:
ls "$file"

For more information:
  https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...
shellcheck exit status: 1

PowerShell has no word splitting: a variable is passed to a cmdlet as one value, whatever it contains. Get-ChildItem $file finds meter readings.csv without quotes. Wildcards in the value are still interpreted by cmdlets that accept them, which is what -LiteralPath is for (see Files, Paths and Globbing).

Parameter expansion

Parameter expansion is Bash's ${…} syntax for reading a variable and changing its value on the way out: taking part of it, replacing text, changing case, or supplying a default. It runs inside the shell, so it is faster and safer than piping a value through sed or cut.

host="srv-web01"
file="/var/log/kestrel/billing.log"
echo "${host^^}  ${#host}  ${host:4}  ${host/web/db}"
echo "${file##*/}  ${file%/*}  ${file%.log}.old  ${file##*.}"
echo "Environment: ${KESTREL_ENV:-test}"
SRV-WEB01  9  web01  srv-db01
billing.log  /var/log/kestrel  /var/log/kestrel/billing.old  log
Environment: test
Expansion Result PowerShell equivalent
${#host} Length: 9 $name.Length
${host^^}, ${host,,} Upper case, lower case $name.ToUpper(), .ToLower()
${host:4} From position 4: web01 $name.Substring(4)
${host/web/db} Replace the first match $name.Replace('web', 'db') (all matches)
${file##*/} Remove the longest match from the start: the file name Split-Path $file -Leaf
${file%/*} Remove the shortest match from the end: the folder Split-Path $file -Parent
${VAR:-default} The default if VAR is empty or unset $var ?? 'default' (when $null)
${VAR:?message} Stop with an error if VAR is empty or unset [Parameter(Mandatory)]

The PowerShell column assumes a variable $name with the same text (not $host: $Host is a built-in PowerShell variable). The ?? operator exists since PowerShell 7.0. ${1:?message} is a compact way to make an argument required:

cat > deploy.sh <<'X'
#!/bin/bash
target="${1:?usage: deploy.sh SERVER}"
echo "Deploying to $target"
X
bash deploy.sh web01
bash deploy.sh
echo "exit status: $?"
Deploying to web01
deploy.sh: line 2: 1: usage: deploy.sh SERVER
exit status: 1

Command substitution and arithmetic

Command substitution, $(command), puts a command's output into a string or variable. Two details matter in scripts: it removes all trailing newlines, and $( ) nests cleanly, where the old backtick form `command` does not. Arithmetic expansion $(( … )) calculates with whole numbers only.

x=$(printf 'web01\n\n\n')
echo "[$x]"
echo "Nested: $(basename "$(dirname /var/log/kestrel/billing.log)")"
[web01]
Nested: kestrel

The quotes inside the nested $( ) are separate from the outer ones, so each level is quoted on its own. PowerShell needs no command substitution syntax: a command's output is a value, so $files = Get-ChildItem just works, and inside a string you use the subexpression operator, below.

Strings in PowerShell

PowerShell has the same two kinds of quotes. A single-quoted string is literal. An expandable string is a double-quoted string in which PowerShell replaces variables by their values when the line runs, a form of string interpolation. The escape character is the backtick `, not the backslash, and a quote inside a string of the same kind is doubled.

$server = 'srv-web01'
$kwh = 4521
'Single: Server $server'
"Double: Server $server"
"Property, wrong: $server.Length characters"
"Property, right: $($server.Length) characters"
"Cost: $($kwh * 0.28) EUR for $kwh kWh"
"Backtick: `$server is literal, a tab:`there"
'It''s fine'
"Say ""hi"""
Single: Server $server
Double: Server srv-web01
Property, wrong: srv-web01.Length characters
Property, right: 9 characters
Cost: 1265.88 EUR for 4521 kWh
Backtick: $server is literal, a tab:	here
It's fine
Say "hi"

The subexpression operator

Line 5 of that example is the classic PowerShell string trap. In an expandable string, only the plain variable name is replaced: $server.Length becomes the value of $server followed by the literal text .Length. Anything more than a variable needs the subexpression operator, $( ), which runs the code between the parentheses and puts its result into the string: a property, a calculation, a method call or a whole command. It looks like Bash's command substitution and plays a similar role, but it runs PowerShell code, not a separate program.

A few more rules complete the picture. An array inside a string is joined with spaces; a hash table shows only its type name, so reach in for the value:

$path = 'C:\Logs\app.log'
"Path: $path"
"Tab in PowerShell: a`tb"
"Backslash is literal: a\tb"
$arr = 1, 2, 3
"Array: $arr"
$h = @{ meter = 'M-1042' }
"Hash table: $h"
"Hash table value: $($h.meter)"
Path: C:\Logs\app.log
Tab in PowerShell: a	b
Backslash is literal: a\tb
Array: 1 2 3
Hash table: System.Collections.Hashtable
Hash table value: M-1042

Because the backslash is an ordinary character, Windows paths need no escaping in PowerShell strings. The flip side: in a regular expression, such as with -replace, the backslash is the regex escape character again, so '\\' matches one backslash there (see Text Processing in Scripts).

Here documents and here-strings

For text of several lines, such as an email body, a configuration snippet or a ticket note, both shells have a block syntax. Bash has the here document: <<EOF expands variables in the block, <<'EOF' with a quoted marker keeps it literal. The short form <<< (a here string) feeds a single string to a command's standard input.

ticket="INC-20419"
server="web01"
cat <<EOT
Ticket: $ticket
Server: $server
EOT
cat <<'EOT'
Literal: $server
EOT
grep -c web <<< "web01 web02"
Ticket: INC-20419
Server: web01
Literal: $server
1

PowerShell's here-string is a multi-line string literal between @" and "@ (expandable) or @' and '@ (literal). Unlike a here document it is a value, not input for a command, so you can store it in a variable. Inside it, quotes need no escaping.

$server = 'web01'
$ticket = 'INC-20419'
$body = @"
Ticket:  $ticket
Server:  $server
Checked: $(Get-Date -Date '2026-10-06T09:00:00' -Format 'yyyy-MM-dd HH:mm')
"@
$body
$template = @'
Use $server and $(...) literally here.
'@
$template
Ticket:  INC-20419
Server:  web01
Checked: 2026-10-06 09:00
Use $server and $(...) literally here.

Undefined variables

A typo in a variable name is one of the most dangerous script bugs, because by default neither shell complains: the misspelled variable is simply empty. In a cleanup script, an empty path can turn a narrow command into a broad one. Both shells can be told to be strict.

cat > total.sh <<'X'
#!/bin/bash
kwh=4521
echo "Total: $kwhh kWh"
set -u
echo "Total: $kwhh kWh"
echo "never printed"
X
bash total.sh
echo "exit status: $?"
Total:  kWh
total.sh: line 5: kwhh: unbound variable
exit status: 1
$kwh = 4521
"Total: $kwhh kWh"
Set-StrictMode -Version Latest
"Total: $kwhh kWh"
"Still running"
Total:  kWh
InvalidOperation: /home/amara/scripts/example.ps1:4
Line |
   4 |  "Total: $kwhh kWh"
     |          ~~~~~
     | The variable '$kwhh' cannot be retrieved because it has not been set.
Still running

Both now catch the typo, but they react differently. Bash's set -u stops the script. PowerShell's strict mode reports an error and, by default, carries on with the next line; to stop, combine it with $ErrorActionPreference = 'Stop'. Both are explained with their pitfalls on Error Handling.

Side by side

Task Bash PowerShell
Assign server=web01 $server = 'web01'
Literal text '$server' '$server'
Text with variables "Host: $server" "Host: $server"
Expression in text "$(( kwh * 2 ))", "$(cmd)" "$($kwh * 2)", "$($s.Length)"
Escape character \ ` (backtick)
Multi-line text <<EOF / <<'EOF' @" … "@ / @' … '@
Default value ${env_name:-test} $envName ?? 'test'
Catch undefined variables set -u Set-StrictMode -Version Latest
Environment variable $HOME, export NAME=value $env:HOME ($env:USERPROFILE on Windows), $env:NAME = 'value'

Environment variables are the one place where the shells meet: Bash reads them like any other variable, PowerShell through the env: prefix. They are also the right place for settings and secrets that must not be written into a script (see Writing Robust and Safe Scripts).