Bash Parameter Expansion Explainer
Every ${…} form — defaults, substrings, pattern removal, substitution — with what it does, which bash added it, and a worked example.
Caps: one ${…} expression up to 512 characters; filter queries up to 256.
All 22 expansion forms shown
Paste a ${…} expression above to see its parameter, its operator and its word named, with a link to the matching table row.
| Form | Group | Meaning | Since | Example |
|---|---|---|---|---|
${v:-word} | Defaults | Expands to word when v is unset or null; ${v-word} (no colon) substitutes only when v is unset. | 2.0 | unset -v u${u:-fallback}fallbackExecuted on GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25) |
${v:=word} | Defaults | Like :- but also assigns word to v, so later expansions see it; ${v=word} tests unset only. | 2.0 | unset -v u${u:=fallback}fallbackExecuted on GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25) |
${v:?message} | Defaults | Expands to v when set; when unset or null it prints message to stderr and a script exits — ${v?message} tests unset only. | 2.0 | v=/usr/local/bin/tool.tar.gz${v:?a message}/usr/local/bin/tool.tar.gzExecuted on GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25) |
${v:+word} | Defaults | The inverse of :- — expands to word only when v IS set and non-null, else to nothing; ${v+word} tests set-at-all. | 2.0 | unset -v u${u:+set}(empty)Executed on GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25) |
${v:offset} | Substrings | The substring of v starting at offset (zero-based) and running to the end. | 2.0 | v=/usr/local/bin/tool.tar.gz${v:15}tool.tar.gzExecuted on GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25) |
${v:offset:length} | Substrings | The substring of v starting at offset, at most length characters long. | 2.0 | v=/usr/local/bin/tool.tar.gz${v:5:9}local/binExecuted on GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25) |
${v: -n} | Substrings | A negative offset counts from the end — the space is required, because ${v:-n} is the use-default operator. | 2.0 | v=abcdef${v: -3}defExecuted on GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25) |
${#v} | Length | The length of v's value in characters — # before the name, by position. | 2.0 | v=/usr/local/bin/tool.tar.gz${#v}26Executed on GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25) |
${v#pattern} | Removal | Removes the SHORTEST match of the glob pattern from the front of v. | 2.0 | v=/usr/local/bin/tool.tar.gz${v#*/}usr/local/bin/tool.tar.gzExecuted on GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25) |
${v##pattern} | Removal | Removes the LONGEST front match — ${v##*/} strips every leading directory, leaving the basename. | 2.0 | v=/usr/local/bin/tool.tar.gz${v##*/}tool.tar.gzExecuted on GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25) |
${v%pattern} | Removal | Removes the SHORTEST match of the glob pattern from the end of v. | 2.0 | v=/usr/local/bin/tool.tar.gz${v%.gz}/usr/local/bin/tool.tarExecuted on GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25) |
${v%%pattern} | Removal | Removes the LONGEST end match — ${v%%.*} cuts at the FIRST dot, taking every extension with it. | 2.0 | v=/usr/local/bin/tool.tar.gz${v%%.*}/usr/local/bin/toolExecuted on GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25) |
${v/pattern/replacement} | Substitution | Replaces the first match of the glob pattern (not a regex) with replacement. | 2.0 | v=/usr/local/bin/tool.tar.gz${v/bin/sbin}/usr/local/sbin/tool.tar.gzExecuted on GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25) |
${v//pattern/replacement} | Substitution | Replaces EVERY match; a / inside the pattern must be escaped as \/ so it isn't read as the delimiter. | 2.0 | v=/usr/local/bin/tool.tar.gz${v//\//\|}\|usr\|local\|bin\|tool.tar.gzExecuted on GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25) |
${v^^} | Case | Uppercases every character of v (an optional glob pattern selects which). | 4.0 | v=tool${v^^}TOOLOn macOS bash 3.2: /bin/bash: ${v^^}: bad substitutionCited — GNU Bash manual |
${v,,} | Case | Lowercases every character of v (an optional glob pattern selects which). | 4.0 | v=TOOL${v,,}toolOn macOS bash 3.2: /bin/bash: ${v,,}: bad substitutionCited — GNU Bash manual |
${!v} | Indirection | Reads v's value as ANOTHER variable's name and expands that one instead. | 2.0 | n=name; name=World${!n}WorldExecuted on GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25) |
${v@Q} | Quoting | Expands to v's value quoted so it can be reused safely as shell input. | 4.4 | v='a b'${v@Q}'a b'On macOS bash 3.2: /bin/bash: ${v@Q}: bad substitutionCited — GNU Bash manual |
${a[@]} | Arrays | Every element of the array a — quoted, each element stays its own word. | 2.0 | a=(x y z w)${a[@]}x y z wExecuted on GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25) |
${a[*]} | Arrays | Every element of a — quoted, joined into ONE word on the first IFS character. | 2.0 | a=(x y z w)${a[*]}x y z wExecuted on GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25) |
${#a[@]} | Arrays | The number of elements in the array a (not the length of one value). | 2.0 | a=(x y z w)${#a[@]}4Executed on GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25) |
${a[@]:offset:length} | Arrays | A slice of the array — length elements starting at offset, as separate words. | 2.0 | a=(x y z w)${a[@]:1:2}y zExecuted on GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25) |
The macOS /bin/bash is from 2007
Apple froze /bin/bash at 3.2.57 (GPLv2), so the case operators ${v,,} and ${v^^} (bash 4.0) and the quoting operator ${v@Q} (bash 4.4) all stop with the error shown in their rows. To use them, run a newer bash (Homebrew installs one) or zsh's own equivalents — the macOS default shell is zsh precisely because /bin/bash stayed frozen.
Do it offline:$ bash -c 'v=/usr/local/bin/tool.tar.gz; echo "${v##*/}"'
How to use it
- Paste exactly one ${…} expression into the box — up to 512 characters. As you type, the card names the parameter, the operator token and the word, flags a leading ! as indirection, and links the table row that teaches the form.
- Read the matched row. Each carries the canonical spelling, its plain meaning, the bash version band, and a worked example whose output was recorded from a real bash run — not typed from memory.
- Narrow the table when you're browsing instead: free text matches forms and meanings (try ## or remove), the group menu cuts to one family, and / focuses the box. Every row keeps a stable #exp- anchor you can hand to a teammate.
That ${var##*/} in a script you inherited has an exact grammar: a parameter, an operator, a word. Paste it above and this page names the three parts and points at the matching row of its 22-form table — defaults, substrings, pattern removal, substitution, case, arrays. It parses the expression; it never runs it, expands it or executes any shell.
Examples
${v##*/}tool.tar.gzThe ## operator deletes the longest front match of */ — every leading directory — which is why this idiom is the shell's built-in basename.
${v%.gz}/usr/local/bin/tool.tarA single % trims the shortest match off the end, so exactly one extension comes off and the .tar stays.
${u:-fallback}fallbackWith u unset, :- substitutes its word without touching u. Write := instead when you also want the assignment to stick.
${v:5:9}local/binSubstring extraction: start at zero-based offset 5, take at most 9 characters. Offsets count characters, not bytes.
${v: -3}defRun with v=abcdef. The space before -3 is load-bearing: it makes the offset negative (count from the end) instead of spelling the :- default operator.
${#a[@]}4Run with a=(x y z w). The # prefix on an [@]-subscripted array counts elements; on a plain variable it counts characters.
Every example on this page is executed against the same engine that powers the tool, on every build.
Gotchas worth knowing
Three forms your macOS terminal will refuse
Apple ships /bin/bash 3.2.57 from 2007, so ${v,,}, ${v^^} (added in bash 4.0) and ${v@Q} (4.4) die there with a bad substitution error. The rows above print the exact stderr; a Homebrew bash runs all three.
The colon is a null test, not decoration
${u:-w} substitutes when u is unset OR empty; ${u-w} only when it is truly unset. The same one-character split applies to :=, :? and :+ — dropping the colon narrows the test to unset-only, which matters the day a variable holds an empty string on purpose.
${v: -3} needs its space
Without the space, bash reads :- as the use-default operator and hands you the whole value with 3 as its fallback. One blank turns the same characters into a from-the-end substring — the classic silent surprise of this grammar.
One # sign, two meanings, position decides
In front of the name it measures — ${#v} is a length. After the name it removes — ${v#pat} strips a prefix. The explainer above disambiguates by position exactly the way bash itself does.
Patterns here are globs, and / needs escaping
The pat in removal and substitution is glob syntax — * and ? — never a regular expression, so .* matches a literal dot then anything. And because / delimits ${v/pat/rep}, replacing a slash itself is spelled ${v//\//|}.
This is not a shell, and arrays are not POSIX
Nothing on this page executes, lints (that is ShellCheck's job) or covers [[ ]], getopts, heredocs, or brace, tilde and arithmetic expansion — different mechanisms with their own rules. So are the listing forms: ${!prefix*} expands to variable NAMES starting with prefix and ${!a[@]} to an array's keys — cousins of ${!v} indirection that the explainer refuses rather than half-explains. Array forms like ${a[@]:1:2} need bash proper: under sh or dash they are a syntax error. Octal permission strings live with the chmod calculator, and rewriting text in a pipeline is the sed tester's stream-editing territory.
Common errors
That doesn't start with ${ — this page explains ${…} parameter expansions, one at a time.- The answer to pasting a whole command line or a bare $v. Trim the paste down to the ${…} span itself — echo, quotes and pipeline around it aren't part of the expansion grammar.
The ${ is never closed — add the matching } and it can be parsed.- Raised when the closing brace is missing or swallowed by a quote. The scan is quote-aware, so a } inside "…" or '…' doesn't count as the terminator — exactly the case where an expansion looks closed but isn't.
macOS /bin/bash (3.2.57) refuses this form: /bin/bash: ${v,,}: bad substitution- Not a typo in your script — a version gap. The lowercase, uppercase and @Q quoting operators postdate the bash Apple froze, so the explainer still parses them but warns you with the real stderr a 3.2 shell prints.
FAQ
Does this page ever run the expansion I paste?
No. The explainer is a parser: it splits your ${…} into parameter, operator and word by grammar alone, so nothing you paste is evaluated, no variables exist, and there is no shell interpreter on the page. The example outputs in the table were recorded once, from a real bash on the machine that built this page, and shipped as frozen text.
What's the difference between ${v#pat} and ${v%pat}?
Direction. # trims from the front, % from the back — mnemonic: on a US keyboard # sits left of % the way a prefix sits left of a suffix. Doubling either switches from shortest to longest match, which is how ${v##*/} eats a whole directory path while ${v#*/} removes just one segment.
Do these forms work in zsh, or in plain sh scripts?
Partly. The defaults, length and prefix/suffix removal are POSIX and portable to any sh. Substrings, substitution, indirection and arrays are bashisms — zsh accepts most with its own quirks, dash accepts none. If a script starts with #!/bin/sh, stick to the POSIX rows.
Keyboard shortcuts
- /
- Focus the filter box