String Manipulation (Parameter Expansion)
| Since: | All Linux | |
|---|---|---|
| macOS(2001 Cheetah) | ||
| Bash 1.0(1989) |
Bash parameter expansion lets you manipulate the string value of a shell variable without calling any external command. By combining various operators with the ${variable} syntax, you can get the string length, extract a substring, strip a prefix or suffix, replace text, change case, and set default values.
String Length
Use ${#var} to get the number of characters in a variable's value.
${#var}
Run the following command:
name="Kiryu Kazuma"
echo ${#name}
12
When used with an array, ${#array[n]} returns the length of element n, not the number of elements. Use ${#array[@]} to get the element count.
members=("Kiryu Kazuma" "Majima Goro" "Akiyama Shun")
echo ${#members[@]}
3
echo ${#members[0]}
12
Substring Extraction
${var:offset:length} extracts a substring starting at offset (zero-based) for length characters. If length is omitted, the rest of the string is returned.
${var:offset}
${var:offset:length}
Run the following command:
filename="kamurocho_map.dat"
echo ${filename:10}
map.dat
echo ${filename:10:3}
map
A negative offset counts from the end of the string. The minus sign needs a space before it or parentheses around it — ${var: -3} or ${var:(-3)} — otherwise it is parsed as the default-value syntax ${var:-default}.
filename="kamurocho_map.dat"
echo ${filename: -3}
dat
Prefix and Suffix Stripping
These operators delete a matching pattern from the beginning or end of a string. # strips the shortest match from the front, ## strips the longest match from the front, % strips the shortest match from the end, and %% strips the longest match from the end.
${var#pattern} # strip shortest match from the front
${var##pattern} # strip longest match from the front
${var%pattern} # strip shortest match from the end
${var%%pattern} # strip longest match from the end
Patterns can include glob characters (*, ?, [...]).
| Syntax | Direction | Match style |
|---|---|---|
| ${var#pattern} | From the front | Shortest match (non-greedy) |
| ${var##pattern} | From the front | Longest match (greedy) |
| ${var%pattern} | From the end | Shortest match (non-greedy) |
| ${var%%pattern} | From the end | Longest match (greedy) |
Strip the directory path to get the basename. ${path##*/} removes everything up to and including the last /.
path="/home/kiryu/clan_roster.txt"
echo ${path#*/}
home/kiryu/clan_roster.txt
echo ${path##*/}
clan_roster.txt
Strip the filename to get the directory, or strip the extension.
path="/home/kiryu/clan_roster.txt"
echo ${path%.txt}
/home/kiryu/clan_roster
echo ${path%/*}
/home/kiryu
Strip the timestamp prefix from a log line.
line="2024-01-15 [INFO] Kiryu Kazuma logged in"
echo ${line#* }
[INFO] Kiryu Kazuma logged in
Substitution
${var/pattern/replacement} replaces the first match. ${var//pattern/replacement} replaces all matches.
${var/pattern/replacement} # replace first match
${var//pattern/replacement} # replace all matches
To replace only at the beginning of the string, use ${var/#pattern/replacement}. To replace only at the end, use ${var/%pattern/replacement}.
${var/#pattern/replacement} # replace only if pattern matches at the start
${var/%pattern/replacement} # replace only if pattern matches at the end
Omitting (or using an empty) replacement deletes the match.
| Syntax | What is replaced |
|---|---|
| ${var/pattern/replacement} | First match |
| ${var//pattern/replacement} | All matches |
| ${var/#pattern/replacement} | Match at start only |
| ${var/%pattern/replacement} | Match at end only |
Replace the first comma with a tab character.
record="Majima Goro,majima@example.com,Majima Family"
echo ${record/,/$'\t'}
Majima Goro majima@example.com,Majima Family
Replace all commas with tabs using //.
record="Majima Goro,majima@example.com,Majima Family"
echo ${record//,/$'\t'}
Majima Goro majima@example.com Majima Family
Replace an error keyword in a log entry.
log_entry="ERROR: majima_everywhere.log access denied"
echo ${log_entry/ERROR/WARN}
WARN: majima_everywhere.log access denied
Case Conversion (Bash 4+)
Bash 4.0 and later support case conversion through parameter expansion. ${var^^} converts all characters to uppercase, ${var,,} converts all characters to lowercase, ${var^} uppercases only the first character, and ${var,} lowercases only the first character.
${var^^} # all uppercase
${var,,} # all lowercase
${var^} # first character to uppercase
${var,} # first character to lowercase
You can supply a pattern to limit which characters are converted — for example, ${var^^[a-z]}.
| Syntax | Effect |
|---|---|
| ${var^^} | Convert all characters to uppercase |
| ${var,,} | Convert all characters to lowercase |
| ${var^} | Uppercase the first character only |
| ${var,} | Lowercase the first character only |
| ${var^^pattern} | Uppercase characters matching pattern |
| ${var,,pattern} | Lowercase characters matching pattern |
name="kiryu kazuma"
echo ${name^^}
KIRYU KAZUMA
echo ${name^}
Kiryu kazuma
echo ${name^^k}
Kiryu Kazuma
Useful for normalizing environment variable names or sanitizing user input.
status="active"
echo ${status^^}
ACTIVE
echo "Status: ${status^^}"
Status: ACTIVE
Default Values and Error Checking
These operators let you return a fallback value, assign a default, or abort with an error message when a variable is unset or null.
${var:-default} # return default if var is unset or null (var unchanged)
${var:=default} # return default if var is unset or null, and assign it to var
${var:+value} # return value if var is set and non-null (var unchanged)
${var:?error_msg} # print error_msg and exit if var is unset or null
The difference between :- and := is that the latter also assigns the default to the variable. Omitting the colon (:) changes the condition to "unset only", so a null (empty) value passes through.
| Syntax | Condition | Behavior | Assigns to var |
|---|---|---|---|
| ${var:-default} | Unset or null | Returns default | No |
| ${var:=default} | Unset or null | Returns default | Yes |
| ${var:+value} | Set and non-null | Returns value | No |
| ${var:?error_msg} | Unset or null | Exits with error | No |
| ${var-default} | Unset only | Returns default | No |
| ${var=default} | Unset only | Returns default | Yes |
| ${var+value} | Set (even if null) | Returns value | No |
| ${var?error_msg} | Unset only | Exits with error | No |
unset clan
echo ${clan:-"Dojima Family"}
Dojima Family
echo $clan
clan=""
echo ${clan:-"Dojima Family"}
Dojima Family
echo ${clan-"Dojima Family"}
:= also assigns the value to the variable, making it useful for initialization.
unset logfile
echo ${logfile:=/var/log/clan_roster.log}
/var/log/clan_roster.log
echo $logfile
/var/log/clan_roster.log
:? is useful for checking required arguments in a script. If the variable is unset or null, the error message is printed to standard error and the script exits with status 1.
echo ${TARGET_DIR:?"TARGET_DIR is not set"}
bash: TARGET_DIR: TARGET_DIR is not set
Sample Code
A script that combines parameter expansion to process a file: it extracts the basename (without extension) and the extension, and applies a default value for the backup directory.
clan_roster_backup.sh
#!/bin/bash
# Check required variable (exit with error if missing)
INPUT_FILE=${1:?"Usage: $0 <filename>"}
BACKUP_DIR=${BACKUP_DIR:-/tmp/backup}
# Decompose the filename into its parts
filename=${INPUT_FILE##*/} # strip directory
basename=${filename%.*} # strip extension
ext=${filename##*.} # extract extension
ext_lower=${ext,,} # normalize extension to lowercase
echo "Filename: $filename"
echo "Basename: $basename"
echo "Extension: $ext_lower"
echo "Backup dir: $BACKUP_DIR"
# Create the backup directory if it does not exist
[ -d "$BACKUP_DIR" ] || mkdir -p "$BACKUP_DIR"
# Copy with a timestamp suffix
timestamp=$(date +%Y%m%d_%H%M%S)
cp "$INPUT_FILE" "$BACKUP_DIR/${basename}_${timestamp}.${ext_lower}"
echo "Backup complete: ${basename}_${timestamp}.${ext_lower}"
Run the following command:
bash clan_roster_backup.sh /home/kiryu/clan_roster.txt Filename: clan_roster.txt Basename: clan_roster Extension: txt Backup dir: /tmp/backup Backup complete: clan_roster_20240115_093012.txt
A script that normalizes log entries: converts character names to title case and standardizes delimiters.
normalize_log.sh
#!/bin/bash
LOGFILE=${1:-majima_everywhere.log}
while IFS= read -r line; do
# Strip the timestamp prefix
content=${line#*] }
# Replace all commas with pipes
normalized=${content//,/|}
# Capitalize the first character
normalized=${normalized^}
echo "$normalized"
done < "$LOGFILE"
Overview
Parameter expansion does not fork a subshell or launch an external process, so it is faster than external commands such as sed, awk, and cut. The difference becomes especially noticeable in scripts that process large numbers of files in a loop.
That said, parameter expansion only supports glob patterns — not regular expressions. For complex pattern matching or back-references, use sed or awk. Case conversion (^^ and ,,) requires Bash 4.0 or later. macOS ships with /bin/bash version 3.2 (for licensing reasons), so these operators are not available there. Installing Bash 5.x via Homebrew, or switching to Zsh, is one way to work around this on macOS.
Common Mistakes
Common Mistake: Mixing Up # and % Direction
On a standard keyboard, # is on the left side and % is on the right side. Use this as a memory aid: # removes from the left (front), and % removes from the right (end).
Use ## (front, longest match) to strip the directory path and get the basename.
path="/home/akiyama/real_estate_records.csv"
echo ${path##*/}
real_estate_records.csv
Use % to strip the extension from the end. Using # by mistake produces unintended results.
path="/home/akiyama/real_estate_records.csv"
echo ${path%.csv}
/home/akiyama/real_estate_records
echo ${path#*.}
csv
NG: trying to remove the trailing extension using # instead of %.
path="real_estate_records.csv"
echo ${path#*.csv}
real_estate_records.csv
${path#*.csv} tries to strip the shortest front-anchored match of *.csv. Because real_estate_records.csv matches that pattern in full, the entire string is consumed and an empty string (or the original string, depending on the Bash version) is returned. To strip a trailing extension, always use ${path%.*} or ${path%.csv}.
Another common confusion is between ## (longest-match single strip from the front) and // (replace all occurrences). ## removes a pattern once from the beginning; it does not act globally like //.
If you find any errors or copyright issues, please contact us.