Bash Parameter Expansion Cheatsheet
Bash parameter expansion syntax for default values, substrings, prefix and suffix removal, search and replace, case conversion, and arrays.
Parameter expansion reads, tests, and reshapes variable values inside ${...} without running an external command. The syntax below is for Bash 5. If a script must also run on macOS, which ships Bash 3.2, check the version card first.
Basic Expansion
Braces mark where the variable name ends. Quote expansions to keep the value as one word.
| Expansion | Result |
|---|---|
$var | Value of var |
${var}_old | Value followed by _old; without braces, Bash reads $var_old |
"${var}" | Value as one word, with no word splitting or globbing |
${10} | Tenth positional parameter; $10 is $1 followed by 0 |
Default Values
The colon forms treat an empty value like an unset one. Without the colon, only an unset variable triggers the operator.
| Expansion | Result |
|---|---|
${var:-word} | word if var is unset or empty; var stays unchanged |
${var-word} | word only if var is unset |
${var:=word} | Assign word to var if unset or empty, then expand it |
${var=word} | Assign word only if var is unset |
${var:?message} | Print message and fail if var is unset or empty |
${var?message} | Fail only if var is unset |
${var:+word} | word if var is set and non-empty, otherwise nothing |
${var+word} | word if var is set, even to an empty string |
: "${var:=word}" | Assign a default on its own line |
Bash expands word only when it is used, so ${var:-$(cmd)} runs cmd only when var is unset or empty. In a script, a failed :? check exits the script.
Length and Substrings
Offsets start at 0. A negative offset needs a space or parentheses, otherwise Bash reads it as the :- operator.
| Expansion | Result |
|---|---|
${#var} | Number of characters in the value |
${var:7} | Everything from offset 7 to the end |
${var:0:5} | Five characters starting at offset 0 |
${var: -5} | Last five characters |
${var:(-5)} | Last five characters, using parentheses instead of a space |
${var: -5:3} | Three characters, starting five from the end |
${var:1:-1} | Value without its first and last character |
${var:i:n} | Offset and length from variables; both are arithmetic, so $ is optional |
${var:-5} is a default value, not an offset. It returns the whole value when var is set and non-empty, or 5 when it is unset or empty. In a UTF-8 locale, lengths and offsets count characters, not bytes.
Remove a Prefix or Suffix
# trims from the start and % trims from the end. One operator removes the shortest match; two remove the longest.
| Expansion | Result |
|---|---|
${var#pattern} | Remove the shortest match from the start |
${var##pattern} | Remove the longest match from the start |
${var%pattern} | Remove the shortest match from the end |
${var%%pattern} | Remove the longest match from the end |
${var#"$prefix"} | Remove the exact text in prefix, even if it contains * or ? |
${dir%/} | Remove one trailing slash |
A pattern that does not match leaves the value unchanged.
Split Paths and Filenames
These examples use path=/var/www/site/index.backup.html and file=archive.tar.gz.
| Expansion | Result |
|---|---|
${path##*/} | index.backup.html, the filename |
${path%/*} | /var/www/site, the directory |
${path%.*} | /var/www/site/index.backup, without the last extension |
${path%%.*} | /var/www/site/index, without any extension |
${path##*.} | html, the last extension |
${file#*.} | tar.gz, every extension |
${file%%.*} | archive, the name before the first dot |
A name without a dot comes back unchanged from ${file##*.}, and ${file%.*} turns .bashrc into an empty string. Extension patterns also match dots in directory names, so extract the filename with file=${path##*/} before removing extensions when directory names may contain dots. For the edge cases that the basename command
handles, such as a trailing slash, use the command instead.
Search and Replace
Patterns are globs, not regular expressions. Leave out the replacement to delete the match.
| Expansion | Result |
|---|---|
${var/pattern/text} | Replace the first match |
${var//pattern/text} | Replace every match |
${var/#pattern/text} | Replace a match at the start only |
${var/%pattern/text} | Replace a match at the end only |
${var//pattern} | Delete every match |
${var// /_} | Replace each space with an underscore |
${var//[0-9]/#} | Replace each digit with # |
${var/#/text} | Add text at the start |
${var/%/text} | Add text at the end |
${var//o/[&]} | Wrap each o in brackets; & inserts the matched text |
${var//o/\&} | Replace each o with a literal & |
Bash 5.2 turned on patsub_replacement, so an unquoted & in the replacement means the matched text, even when it comes from a variable. Quote the replacement, as in ${var//o/"$text"}, to keep it literal.
Change Case
A pattern after the operator limits which characters change.
| Expansion | Result |
|---|---|
${var^} | Uppercase the first character |
${var^^} | Uppercase every character |
${var,} | Lowercase the first character |
${var,,} | Lowercase every character |
${var^^[aeiou]} | Uppercase only the vowels |
[[ ${a,,} == "${b,,}" ]] | Compare two strings without regard to case |
Transform Values
The ${var@op} operators format a value or describe the variable that holds it.
| Expansion | Result |
|---|---|
${var@Q} | Value quoted so the shell can read it back as input |
${var@E} | Value with backslash escapes such as \n and \t expanded |
${var@P} | Value expanded like a prompt string, so \u@\h becomes user@host |
${var@A} | Assignment or declare command that recreates var |
${var@a} | Attribute flags, such as a (array), i (integer), r (read-only), or x (exported) |
${var@U} | Uppercase the value |
${var@u} | Uppercase the first character |
${var@L} | Lowercase the value |
"${arr[@]@Q}" | Every element, quoted separately |
${map[@]@K} | Keys and quoted values of an associative array, as one string |
"${map[@]@k}" | Keys and values as separate words |
Arrays
Quote [@] to keep each element as one word. The string operators above apply to every element.
| Expansion | Result |
|---|---|
${arr[0]} | Element at index 0 |
${arr[-1]} | Last element |
"${arr[@]}" | Every element, one word each |
"${arr[*]}" | Every element in one word, joined by the first character of IFS |
${#arr[@]} | Number of elements |
${#arr[2]} | Length of element 2 |
${!arr[@]} | Indices, or the keys of an associative array |
"${arr[@]:1:2}" | Two elements, starting at index 1 |
"${arr[@]: -2}" | Last two elements when indices are consecutive |
"${arr[@]%.*}" | Every element without its extension |
"${arr[@]/#/dir/}" | Every element with dir/ added at the start |
Negative slice offsets count back from one greater than the highest index, so gaps can leave fewer elements in the result. $arr and ${#arr} refer only to element 0. For declaring and changing arrays, see the Bash arrays
guide.
Positional Parameters
$@ accepts the same substring and pattern operators as an array.
| Expansion | Result |
|---|---|
${1:-.} | First argument, or . when it is missing or empty |
${1:?usage: $0 FILE} | Stop with a usage message when the first argument is missing or empty |
$# | Number of arguments |
"${@:2}" | Every argument from the second onward |
"${@:2:3}" | Three arguments, starting at the second |
${@: -1} | Last argument |
${!#} | Last argument, using indirection |
"${@/#/--}" | Every argument with -- added at the start |
See Bash positional parameters
for shift, $*, and quoting rules.
Indirect Expansion
These forms expand a variable whose name is stored in another variable.
| Expansion | Result |
|---|---|
${!ref} | Value of the variable named in ref |
${!ref:-default} | Same, with a default when that variable is unset or empty |
ref='arr[@]'; "${!ref}" | Every element of the array named in ref |
${!prefix*} | Names of all variables that start with prefix |
"${!prefix@}" | Same names, one word each |
declare -n ref=var | Make ref a nameref; reading or assigning ref uses var |
Do not build the name in ref from untrusted input. Bash evaluates array subscripts in the name, so ref='a[$(cmd)]' runs cmd.
Pattern Syntax
The #, %, and / operators match with the same wildcards
as filenames. The extglob forms need shopt -s extglob.
| Pattern | Matches |
|---|---|
* | Any string, including an empty one |
? | Any single character |
[abc] | One of the listed characters |
[!abc] or [^abc] | One character that is not listed |
[0-9] | One character in the range |
[[:space:]] | One character from a class, such as [:digit:], [:alpha:], or [:upper:] |
+(pattern) | One or more matches (extglob) |
*(pattern) | Zero or more matches (extglob) |
?(pattern) | Zero or one match (extglob) |
@(jpg|jpeg) | Exactly one of the alternatives (extglob) |
Common Recipes
| Recipe | Result |
|---|---|
echo "${PATH//:/$'\n'}" | Print each PATH directory on its own line |
n=${var//[^,]}; echo "${#n}" | Count the commas in var |
(IFS=,; printf '%s\n' "${arr[*]}") | Join array elements with commas |
host=${url#*://}; host=${host%%/*} | Host and port from a URL such as https://example.com:8080/page |
${ver%%.*} | Major version, 5 from 5.3.9 |
${var##+([[:space:]])} | Trim leading whitespace (extglob) |
${var%%+([[:space:]])} | Trim trailing whitespace (extglob) |
${var#"${var%%[![:space:]]*}"} | Trim leading whitespace without extglob |
${var%"${var##*[![:space:]]}"} | Trim trailing whitespace without extglob |
${file/%.@(jpg|jpeg)/.png} | Change a .jpg or .jpeg extension to .png (extglob) |
[[ -z ${var+x} ]] | True only when var is unset |
Common Mistakes
Most expansion bugs come from a missing space, missing quotes, or the wrong shell.
| Mistake | Fix |
|---|---|
${var:-3} returns the whole value | Add a space for a negative offset: ${var: -3} |
| A value with spaces splits into several words | Quote the expansion: "$var", "${arr[@]}" |
| A variable in the pattern acts as a wildcard | Quote it: ${var#"$prefix"} |
& in a replacement turns into the match | Write \& or quote the replacement |
.+ or ^ in a pattern does not work | Patterns are globs; use [[ $var =~ regex ]] or sed for regular expressions |
unbound variable under set -u | Write ${var:-} for variables that may be unset |
Bad substitution | The script runs under sh or an old Bash; run it with bash and check the version |
substring expression < 0 | The negative length points before the offset, or Bash is older than 4.2 |
See the Bash set command
guide for set -u and the other strict mode options.
Bash Version Support
Run echo "$BASH_VERSION" to check your version. Bash 3.2, still /bin/bash on macOS, has none of the features below.
| Feature | Minimum Bash |
|---|---|
${var^^}, ${var,,}, associative arrays | 4.0 |
${var:1:-1}, ${arr[-1]} | 4.2 |
declare -n namerefs | 4.3 |
@Q, @E, @P, @A, @a | 4.4 |
@U, @u, @L, @K | 5.1 |
@k, & as the match in replacements | 5.2 |
Related Guides
Use these guides for worked examples and the full syntax.
| Reference | Covers |
|---|---|
| Bash Parameter Expansion | Worked examples of defaults, trimming, and replacement |
| Bash String Manipulation | Substrings, replacement, case, and substring checks |
| Bash Cheatsheet | Variables, conditionals, loops, and functions |
| Regex Cheatsheet | Regular expressions for [[ =~ ]], grep, and sed |
| GNU Bash Shell Parameter Expansion | Official definitions of every operator |