Bash Variables & Expansion

Variable assignment, parameter expansion, and string manipulation.

Special Variables

# Exit and process
$?                                           # Exit code of last command
$$                                           # Current shell PID
$!                                           # PID of last background process
$-                                           # Current shell options

# Script and arguments
$0                                           # Script name (or shell)
$1, $2, ...                                  # Positional parameters
${10}                                        # 10th parameter (braces required)
$#                                           # Number of parameters
$@                                           # All parameters (separate words)
$*                                           # All parameters (single string)
"$@"                                         # ALWAYS quote! Preserves spacing
"$*"                                         # All params as one string with IFS

# Last argument
$_                                           # Last argument of previous command

# Practical usage
echo "Script: $0"
echo "Args: $#"
echo "First: $1"
echo "All: $@"
echo "Last command exit: $?"
echo "Shell PID: $$"
echo "Background PID: $!"

# Iterate all arguments
for arg in "$@"; do
    echo "Argument: $arg"
done

# Shift through arguments
while (( $# > 0 )); do
    echo "Processing: $1"
    shift
done

Parameter Expansion

# Default values
${var:-default}                              # Use default if unset/empty
${var:=default}                              # Set AND use default if unset/empty
${var:+alternate}                            # Use alternate if set and non-empty
${var:?error message}                        # Error if unset/empty

# Without colon: only checks if unset (not empty)
${var-default}                               # Use default only if unset
${var=default}                               # Set only if unset
${var+alternate}                             # Use alternate if set (even if empty)
${var?error}                                 # Error only if unset

# String length
${#var}                                      # Length of var
${#arr[@]}                                   # Array length

# Substring extraction
${var:offset}                                # From offset to end
${var:offset:length}                         # From offset, length chars
${var: -3}                                   # Last 3 chars (note space!)
${var:0: -3}                                 # All except last 3

# Examples
path="/home/user/documents/file.txt"
echo "${path:0:5}"                           # "/home"
echo "${path: -8}"                           # "file.txt"
echo "${path:6:4}"                           # "user"

String Manipulation

# Removal patterns
${var#pattern}                               # Remove shortest match from start
${var##pattern}                              # Remove longest match from start
${var%pattern}                               # Remove shortest match from end
${var%%pattern}                              # Remove longest match from end

# Examples
file="/home/user/documents/report.tar.gz"
echo "${file##*/}"                           # "report.tar.gz" (basename)
echo "${file%/*}"                            # "/home/user/documents" (dirname)
echo "${file%%.*}"                           # "/home/user/documents/report"
echo "${file#*.}"                            # "tar.gz"
echo "${file##*.}"                           # "gz" (extension only)

# Substitution
${var/pattern/replacement}                   # Replace first match
${var//pattern/replacement}                  # Replace all matches
${var/#pattern/replacement}                  # Replace if starts with pattern
${var/%pattern/replacement}                  # Replace if ends with pattern

# Examples
str="hello world world"
echo "${str/world/universe}"                 # "hello universe world"
echo "${str//world/universe}"                # "hello universe universe"
echo "${str/#hello/hi}"                      # "hi world world"
echo "${str/%world/universe}"                # "hello world universe"

# Case conversion (bash 4.0+)
${var^}                                      # First char uppercase
${var^^}                                     # All uppercase
${var,}                                      # First char lowercase
${var,,}                                     # All lowercase
${var~}                                      # Toggle first char
${var~~}                                     # Toggle all

# Examples
name="john doe"
echo "${name^}"                              # "John doe"
echo "${name^^}"                             # "JOHN DOE"

NAME="JOHN DOE"
echo "${NAME,}"                              # "jOHN DOE"
echo "${NAME,,}"                             # "john doe"

Indirect References and Namerefs

# Indirect expansion
var="hello"
name="var"
echo "${!name}"                              # "hello" (value of $var)

# List matching variables
echo "${!BASH*}"                             # All vars starting with BASH

# Nameref (bash 4.3+)
declare -n ref=var
echo "$ref"                                  # "hello"
ref="world"
echo "$var"                                  # "world" (modified via ref)

# Nameref in functions
set_value() {
    local -n target=$1
    target="$2"
}
myvar=""
set_value myvar "new value"
echo "$myvar"                                # "new value"

# Return array via nameref
get_hosts() {
    local -n result=$1
    result=("vault-01" "ise-01" "bind-01")
}
declare -a hosts
get_hosts hosts
echo "${hosts[@]}"

# Dynamic variable names (eval - use carefully!)
for env in dev staging prod; do
    eval "${env}_host=\${${env^^}_HOST}"
done

# Safer: use associative array
declare -A hosts
hosts[dev]="$DEV_HOST"
hosts[staging]="$STAGING_HOST"
hosts[prod]="$PROD_HOST"

Environment Variables

# Common environment variables
$HOME                                        # User home directory
$USER                                        # Current username
$HOSTNAME                                    # System hostname
$PWD                                         # Current working directory
$OLDPWD                                      # Previous directory
$PATH                                        # Executable search path
$SHELL                                       # Current shell
$TERM                                        # Terminal type
$EDITOR                                      # Default editor
$LANG                                        # Locale setting
$TZ                                          # Timezone

# Bash-specific
$BASH_VERSION                                # Bash version
$BASH_SOURCE                                 # Source file being executed
$LINENO                                      # Current line number
$FUNCNAME                                    # Current function name
$RANDOM                                      # Random number 0-32767
$SECONDS                                     # Seconds since shell start
$EPOCHSECONDS                                # Unix timestamp (bash 5.0+)

# Export variable to child processes
export MY_VAR="value"

# Export and assign
export MY_VAR="value"

# Set for single command
MY_VAR="value" command                       # Only for this command

# Unset variable
unset MY_VAR

# Check if exported
declare -p MY_VAR                            # Shows attributes

# List all environment variables
env
printenv

# Source environment file
set -a                                       # Export all following
source .env
set +a

Array Variables

# Indexed array
declare -a arr=("one" "two" "three")
arr+=("four")                                # Append
echo "${arr[0]}"                             # First element
echo "${arr[@]}"                             # All elements
echo "${#arr[@]}"                            # Length
echo "${!arr[@]}"                            # All indices

# Associative array (bash 4.0+)
declare -A map
map["key"]="value"
map["host"]="vault-01"
echo "${map[key]}"                           # Access by key
echo "${!map[@]}"                            # All keys
echo "${map[@]}"                             # All values

# Array slicing
echo "${arr[@]:1:2}"                         # Elements 1 and 2
echo "${arr[@]: -2}"                         # Last 2 elements

# Array from command
mapfile -t lines < file.txt
readarray -t lines < file.txt               # Same as mapfile

# Array from string
IFS=',' read -ra items <<< "a,b,c"

# Check if array
declare -p var | grep -q "declare -a" && echo "Indexed array"
declare -p var | grep -q "declare -A" && echo "Associative array"

Readonly and Local Variables

# Readonly (constant)
readonly CONFIG_PATH="/etc/myapp"
declare -r DB_HOST="localhost"               # Same

# Attempting to modify fails
CONFIG_PATH="/other"                         # Error!

# Local variables (function scope)
myfunc() {
    local var="local value"
    local -r const="immutable"               # Local and readonly
    echo "$var"
}

# Local shadows global
GLOBAL="global"
myfunc() {
    local GLOBAL="local"
    echo "$GLOBAL"                           # "local"
}
myfunc
echo "$GLOBAL"                               # "global" (unchanged)

# Local with attributes
myfunc() {
    local -i num=5                           # Integer
    local -a arr=(1 2 3)                     # Array
    local -A map                             # Associative array
    local -l lower="HELLO"                   # Lowercase
    local -u upper="hello"                   # Uppercase
}

# Integer attribute
declare -i count=0
count="5 + 3"                                # Evaluated as arithmetic
echo "$count"                                # 8

Infrastructure Variable Patterns

# Configuration with defaults
: "${VAULT_ADDR:=https://vault-01:8200}"
: "${ISE_HOST:=ise-01.inside.domusdigitalis.dev}"
: "${LOG_LEVEL:=INFO}"

# Validate required variables
: "${API_TOKEN:?API_TOKEN is required}"
: "${DB_PASSWORD:?DB_PASSWORD must be set}"

# Load config from file
load_config() {
    local file="${1:-.env}"
    if [[ -f "$file" ]]; then
        set -a
        source "$file"
        set +a
    fi
}

# Build connection strings
DB_URL="postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_NAME}"

# Dynamic host configuration
declare -A HOSTS=(
    [vault]="${VAULT_HOST:-vault-01}"
    [ise]="${ISE_HOST:-ise-01}"
    [bind]="${BIND_HOST:-bind-01}"
)

for service in "${!HOSTS[@]}"; do
    echo "$service: ${HOSTS[$service]}"
done

# Environment-specific settings
case "${ENVIRONMENT:-dev}" in
    dev)
        API_URL="https://api-dev.example.com"
        DEBUG=true
        ;;
    staging)
        API_URL="https://api-staging.example.com"
        DEBUG=true
        ;;
    prod)
        API_URL="https://api.example.com"
        DEBUG=false
        ;;
esac

# Safe credential handling
get_password() {
    local service="$1"
    # From gopass
    gopass show "v3/domains/d000/$service" 2>/dev/null || \
    # From environment
    eval "echo \"\${${service^^}_PASSWORD}\"" || \
    # Prompt user
    read -s -p "$service password: " pass && echo "$pass"
}

Arithmetic Variables

# Integer attribute
declare -i num
num="5 + 3"                                  # Evaluated!
echo "$num"                                  # 8

num="hello"                                  # Becomes 0
echo "$num"                                  # 0

# Arithmetic expansion
result=$((5 + 3))
result=$((a * b + c))
result=$((RANDOM % 100))                     # Random 0-99

# Compound assignment
((count++))                                  # Increment
((count--))                                  # Decrement
((count += 5))                               # Add 5
((count *= 2))                               # Double

# Arithmetic in conditionals
if (( count > 10 )); then
    echo "Greater than 10"
fi

# let command (older style)
let "result = 5 + 3"
let "count++"

# Floating point (use bc or awk)
result=$(echo "scale=2; 22/7" | bc)
result=$(awk "BEGIN {printf \"%.2f\", 22/7}")

# Base conversion
echo $((16#FF))                              # Hex to decimal: 255
echo $((2#1010))                             # Binary to decimal: 10
echo $((8#77))                               # Octal to decimal: 63

printf "%x\n" 255                            # Decimal to hex: ff
printf "%o\n" 64                             # Decimal to octal: 100

Quoting and Escaping

# Double quotes: Variables expand
var="world"
echo "Hello $var"                            # Hello world
echo "Home: $HOME"                           # Home: /home/user

# Single quotes: Literal, no expansion
echo 'Hello $var'                            # Hello $var
echo '$HOME'                                 # $HOME

# Mixed quoting
echo "It's a $var"                           # It's a world
echo 'He said "hi"'                          # He said "hi"
echo "He said \"hi\""                        # He said "hi"

# $'...' - ANSI-C quoting
echo $'Line 1\nLine 2'                       # Interprets \n
echo $'\t'                                   # Tab
echo $'\e[31mRed\e[0m'                        # ANSI color

# Command substitution
files=$(ls)
files=`ls`                                   # Old style, avoid

# Escape special characters
echo "Price: \$100"                          # Price: $100
echo "Path: /home/user\ name"                # Escaped space
echo "Tab:	end"                             # Literal tab

# Array quoting
arr=("one" "two three" "four")
for item in "${arr[@]}"; do                  # MUST quote!
    echo "$item"
done

# Command arguments with spaces
mkdir "My Directory"                         # Single argument
cp "file name.txt" "dest dir/"               # Both quoted

Variable Gotchas

# WRONG: Space around =
var = "value"                                # Error: var: command not found

# CORRECT: No spaces
var="value"

# WRONG: Unquoted variable with spaces
file="my file.txt"
cat $file                                    # Tries to cat "my" and "file.txt"

# CORRECT: Always quote
cat "$file"

# WRONG: Using $ in assignment
$var="value"                                 # Tries to run command!

# CORRECT
var="value"

# WRONG: Arithmetic on string
var="hello"
echo $((var + 1))                            # Error or 1 (var = 0)

# WRONG: Command substitution without quotes
files=$(ls *.txt)
for f in $files; do                          # Splits on spaces!
    echo "$f"
done

# CORRECT: Use array
files=(*.txt)
for f in "${files[@]}"; do
    echo "$f"
done

# WRONG: Export and assign on same line in older bash
export VAR=$(command)                        # Exit code lost!

# CORRECT: Separate lines if you need exit code
VAR=$(command)
export VAR

# WRONG: Uninitialized array element
arr[5]="value"
echo "${arr[0]}"                             # Empty, not error!

# WRONG: Overwriting IFS globally
IFS=,
read -ra items <<< "a,b,c"
# IFS is now broken for everything!

# CORRECT: Local or restore
old_IFS=$IFS
IFS=,
read -ra items <<< "a,b,c"
IFS=$old_IFS

# Or use subshell
(IFS=,; read -ra items <<< "a,b,c"; ...)