Bash Cheatsheet

Command Substitution

Use this Bash reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Basic Syntax

# Preferred: $() — nestable, readable
result=$(command)
result=$(ls -la)

# Legacy: backticks `` — avoid (hard to nest, harder to read)
result=`command`

# Both capture stdout; stderr still goes to terminal
output=$(cmd 2>&1)    # capture both stdout and stderr
output=$(cmd 2>/dev/null)  # capture stdout, discard stderr

Capturing Command Output

# Assign to variable
date_str=$(date '+%Y-%m-%d')
user=$(whoami)
count=$(ls | wc -l)
first_line=$(head -1 file.txt)
last_line=$(tail -1 file.txt)

# Use inline (don't assign)
echo "Today is $(date '+%A, %B %d')"
echo "Files: $(ls | wc -l)"
mkdir "backup-$(date +%Y%m%d)"    # date in filename

# In conditions
if [[ $(id -u) -eq 0 ]]; then echo "root"; fi
if [[ $(whoami) == "deploy" ]]; then echo "deploy user"; fi

Nesting Command Substitutions

# $() nests cleanly
echo $(basename $(dirname $(readlink -f "$0")))

# Equivalent with intermediate variables (more readable)
script_path=$(readlink -f "$0")
script_dir=$(dirname "$script_path")
parent=$(basename "$script_dir")
echo "$parent"

# Deep nesting example
latest=$(ls -t $(find . -name "*.log") | head -1)

Arithmetic via Command Substitution

# Use $(( )) for arithmetic (not a command substitution, but similar syntax)
result=$(( 2 + 2 ))
n=$(( count * 2 ))
pi_approx=$(echo "scale=4; 355/113" | bc)   # float via bc
sqrt=$(echo "sqrt(2)" | bc -l)

# expr — old style, avoid in favor of $(( ))
result=$(expr 5 + 3)    # 8
result=$(expr "$a" \* "$b")  # multiply (must escape *)

Common Patterns

# Get script's own directory
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)

# Get absolute path
abs=$(realpath "$file")
abs=$(cd "$(dirname "$file")" && pwd)/$(basename "$file")

# Count lines/words/chars
lines=$(wc -l < file.txt)   # note < avoids filename in output
words=$(wc -w < file.txt)
chars=$(wc -c < file.txt)

# Trim trailing newline ($(…) does this automatically)
content=$(cat file.txt)      # trailing newlines stripped

# Split output into array
mapfile -t arr < <(find . -name "*.py")
files=( $(glob_pattern) )    # simple, breaks on spaces

# Get PID of current shell (not $$, which works but avoid in subshells)
mypid=$(exec sh -c 'echo "$PPID"')

Whitespace and Trailing Newlines

# Command substitution strips trailing newlines automatically
str=$(printf "hello\n\n\n")
echo "${#str}"        # 5, not 8 — trailing \n stripped

# To preserve trailing newlines, append a sentinel character
str=$(printf "line1\nline2\n"; echo x)
str="${str%x}"        # remove the sentinel

# Word splitting on unquoted substitution
files=$(ls)
echo $files           # word-split and glob-expanded (dangerous!)
echo "$files"         # safe: single string with newlines

# Loop over lines (right way)
while IFS= read -r line; do
  echo "$line"
done < <(find . -name "*.txt")

Process Substitution vs Command Substitution

# Command substitution: captures output as a string
content=$(cat file.txt)

# Process substitution: provides a filename-like path to command output
diff <(sort a.txt) <(sort b.txt)   # <() is a file, not a string

# Use process substitution when a command needs a filename argument
# Use command substitution when you need the output as a value

# Feed process substitution to while loop (avoids subshell, vars persist)
count=0
while IFS= read -r line; do
  (( count++ ))
done < <(grep "pattern" bigfile.txt)
echo "Matches: $count"    # works — no subshell

Subshell Behavior

# $() creates a subshell — variable assignments don't persist
result=$(x=10; echo $x)
echo $x   # empty — x was set only in the subshell

# To persist changes, use eval (carefully) or restructure
read x <<< "$(some_command)"   # one way to extract a value

# The subshell inherits the parent's environment (read-only copy)
VAR="parent"
child=$(echo $VAR)    # VAR is visible
echo "$child"         # "parent"

Capturing Exit Status

# The exit status of command substitution is the exit status of the command
output=$(grep "pattern" file.txt)
status=$?
echo "Exit: $status"

# Combine capture and status check
if output=$(some_command); then
  echo "Success: $output"
else
  echo "Failed with status $?"
fi

# With set -e, a failed command substitution exits the script
set -e
content=$(false_command)   # script exits here if false_command fails
# Use || to handle it:
content=$(false_command) || { echo "failed"; exit 1; }

Useful Command Substitution Idioms

# Current timestamp
ts=$(date '+%Y%m%d_%H%M%S')

# Random hex string
rand=$(openssl rand -hex 16)

# Git info
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
commit=$(git rev-parse --short HEAD 2>/dev/null)

# System info
os=$(uname -s)
arch=$(uname -m)
cores=$(nproc)          # Linux
cores=$(sysctl -n hw.ncpu)  # macOS

# File content (faster than cat in some shells)
content=$(<file.txt)   # Bash built-in, no external process

# Check if inside a git repo
git_root=$(git rev-parse --show-toplevel 2>/dev/null) && echo "in git repo"

# Get the real username even when sudo'd
real_user=$(logname 2>/dev/null || echo "${SUDO_USER:-$USER}")