Skip to content

Variables

Variables are the foundation of any programming language, and bash is no exception. In this chapter, we’ll explore variables in depth, including variable types, declaration, scope, and special parameters. This knowledge is essential for writing robust bash scripts used in DevOps, SRE, and System Administration.


A variable is a named storage location that holds a value. In bash, variables are dynamically typed and don’t need explicit type declaration.

┌─────────────────────────────────────────────────────────────────────┐
│ VARIABLE CONCEPTS │
└─────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────┐
│ VARIABLE STRUCTURE │
│ ───────────────── │
│ │
│ ┌─────────────────┐ ┌──────────────────────────────┐ │
│ │ Variable Name │ ────▶│ Stored Value │ │
│ │ │ │ │ │
│ │ SERVER_NAME │ │ "production-web-01" │ │
│ │ PORT │ │ 8080 │ │
│ │ DB_HOST │ │ "db.example.com" │ │
│ │ MAX_RETRIES │ │ 3 │ │
│ └─────────────────┘ └──────────────────────────────┘ │
│ │
│ Variable = Name + Value + Type (implicit) │
└───────────────────────────────────────────────────────────────────────┘
Terminal window
# Valid variable names
name="John"
server_name="prod-server"
PORT=8080
dbHost="localhost"
MAX_RETRIES=3
_private_var="hidden"
MY_VAR_123="allowed"
# Invalid variable names (will cause errors)
# 123variable="starts with number"
# my-var="contains hyphen"
# my.var="contains dot"
# my var="contains space"
# if="reserved keyword"
Terminal window
# Bash variables are case-sensitive
NAME="John"
name="Jane"
Name="Mike"
echo "NAME=$NAME" # NAME=John
echo "name=$name" # name=Jane
echo "Name=$Name" # Name=Mike
# Convention: Use UPPERCASE for constants, lowercase for variables
# This is a common convention, not a requirement

Terminal window
# String assignment (no spaces around =)
greeting="Hello"
message='Welcome to Bash Scripting'
# Number assignment (stored as string internally)
count=42
price=19.99
# Empty variable
empty_var=
# or
empty_var=""
# Command output assignment
current_date=$(date)
file_count=$(ls -1 | wc -l)
disk_usage=$(df -h / | tail -1 | awk '{print $5}')

The declare command is used to give attributes to variables.

Terminal window
# Declare integer variable
declare -i age=25
age="30" # Automatically converted to integer
# Declare readonly variable
declare -r CONSTANT="immutable"
# CONSTANT="changed" # Error: readonly variable
# Declare array
declare -a fruits=("apple" "banana" "cherry")
# Declare associative array (bash 4+)
declare -A user_info
user_info["name"]="John"
user_info["email"]="john@example.com"
# Declare export variable
declare -x PATH_VAR="/usr/local/bin"
# Show variable attributes
declare -p variable_name
declare -p age
# Output: declare -i age="30"
# List all variables with attributes
declare

typeset is an alias for declare in bash. It’s more portable to other shells.

Terminal window
# Same as declare
typeset -i counter=10
typeset -r CONST="value"
# Use typeset for better portability
typeset -A config # Associative array

Terminal window
# Basic access with $
echo $name
echo $PORT
# Recommended: Use ${} for clarity and to avoid ambiguity
echo ${name}
echo ${PORT}
# When to use ${}:
# - At the end of a variable, both work
# - When concatenating with other text, ${} is required
# This works:
echo "Hello $name"
# This works:
echo "Hello ${name}"
# This FAILS (ambiguous):
# echo "Hello $name_suffix" # Tries to find $name_suffix
# This WORKS:
# echo "Hello ${name}_suffix" # Finds $name and appends _suffix
# Examples:
server="production"
echo "Server: ${server}-01"
echo "Port: ${PORT}8080"

Bash provides powerful parameter expansion for handling defaults.

