Bash Scripting Sunday #1: Bash Parameter Expansion - Save Time and Avoid Subshells

In this entry, I’d like to show you how to use Bash Parameter Expansion to make your life a little easier, your scripts a little quicker, and use less resources.

Here’s an example script that isn’t using Bash Parameter Expansion:

#!/usr/bin/env bash
filename="/path/to/file.txt"
echo "Basename: $(basename $filename)"      # file.txt
echo "Dirname: $(dirname $filename)"        # /path/to

This script will output:

Basename: file.txt
Dirname: /path/to

A simple script, but it’s forking two subprocesses; one for basename and one for dirname. Bash has this functionality built in with its Parameter Expansion.

Here’s an updated script to do the same that is using Bash Parameter Expansion:

#!/usr/bin/env bash
echo "Basename: ${filename##*/}"
echo "Dirname: ${filename%/*}"

The above updated script will not fork any subprocesses, and instead purely manipulate the already stored string variable $filename

In Bash, when you enclose the variable name inside {} you are telling Bash that you wish to perform operations on the variable’s value, not just display it.

Removing Prefixes (# and ##)

These operators remove the shortest or longest match from the beginning of a variable.

# (Shortest Match)

  • Removes the shortest match of a pattern from the beginning.
filename="/path/to/file.txt"
echo "${filename#*/}"       # "path/to/file.txt" (removes first "/")

## (Longest Match)

  • Removes the longest match of a pattern from the beginning.
filename="/path/to/file.txt"
echo "${filename##*/}"      # "file.txt" (removes everything before the last "/")

Removing Suffixes (% and %%)

These operators remove the shortest or longest match from the end of a variable.

% (Shortest Match)

  • Removes the shortest match of a pattern from the end.
filename="/path/to/file.txt"
echo "${filename%.*}"       # "/path/to/file" (removes ".txt")

%% (Longest Match)

  • Removes the longest match of a pattern from the end.
filename="/path/to/archive.tar.gz"
echo "${filename%%.*}"      # "/path/to/archive" (removes everything after first ".")

Example Use Case

Extracting a directory name or removing file extensions.

Benefits to using Bash Parameter Expansion

Performance

  • Faster execution: Parameter expansion is done entirely within Bash, whereas dirname and basename require forking a new process.
  • When processing many files in a loop, avoiding subshells can significantly improve performance.

Less Resource Usage

  • Calling dirname or basename spawns a new process every time, which consumes CPU and memory, especially in large loops.

  • Example of inefficient code:

    while read -r file; do
        dir=$(dirname $file")       # Forks a new process
        base=$(basename "$file")    # Another fork
        echo "Dir: $dir, Base: $base"
    done < filelist.txt
    

    A better version using parameter expansion:

    while read -r file; do
        echo "Dir: ${file%/*}, Base: ${file##*/}"
    done < filelist.txt
    

    This avoids multiple subprocesses, making the script more efficient.

Portability & Consistency

  • dirname and basename may behave differently across Unix-like systems, especially with unusual file paths (like / or empty strings).
  • Bash’s parameter expansion is consistent across different environments, ensuring predictable behaviour.

Less Dependency on External Tools

  • If you’re writing a script for minimal environments (like embedded Linux or containers with busybox), basename and dirname might not be available.
  • Bash’s built-in expansion works as long as Bash is available.