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"; ...)