Bash scripting guide · part 7 of 32
Parameter expansion
The ${...} operators for defaults, lengths, substrings, trimming prefixes and suffixes, search and replace, and case conversion, without calling sed or cut.
${name} is the plain form of $name. Inside the braces, bash accepts a set of operators that
transform the value as it is expanded: supply a default, cut off a prefix, replace text,
change case. They run inside the shell, with no extra process, so they are much faster than
piping a variable through sed, cut or basename, and they handle any characters
correctly. The parameter expansion cheat sheet lists every form on one page.
Defaults for unset or empty variables
Four operators deal with variables that may be missing. With the colon they treat an empty value like an unset one; without it, only truly unset counts.
| Form | Result when var is unset or empty |
|---|---|
${var:-word} |
expands to word; var unchanged |
${var:=word} |
assigns word to var, then expands to it |
${var:?msg} |
prints msg and fails (a script exits) |
${var:+word} |
the opposite: word only when var is set and non-empty |
Default values for optional settings
port=""
echo "port: ${port:-8080}"
echo "port without colon: [${port-8080}]"
echo "log level: ${LOG_LEVEL:=info}, and now LOG_LEVEL=$LOG_LEVEL"
verbose=1
echo "flags: ${verbose:+--verbose}"
port: 8080 port without colon: [] log level: info, and now LOG_LEVEL=info flags: --verbose
port is set but empty, so :- used 8080, while - without the colon kept the empty
value. := assigned info to LOG_LEVEL as well. :+ is handy for optional flags: it
produces --verbose only when verbose has a value.
${1:-default} is the standard way to give a script argument a default, as the fixture's
scripts/greet.sh does. := cannot assign to positional parameters, only to named variables;
the usual pattern is : "${OUTPUT_DIR:=./out}", where : is the do-nothing command that
just causes the expansion.
Fail early when a required value is missing
#!/usr/bin/env bash
target=${1:?usage: deploy.sh TARGET}
echo "deploying to $target"
bash deploy.sh staging
bash deploy.sh
deploying to staging deploy.sh: line 2: 1: usage: deploy.sh TARGET
With no argument, :? printed the message, prefixed with the script name, line and
parameter, and the script exited with status 1 before doing anything.
Length and substrings
${#var} is the length in characters (not bytes, in a UTF-8 locale). ${var:offset:length}
extracts a substring, counting from 0. A negative offset counts from the end, but it needs a
space or parentheses before the minus, because ${var:-...} already means "default".
Length, substrings and negative offsets
ts="2026-03-14 08:00:01"
echo "length: ${#ts}"
echo "date: ${ts:0:10} time: ${ts:11}"
echo "last two: ${ts: -2}"
echo "wrong: ${ts:-2}"
word="café"; echo "${#word} characters"
length: 19 date: 2026-03-14 time: 08:00:01 last two: 01 wrong: 2026-03-14 08:00:01 4 characters
${ts:11} runs to the end. ${ts: -2} takes the last two characters, but without the space
${ts:-2} is the default operator and returns the whole (non-empty) value. café counts as 4
characters because the locale is UTF-8.
Removing a prefix or suffix
These four take a glob pattern (Globbing), not a regular expression:
${var#pattern}removes the shortest match from the start,##the longest.${var%pattern}removes the shortest match from the end,%%the longest.
A way to remember: on a US keyboard # is left of $ and % is right of it. They are the
shell's built-in basename, dirname and extension handling.
Strip directories and extensions
path=/var/log/nginx/access.log.2.gz
echo "file: ${path##*/}"
echo "dir: ${path%/*}"
echo "no ext: ${path%.*}"
echo "base name: ${path%%.*}"
echo "first ext: ${path#*.}"
file: access.log.2.gz dir: /var/log/nginx no ext: /var/log/nginx/access.log.2 base name: /var/log/nginx/access first ext: log.2.gz
##*/ removed everything up to the last slash. %.* removed the shortest .something
from the end (.gz), while %%.* removed from the first dot onward. #*. removed up
to and including the first dot.
Change file extensions in a loop
for f in photos/*.png; do
echo mv "$f" "${f%.png}.jpg"
done
mv photos/IMG_2041.png photos/IMG_2041.jpg mv photos/beach.png photos/beach.jpg mv photos/forest.png photos/forest.jpg
${f%.png} drops the extension, then .jpg is appended. The echo in front makes this a
dry run; remove it to actually rename. The same pattern converts .txt to .md, .tar.gz to
.tgz (with %.tar.gz), and so on.
Search and replace
${var/pattern/replacement} replaces the first match, ${var//pattern/replacement} every
match. Anchor a pattern with /# (must match at the start) or /% (at the end). An empty
replacement deletes. Patterns are globs again, so * is greedy and ? matches one character.
Replace text inside a variable
line="fix login bug, fix logout bug"
echo "${line/fix/FIXED}"
echo "${line//fix/FIXED}"
echo "${line//bug/}"
f="report.txt.txt"; echo "${f/%.txt/}"
csv="a b c"; echo "${csv// /,}"
FIXED login bug, fix logout bug FIXED login bug, FIXED logout bug fix login , fix logout report.txt a,b,,c
One / changed only the first fix; // changed both. An empty replacement removed every
bug. /% anchored the match at the end, so only the last .txt went. Turning spaces into
commas keeps the two consecutive spaces as two commas.
In bash 5.2, & in the replacement stands for the matched text when the patsub_replacement
option is on, which it is by default. Quote it (\& or inside quotes) if you mean a literal
ampersand.
Case conversion
${var^^} uppercases the whole value, ${var,,} lowercases it, and the single forms ^ and
, change only the first character. These need bash 4 or later; macOS's /bin/bash 3.2 does
not have them.
Uppercase, lowercase and capitalize
env_name=Production
echo "${env_name^^} ${env_name,,}"
name=alice; echo "${name^}"
read -r answer <<< "YES"
[[ ${answer,,} == y* ]] && echo "accepted: $answer"
PRODUCTION production Alice accepted: YES
Lowercasing before a comparison is the easy way to accept yes, Yes and YES alike.
Indirection and transformations
${!name} expands the variable whose name is stored in name. ${!prefix@} lists all
variable names starting with prefix. Bash 4.4 added ${var@op} transformations; the most
useful are @Q (quote the value for reuse as shell input) and @U/@L (case, bash 5.1).
Look up a variable by name
db_host=db01; db_port=5432; db_name=demo
for key in host port name; do
var="db_$key"
echo "$var = ${!var}"
done
echo "all: ${!db_@}"
msg="it's 5 * 2"; echo "${msg@Q}"
db_host = db01 db_port = 5432 db_name = demo all: db_host db_name db_port 'it'\''s 5 * 2'
${!var} read db_host, db_port and db_name through a computed name. ${!db_@} listed
the matching variable names. @Q printed the value quoted so it can be pasted back into a
shell safely. For real lookups by key, an associative array (Associative arrays) is
clearer than indirection.
Operators on arrays and positional parameters
Every operator above also works on "${array[@]}" and "$@", applied to each element:
"${files[@]%.txt}" strips the extension from every element. ${#array[@]} is the number of
elements, and ${array[@]:1:2} slices elements rather than characters.
Apply an operator to every element
files=(notes.txt todo.txt "file with spaces.txt")
printf '[%s]\n' "${files[@]%.txt}"
echo "count: ${#files[@]}, slice: ${files[@]:1:2}"
set -- alpha beta; echo "${@^}"
[notes] [todo] [file with spaces] count: 3, slice: todo.txt file with spaces.txt Alpha Beta
%.txt was applied to each of the three elements, and quoting kept the one with spaces whole.
The slice took two elements starting at index 1. ${@^} capitalized every positional parameter.
Related
basenameStrip the directory part (and optionally a suffix) from a path, leaving the file name.dirnameStrip the last component from a path, leaving the directory part.sedEdit a stream of text with a script of commands, most often to substitute text or delete lines.cutPrint selected fields, characters or bytes from each line, such as columns of a CSV or TSV file.trTranslate, squeeze or delete characters from standard input; for case changes, line endings and stripping characters.- VariablesAssigning and reading shell variables, the no-spaces rule, braces, unset vs empty, scope, readonly, and the special variables bash sets for you.
- String manipulationLength, substrings, prefix and suffix removal, search and replace, case, trimming, splitting and joining, done with bash's own expansions instead of external tools.
- ArraysIndexed arrays in bash, creating, reading, appending, looping, slicing and removing elements, and using them to build command lines safely.
- QuotingSingle quotes, double quotes, backslashes and $'...', what each protects, and the unquoted-variable bugs they prevent, each shown failing and then fixed.
- GlobbingHow *, ? and [...] match file names, what happens when nothing matches, and the shell options nullglob, failglob, dotglob, globstar and extglob.