Terminal window
# Use default value if variable is unset or null
name=""
echo "Hello ${name:-Guest}" # Output: Hello Guest
# Note: name is still empty after this
# Assign default value if variable is unset or null
name=""
echo "Hello ${name:=Guest}" # Output: Hello Guest
echo "name is now: $name" # Output: name is now: Guest
# Use alternate value if variable is set
name="John"
echo "Hello ${name:+User}" # Output: Hello User (uses alternate)
# Show error if variable is unset
# set -u is usually better (see below)
# ${name:?Error message}
Terminal window
# Get length of variable
text="Hello World"
echo ${#text} # Output: 11
# Substring extraction
text="Hello World"
echo ${text:0:5} # Output: Hello (from position 0, length 5)
echo ${text:6:5} # Output: World (from position 6, length 5)
echo ${text:-5} # Output: World (last 5 characters)
# String removal (pattern matching)
filename="report-2024-01-15.csv"
echo ${filename#*.} # csv (remove shortest match from start)
echo ${filename##*.} # csv (remove longest match from start)
echo ${filename%.*} # report-2024-01-15 (remove shortest from end)
echo ${filename%%.*} # report (remove longest from end)
# Search and replace
text="Hello World"
echo ${text/World/Universe} # Hello Universe (first match)
echo ${text//o/O} # HellO WOrld (all matches)
echo ${text/#Hello/Hi} # Hi World (if starts with Hello)
echo ${text/%World/Everyone} # Hello Everyone (if ends with World)
# Case modification (bash 4+)
text="Hello World"
echo ${text^} # Hello World (first char uppercase)
echo ${text^^} # HELLO WORLD (all uppercase)
echo ${text,} # hello World (first char lowercase)
echo ${text,,} # hello world (all lowercase)

Bash provides special parameters that have special meaning.

┌─────────────────────────────────────────────────────────────────────┐
│ SPECIAL PARAMETERS │
└─────────────────────────────────────────────────────────────────────┘
┌──────────────────┬──────────────────────────────────────────────────┐
│ Parameter │ Description │
├──────────────────┼──────────────────────────────────────────────────┤
│ $0 │ Script name │
│ $1-$9 │ Positional parameters (1st-9th argument) │
│ ${10}+ │ Positional parameters (10th+ arguments) │
│ $# │ Number of positional parameters │
│ $@ │ All positional parameters (as separate words) │
│ $* │ All positional parameters (as single word) │
│ $? │ Exit status of last command │
│ $$ │ Process ID of current shell │
│ $! │ Process ID of last background job │
│ $_ │ Last argument to previous command │
│ $- │ Current shell options (set flags) │
│ $BASH_VERSION │ Bash version string │
│ $BASH_SOURCE │ Source file path │
│ $FUNCNAME │ Current function name (array) │
│ $LINENO │ Current line number │
│ $SECONDS │ Seconds since shell started │
│ $RANDOM │ Random integer │
└──────────────────┴──────────────────────────────────────────────────┘
positional_params.sh
#!/usr/bin/env bash
echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "Third argument: $3"
echo "Total arguments: $#"
echo "All arguments: $@"
echo "All arguments: $*"
# Arguments from 10 onwards need braces
echo "10th argument: ${10}"
process_args.sh
#!/usr/bin/env bash
# Check minimum arguments
if [ $# -lt 2 ]; then
echo "Usage: $0 <source> <destination>"
exit 1
fi
SOURCE="$1"
DESTINATION="$2"
echo "Copying from $SOURCE to $DESTINATION"
cp -r "$SOURCE" "$DESTINATION"
echo "Done! Copied $# arguments"
at_vs_star.sh
#!/usr/bin/env bash
# Both $@ and $* expand to all arguments
# But they behave differently when quoted
# With double quotes:
# "$@" → Each argument as separate quoted string
# "$*" → All arguments as single quoted string
# Example:
set -- "arg one" "arg two" "arg three"
echo "Using \$@:"
for arg in "$@"; do
echo " [$arg]"
done
echo ""
echo "Using \$*:"
for arg in "$*"; do
echo " [$arg]"
done
# Output:
# Using $@:
# [arg one]
# [arg two]
# [arg three]
#
# Using $*:
# [arg one arg two arg three]
Terminal window
# Check exit status of last command
ls /tmp
echo "Exit status: $?" # 0 (success)
ls /nonexistent
echo "Exit status: $?" # 2 (error)
# Use in conditionals
if grep -q "pattern" file.txt; then
echo "Pattern found"
else
echo "Pattern not found"
fi
Terminal window
# Current shell process ID
echo "Current shell PID: $$"
# Useful for creating unique temporary files
temp_file="/tmp/output_$$.txt"
echo "Temp file: $temp_file"
# In scripts, this is the script's PID, not parent shell
Terminal window
# Start a background process
sleep 10 &
echo "Background job PID: $!"
# Capture the PID for later use
long_running_command &
BG_PID=$!
echo "Started job with PID: $BG_PID"
# Later, you can check if it's still running
if kill -0 $BG_PID 2>/dev/null; then
echo "Job is still running"
else
echo "Job completed"
fi

Environment variables are variables that are available to all child processes spawned by the shell. They carry information about the system and user preferences.

┌─────────────────────────────────────────────────────────────────────┐
│ ENVIRONMENT VARIABLES │
└─────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────┐
│ SYSTEM ENVIRONMENT FLOW │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ System │ ────▶│ Shell │ ────▶│ Child │ │
│ │ (init) │ │ (bash) │ │ Processes │ │
│ │ │ │ │ │ │ │
│ │ env vars │ ────▶│ + shell │ ────▶│ + inherited│ │
│ │ │ │ vars │ │ env vars │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ Child processes inherit environment but NOT shell variables │
└───────────────────────────────────────────────────────────────────────┘
Terminal window
# User information
echo "User: $USER"
echo "UID: $UID"
echo "Home: $HOME"
echo "Shell: $SHELL"
# System information
echo "Hostname: $HOSTNAME"
echo "OS: $OSTYPE"
echo "Architecture: $HOSTTYPE"
# Paths
echo "PATH: $PATH"
echo "Current dir: $PWD"
echo "Previous dir: $OLDPWD"
# Locale
echo "Language: $LANG"
echo "LC_ALL: $LC_ALL"
# Editor
echo "Editor: $EDITOR"
echo "PAGER: $PAGER"
# Locale settings
echo "Home: $HOME"
echo "Username: $LOGNAME"
Terminal window
# Export variable to make it available to child processes
export MY_VAR="value"
MY_VAR="value"
export MY_VAR
# Or combine:
export MY_VAR="value"
# Show exported variables
export -p
# Remove variable
unset MY_VAR

Difference Between Shell and Environment Variables

Section titled “Difference Between Shell and Environment Variables”
Terminal window
# Shell variable (not exported)
MY_VAR="shell only"
echo $MY_VAR
bash -c 'echo $MY_VAR' # Nothing (not inherited)
# Environment variable (exported)
export MY_VAR="environment"
echo $MY_VAR
bash -c 'echo $MY_VAR' # Outputs: environment

variable_scope.sh
#!/usr/bin/env bash
# Global variable
GLOBAL_VAR="I'm global"
test_function() {
# Access global variable
echo "Inside function, GLOBAL_VAR = $GLOBAL_VAR"
# Local variable (only exists in this function)
local LOCAL_VAR="I'm local"
echo "Inside function, LOCAL_VAR = $LOCAL_VAR"
# Modify global variable
GLOBAL_VAR="Modified global"
}
echo "Before function: GLOBAL_VAR = $GLOBAL_VAR"
test_function
echo "After function: GLOBAL_VAR = $GLOBAL_VAR"
# This will fail - LOCAL_VAR doesn't exist outside function
# echo $LOCAL_VAR # Error
pass_variables.sh
#!/usr/bin/env bash
# Pass by value (default)
greet() {
local name="$1"
echo "Hello, $name!"
name="Modified" # Only modifies local copy
}
user="Alice"
greet "$user"
echo "After function: $user" # Still "Alice"
# Pass by reference (using nameref - bash 4.3+)
modify() {
local -n ref="$1"
ref="Modified value"
}
original="Original value"
modify original
echo "$original" # Outputs: Modified value

Terminal window
# 1. Default configuration values
CONFIG_DIR="${CONFIG_DIR:-/etc/myapp}"
LOG_FILE="${LOG_FILE:-/var/log/myapp.log}"
MAX_CONNECTIONS="${MAX_CONNECTIONS:-100}"
# 2. Setting defaults for optional arguments
INPUT_FILE="${1:-input.txt}"
OUTPUT_FILE="${2:-output.txt}"
# 3. Using pattern matching for file extensions
file="document.pdf"
extension="${file##*.}" # pdf
basename="${file%.*}" # document
# 4. Safe variable access
USERNAME="${USER:-unknown}"
# Instead of: echo $USER (might be empty)
# 5. Mandatory variables check
REQUIRED_VAR="${REQUIRED_VAR:?Error: REQUIRED_VAR must be set}"
# 6. Case transformation for consistency
input="HeLLo"
normalized="${input,,}" # hello
UPPERCASE="${input^^}" # HELLO

Terminal window
# Method 1: Direct assignment
fruits=("apple" "banana" "cherry")
# Method 2: Index assignment
colors[0]="red"
colors[1]="green"
colors[2]="blue"
# Method 3: Using declare
declare -a numbers=(1 2 3 4 5)
# Method 4: Empty array
empty=()
Terminal window
fruits=("apple" "banana" "cherry" "date")
# First element
echo ${fruits[0]} # apple
# Last element
echo ${fruits[-1]} # date
echo ${fruits[-2]} # cherry
# All elements
echo ${fruits[@]} # apple banana cherry date
echo ${fruits[*]} # apple banana cherry date
# Number of elements
echo ${#fruits[@]} # 4
# Length of specific element
echo ${#fruits[0]} # 5 (length of "apple")
Terminal window
fruits=("apple" "banana" "cherry")
# Add element
fruits+=("date")
# or
fruits[4]="elderberry"
# Remove element
unset fruits[1] # Remove second element
fruits=("${fruits[@]}") # Reindex
# Slice array
echo ${fruits[@]:1:2} # banana cherry (from index 1, length 2)
# Search in array
fruits=("apple" "banana" "cherry")
echo "${fruits[@]}" | grep -q "banana" && echo "Found"
# Loop through array
for fruit in "${fruits[@]}"; do
echo "Fruit: $fruit"
done

Terminal window
# Must declare before use (bash 4+)
declare -A user_info
# Assign values
user_info["name"]="John Doe"
user_info["email"]="john@example.com"
user_info["role"]="admin"
user_info["active"]="true"
# Or initialize all at once
declare -A config=(
[host]="localhost"
[port]="8080"
[database]="mydb"
[debug]="true"
)
Terminal window
declare -A user_info
user_info["name"]="John"
user_info["email"]="john@example.com"
# Single element
echo ${user_info["name"]} # John
# All keys
echo ${!user_info[@]} # name email
# All values
echo ${user_info[@]} # John john@example.com
# Number of elements
echo ${#user_info[@]} # 2
# Loop through keys and values
for key in "${!user_info[@]}"; do
echo "$key = ${user_info[$key]}"
done
config_example.sh
#!/usr/bin/env bash
declare -A CONFIG
# Database configuration
CONFIG[db_host]="localhost"
CONFIG[db_port]="5432"
CONFIG[db_name]="production"
CONFIG[db_user]="appuser"
# Application configuration
CONFIG[app_port]="8080"
CONFIG[log_level]="info"
CONFIG[max_connections]="100"
# Function to get config value
get_config() {
local key="$1"
echo "${CONFIG[$key]:-}"
}
# Usage
echo "Database: ${CONFIG[db_host]}:${CONFIG[db_port]}/${CONFIG[db_name]}"
echo "Log level: $(get_config log_level)"

Terminal window
# Use descriptive names
# Bad:
x=10
y="hello"
fn()
# Good:
max_retries=10
user_greeting="Hello"
create_backup()
# Use UPPERCASE for constants
readonly MAX_CONNECTIONS=100
readonly DEFAULT_TIMEOUT=30
Terminal window
# Always quote variable expansions to handle spaces
name="John Smith"
echo "$name" # John Smith
echo $name # John Smith (might work but unsafe)
# Quotes prevent word splitting and glob expansion
file="my document.txt"
ls "$file" # Works
ls $file # Might fail - tries to find "my" and "document.txt"
# Use double quotes for variable expansion
# Use single quotes for literal strings
Terminal window
# Constants should be readonly
readonly APP_NAME="MyApp"
readonly LOG_DIR="/var/log/myapp"
readonly CONFIG_FILE="/etc/myapp/config.yaml"
# Try to modify - will get error
# APP_NAME="NewName" # Error: readonly variable

Terminal window
# Wrong - causes error
var = "value"
# Error: var: command not found
# Correct
var="value"
Terminal window
# Wrong
filename="my file.txt"
rm $filename
# Correct - always quote
rm "$filename"
Terminal window
# Wrong - with set -u, this causes error
echo $undefined_var
# Correct - provide default
echo "${undefined_var:-default}"
# Or check if set
if [ -z "${var+x}" ]; then
echo "Variable is not set"
fi
Terminal window
# Wrong - assigns to first element
array=value
# This creates array with one element
# Correct - use parentheses
array=(value1 value2 value3)

load_config.sh
#!/usr/bin/env bash
# Default configuration
declare -A CONFIG
CONFIG[host]="localhost"
CONFIG[port]="8080"
CONFIG[debug]="false"
# Load from file (key=value format)
load_config() {
local config_file="$1"
if [ -f "$config_file" ]; then
while IFS='=' read -r key value; do
# Skip comments and empty lines
[[ "$key" =~ ^# ]] && continue
[[ -z "$key" ]] && continue
CONFIG["$key"]="$value"
done < "$config_file"
fi
}
# Usage
load_config "/etc/myapp/config.env"
# Access config
echo "Host: ${CONFIG[host]}"
echo "Port: ${CONFIG[port]}"
env_settings.sh
#!/usr/bin/env bash
# Environment-based configuration
ENVIRONMENT="${ENVIRONMENT:-development}"
case "$ENVIRONMENT" in
production)
readonly DB_HOST="prod-db.example.com"
readonly DB_PORT="5432"
readonly LOG_LEVEL="warn"
readonly MAX_CONNECTIONS="200"
;;
staging)
readonly DB_HOST="staging-db.example.com"
readonly DB_PORT="5432"
readonly LOG_LEVEL="debug"
readonly MAX_CONNECTIONS="50"
;;
development|*)
readonly DB_HOST="localhost"
readonly DB_PORT="5432"
readonly LOG_LEVEL="debug"
readonly MAX_CONNECTIONS="10"
;;
esac
echo "Environment: $ENVIRONMENT"
echo "Database: $DB_HOST:$DB_PORT"
echo "Log Level: $LOG_LEVEL"
parse_args.sh
#!/usr/bin/env bash
# Default values
VERBOSE=false
FORCE=false
OUTPUT_FILE=""
# Parse arguments
while [ $# -gt 0 ]; do
case "$1" in
-v|--verbose)
VERBOSE=true
shift
;;
-f|--force)
FORCE=true
shift
;;
-o|--output)
OUTPUT_FILE="$2"
shift 2
;;
-h|--help)
echo "Usage: $0 [-v|--verbose] [-f|--force] [-o|--output FILE]"
exit 0
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
# Use parsed values
[ "$VERBOSE" = true ] && echo "Verbose mode enabled"
[ "$FORCE" = true ] && echo "Force mode enabled"
[ -n "$OUTPUT_FILE" ] && echo "Output file: $OUTPUT_FILE"

In this chapter, you learned:

  • ✅ Variable declaration and assignment
  • ✅ Accessing variables with $ and ${}
  • ✅ Parameter expansion for default values
  • ✅ Special parameters ($0, $1, $@, $*, $?, $$, etc.)
  • ✅ Environment variables and their scope
  • ✅ Indexed arrays and associative arrays
  • ✅ Best practices for variable naming
  • ✅ Common mistakes and how to avoid them
  • ✅ Practical examples for DevOps scripts

  1. Create a script that declares and uses several variables
  2. Practice using parameter expansion with defaults
  3. Create an array and iterate through its elements
  1. Write a configuration loader that reads key=value pairs
  2. Implement an argument parser with multiple options
  3. Create an associative array for storing server information
  1. Write a function that modifies a global variable using nameref
  2. Create a script that handles environment-specific configurations
  3. Implement a simple key-value store using associative arrays

Now that you understand variables, continue to learn about special variables and operators in the next chapters.


Previous Chapter: Basic Syntax Next Chapter: Special Variables