Language
日本語
English

Caution

JavaScript is disabled in your browser.
This site uses JavaScript for features such as search.
For the best experience, please enable JavaScript before browsing this site.

Linux & Mac & Bash Command Dictionary

  1. Home
  2. Linux & Mac & Bash Command Dictionary
  3. String Manipulation (Parameter Expansion)

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 (*, ?, [...]).

SyntaxDirectionMatch style
${var#pattern}From the frontShortest match (non-greedy)
${var##pattern}From the frontLongest match (greedy)
${var%pattern}From the endShortest match (non-greedy)
${var%%pattern}From the endLongest 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.

SyntaxWhat 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]}.

SyntaxEffect
${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.

SyntaxConditionBehaviorAssigns to var
${var:-default}Unset or nullReturns defaultNo
${var:=default}Unset or nullReturns defaultYes
${var:+value}Set and non-nullReturns valueNo
${var:?error_msg}Unset or nullExits with errorNo
${var-default}Unset onlyReturns defaultNo
${var=default}Unset onlyReturns defaultYes
${var+value}Set (even if null)Returns valueNo
${var?error_msg}Unset onlyExits with errorNo
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 .