Parameter Expansion and Variable Attributes ⭐⭐

Parameter Expansion and Variable Attributes ⭐⭐

Previous: 01_Shell_Fundamentals.md | Next: 03_Arrays_and_Data.md


This lesson explores bash parameter expansion, a powerful feature that allows you to manipulate variables directly in the shell without external tools. We'll cover string operations, default values, variable attributes, and practical patterns.

1. String Removal Operators

Parameter expansion provides built-in operators to remove patterns from the beginning or end of strings. These are faster than using sed or external tools.

Removal from Start (# and ##)

#!/usr/bin/env bash

# # removes shortest match from start
# ## removes longest match from start

filepath="/usr/local/bin/script.sh"

# Remove shortest match from start
echo "${filepath#*/}"      # usr/local/bin/script.sh
echo "${filepath#*/*/}"    # local/bin/script.sh

# Remove longest match from start
echo "${filepath##*/}"     # script.sh (basename)
echo "${filepath##*.}"     # sh (extension)

# Practical: extract filename from path
filename="${filepath##*/}"
echo "Filename: $filename"

# Remove path, keep filename
url="https://example.com/path/to/file.tar.gz"
file="${url##*/}"
echo "File: $file"  # file.tar.gz

Removal from End (% and %%)

#!/usr/bin/env bash

# % removes shortest match from end
# %% removes longest match from end

filepath="/usr/local/bin/script.sh"

# Remove shortest match from end
echo "${filepath%/*}"      # /usr/local/bin (dirname)
echo "${filepath%.*}"      # /usr/local/bin/script (remove extension)

# Remove longest match from end
echo "${filepath%%/*}"     # (empty, removes everything)
echo "${filepath%%.*}"     # /usr/local/bin/script

# Practical: get directory from path
directory="${filepath%/*}"
echo "Directory: $directory"

# Remove extension
filename="archive.tar.gz"
base="${filename%.*}"      # archive.tar
base="${filename%%.*}"     # archive (remove all extensions)
echo "Base: $base"

Comparison Table

Operator Direction Match Example Result
${var#pattern}` | From start | Shortest | `${path#*/} Remove first dir
${var##pattern}` | From start | Longest | `${path##*/} Basename
${var%pattern}` | From end | Shortest | `${file%.*} Remove extension
${var%%pattern}` | From end | Longest | `${file%%.*} Remove all extensions

Practical Examples

#!/usr/bin/env bash

# Extract components from URLs
url="https://user@example.com:8080/path/to/resource.html?query=1#anchor"

# Remove protocol
no_protocol="${url#*://}"
echo "No protocol: $no_protocol"
# user@example.com:8080/path/to/resource.html?query=1#anchor

# Extract domain
temp="${url#*://}"
domain="${temp%%/*}"
echo "Domain: $domain"  # user@example.com:8080

# Extract path
temp="${url#*://}"
temp="${temp#*/}"
path="/${temp%%\?*}"
echo "Path: $path"  # /path/to/resource.html

# Remove query string and anchor
clean_url="${url%%\?*}"
clean_url="${clean_url%%#*}"
echo "Clean URL: $clean_url"
# https://user@example.com:8080/path/to/resource.html

# Extract filename without extension from path
fullpath="/var/log/nginx/access.log.2024-02-13"
filename="${fullpath##*/}"      # access.log.2024-02-13
basename="${filename%%.*}"      # access
extension="${filename#*.}"      # log.2024-02-13
first_ext="${filename##*.}"     # 2024-02-13

echo "File: $filename"
echo "Base: $basename"
echo "Full ext: $extension"
echo "Last ext: $first_ext"

Batch File Processing

#!/usr/bin/env bash

# Remove extensions from multiple files
for file in *.tar.gz; do
    base="${file%.tar.gz}"
    echo "Extracting $file to $base/"
    mkdir -p "$base"
    tar xzf "$file" -C "$base"
done

# Convert file extensions
shopt -s nullglob
for file in *.jpeg; do
    newname="${file%.jpeg}.jpg"
    mv -v "$file" "$newname"
done

# Clean up numbered backups
for file in *.txt.{1..10}; do
    [ -f "$file" ] || continue
    original="${file%.*}"  # Remove .1, .2, etc.
    echo "Backup: $file -> original: $original"
done

2. Search and Replace

Parameter expansion supports pattern search and replacement, providing a lightweight alternative to sed for simple string operations.

Basic Search and Replace

#!/usr/bin/env bash

# / replaces first occurrence
# // replaces all occurrences

text="Hello World World World"

# Replace first occurrence
echo "${text/World/Bash}"     # Hello Bash World World

# Replace all occurrences
echo "${text//World/Bash}"    # Hello Bash Bash Bash

# Replace at start (prefix with #)
text="prefixSuffixprefix"
echo "${text/#prefix/START}"  # STARTSuffixprefix

# Replace at end (prefix with %)
echo "${text/%prefix/END}"    # prefixSuffixEND

# Delete pattern (replace with empty)
echo "${text//prefix/}"       # Suffix

Pattern Matching

#!/usr/bin/env bash

# Use glob patterns in search
path="/usr/local/bin:/usr/bin:/bin"

# Replace first path separator
echo "${path/:/ : }"          # /usr/local/bin : /usr/bin:/bin

# Replace all path separators
echo "${path//:/ : }"         # /usr/local/bin : /usr/bin : /bin

# Remove all digits
version="bash-5.1.16-release"
echo "${version//[0-9]/}"     # bash-..-release

# Remove all non-alphanumeric
string="Hello, World! 123"
echo "${string//[^a-zA-Z0-9]/}"  # HelloWorld123

Practical Applications

#!/usr/bin/env bash

# Sanitize filenames
sanitize_filename() {
    local filename="$1"

    # Replace spaces with underscores
    filename="${filename// /_}"

    # Remove special characters
    filename="${filename//[^a-zA-Z0-9._-]/}"

    # Replace multiple underscores with single
    while [[ "$filename" =~ __ ]]; do
        filename="${filename//__/_}"
    done

    echo "$filename"
}

echo "$(sanitize_filename 'My Document (draft).txt')"
# My_Document_draft.txt

# Convert between separators
csv_to_tsv() {
    local line="$1"
    echo "${line//,/$'\t'}"
}

tsv_to_csv() {
    local line="$1"
    echo "${line//$'\t'/,}"
}

data="name,age,city"
echo "CSV: $data"
tsv="$(csv_to_tsv "$data")"
echo "TSV: $tsv"
echo "Back to CSV: $(tsv_to_csv "$tsv")"

# URL encoding (basic)
urlencode() {
    local string="$1"
    string="${string// /%20}"
    string="${string//&/%26}"
    string="${string//=/%3D}"
    string="${string//\?/%3F}"
    echo "$string"
}

url="search?q=hello world&lang=en"
echo "Encoded: $(urlencode "$url")"
# search%3Fq%3Dhello%20world%26lang%3Den

Batch Renaming

#!/usr/bin/env bash

# Rename files by replacing patterns
batch_rename() {
    local pattern="$1"
    local replacement="$2"
    local extension="${3:-*}"

    shopt -s nullglob
    for file in *."$extension"; do
        # Skip if pattern not found
        [[ "$file" != *"$pattern"* ]] && continue

        # Generate new name
        newname="${file//$pattern/$replacement}"

        # Rename if new name is different
        if [[ "$file" != "$newname" ]]; then
            echo "Renaming: $file -> $newname"
            mv -n "$file" "$newname"
        fi
    done
}

# Usage examples:
# batch_rename "draft" "final" "txt"
# batch_rename " " "_" "md"
# batch_rename "2024" "2025" "*"

# Remove date stamps from filenames
remove_datestamp() {
    shopt -s nullglob
    for file in *_[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9].*; do
        # Remove pattern _YYYY-MM-DD
        newname="${file/_[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/}"
        echo "Renaming: $file -> $newname"
        mv -n "$file" "$newname"
    done
}

# Lowercase all extensions
lowercase_extensions() {
    shopt -s nullglob
    for file in *.*; do
        name="${file%.*}"
        ext="${file##*.}"
        newext="${ext,,}"  # Lowercase (see section 5)

        if [[ "$ext" != "$newext" ]]; then
            echo "Renaming: $file -> $name.$newext"
            mv -n "$file" "$name.$newext"
        fi
    done
}

3. Substring and Length Operations

Extract portions of strings and determine string lengths using parameter expansion.

Length of String

#!/usr/bin/env bash

# ${#var} returns string length

string="Hello, World!"
echo "Length: ${#string}"  # 13

# Length of empty string
empty=""
echo "Empty length: ${#empty}"  # 0

# Validate input length
validate_password() {
    local password="$1"
    local min_length=8
    local max_length=64

    if [ ${#password} -lt $min_length ]; then
        echo "Password too short (min: $min_length)" >&2
        return 1
    fi

    if [ ${#password} -gt $max_length ]; then
        echo "Password too long (max: $max_length)" >&2
        return 1
    fi

    echo "Password length OK: ${#password} characters"
    return 0
}

validate_password "abc"      # Too short
validate_password "SecureP@ssw0rd"  # OK

Substring Extraction

#!/usr/bin/env bash

# ${var:offset:length}
# Offset is 0-indexed
# Negative offset counts from end (bash 4.2+)

string="Hello, World!"

# Extract from position 0, length 5
echo "${string:0:5}"      # Hello

# Extract from position 7 to end
echo "${string:7}"        # World!

# Extract last 6 characters (negative offset)
echo "${string: -6}"      # World!
echo "${string:(-6)}"     # World! (alternative syntax)

# Extract 5 characters starting 6 from end
echo "${string: -6:5}"    # World

# Practical: extract date components
date="2024-02-13"
year="${date:0:4}"
month="${date:5:2}"
day="${date:8:2}"

echo "Year: $year, Month: $month, Day: $day"
# Year: 2024, Month: 02, Day: 13

# Extract time components
timestamp="2024-02-13T14:30:45Z"
time="${timestamp:11:8}"
echo "Time: $time"  # 14:30:45

# Truncate long strings
truncate() {
    local string="$1"
    local max_length="${2:-50}"

    if [ ${#string} -le $max_length ]; then
        echo "$string"
    else
        echo "${string:0:$max_length}..."
    fi
}

long_text="This is a very long string that needs to be truncated for display purposes."
echo "$(truncate "$long_text" 30)"
# This is a very long string th...

Padding Strings

#!/usr/bin/env bash

# Pad string to specific width
pad_left() {
    local string="$1"
    local width="$2"
    local padchar="${3:- }"  # Default to space

    local len=${#string}
    if [ $len -ge $width ]; then
        echo "$string"
        return
    fi

    local padding_needed=$((width - len))
    printf "%${padding_needed}s%s" "" "$string" | tr ' ' "$padchar"
}

pad_right() {
    local string="$1"
    local width="$2"
    local padchar="${3:- }"

    local len=${#string}
    if [ $len -ge $width ]; then
        echo "$string"
        return
    fi

    printf "%-${width}s" "$string" | tr ' ' "$padchar"
}

# Format table
echo "$(pad_right 'Name' 20) $(pad_left 'Value' 10)"
echo "$(pad_right '----' 20 '-') $(pad_left '-----' 10 '-')"
echo "$(pad_right 'CPU Usage' 20) $(pad_left '45%' 10)"
echo "$(pad_right 'Memory' 20) $(pad_left '2.3 GB' 10)"
echo "$(pad_right 'Disk' 20) $(pad_left '123 GB' 10)"

# Zero-pad numbers
zero_pad() {
    local number="$1"
    local width="${2:-3}"
    printf "%0${width}d" "$number"
}

echo "File_$(zero_pad 5 3).txt"     # File_005.txt
echo "File_$(zero_pad 42 4).txt"    # File_0042.txt

Practical String Processing

#!/usr/bin/env bash

# Parse log timestamps
parse_log_line() {
    local line="$1"
    # Format: [2024-02-13 14:30:45] INFO: Message

    # Extract timestamp (chars 1-19)
    local timestamp="${line:1:19}"

    # Extract level (after timestamp + 2 chars for '] ')
    local rest="${line:22}"
    local level="${rest%%:*}"

    # Extract message (after level + ': ')
    local message="${rest#*: }"

    echo "Timestamp: $timestamp"
    echo "Level: $level"
    echo "Message: $message"
}

log_line="[2024-02-13 14:30:45] ERROR: Connection timeout"
parse_log_line "$log_line"

# Credit card masking
mask_credit_card() {
    local cc="$1"
    # Show only last 4 digits
    local masked="${cc:0:${#cc}-4}"
    masked="${masked//[0-9]/X}"
    local visible="${cc: -4}"
    echo "${masked}${visible}"
}

echo "$(mask_credit_card '1234567890123456')"
# XXXXXXXXXXXX3456

# Extract domain from email
extract_domain() {
    local email="$1"
    echo "${email#*@}"
}

extract_username() {
    local email="$1"
    echo "${email%@*}"
}

email="user@example.com"
echo "Username: $(extract_username "$email")"
echo "Domain: $(extract_domain "$email")"

4. Default and Alternate Values

Parameter expansion provides operators to handle undefined or empty variables gracefully.

Default Value Operators

#!/usr/bin/env bash

# ${var:-default}  Use default if unset or empty
# ${var-default}   Use default if unset (not if empty)
# ${var:=default}  Assign and use default if unset or empty
# ${var=default}   Assign and use default if unset (not if empty)
# ${var:+alternate} Use alternate if set and not empty
# ${var+alternate}  Use alternate if set (even if empty)
# ${var:?error}    Error if unset or empty
# ${var?error}     Error if unset (not if empty)

# :- Use default value
unset name
echo "${name:-Anonymous}"  # Anonymous (name still unset)
echo "$name"               # (empty)

name=""
echo "${name:-Anonymous}"  # Anonymous (empty treated as unset)

name="John"
echo "${name:-Anonymous}"  # John

# - Use default only if truly unset
unset value
echo "${value-default}"    # default
value=""
echo "${value-default}"    # (empty string, not default)

# := Assign default value
unset port
echo "${port:=8080}"       # 8080
echo "$port"               # 8080 (now assigned)

# :+ Use alternate value if set
unset debug
echo "Debug: ${debug:+enabled}"   # Debug: (empty)

debug="1"
echo "Debug: ${debug:+enabled}"   # Debug: enabled

# :? Error if unset
check_required() {
    local file="${1:?Error: filename required}"
    echo "Processing: $file"
}

# check_required           # Error: filename required
check_required "data.txt"  # Processing: data.txt

Configuration with Defaults

#!/usr/bin/env bash

# Configuration pattern with environment variables and defaults
DB_HOST="${DB_HOST:-localhost}"
DB_PORT="${DB_PORT:-5432}"
DB_NAME="${DB_NAME:-myapp}"
DB_USER="${DB_USER:-postgres}"
DB_PASSWORD="${DB_PASSWORD:?Error: DB_PASSWORD must be set}"

LOG_LEVEL="${LOG_LEVEL:-INFO}"
LOG_FILE="${LOG_FILE:-/var/log/app.log}"
MAX_CONNECTIONS="${MAX_CONNECTIONS:-100}"
TIMEOUT="${TIMEOUT:-30}"

cat <<EOF
Database Configuration:
  Host: $DB_HOST
  Port: $DB_PORT
  Database: $DB_NAME
  User: $DB_USER

Application Settings:
  Log Level: $LOG_LEVEL
  Log File: $LOG_FILE
  Max Connections: $MAX_CONNECTIONS
  Timeout: ${TIMEOUT}s
EOF

# Connect to database
connect_db() {
    psql -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -U "$DB_USER"
}

# Usage with environment variables:
# DB_PASSWORD=secret ./script.sh
# DB_HOST=prod-db DB_PORT=5433 DB_PASSWORD=secret ./script.sh

Optional Features

#!/usr/bin/env bash

# Enable optional features based on environment
VERBOSE="${VERBOSE:-}"
DEBUG="${DEBUG:-}"
DRY_RUN="${DRY_RUN:-}"

log_verbose() {
    [ -n "$VERBOSE" ] && echo "[VERBOSE] $*" >&2
}

log_debug() {
    [ -n "$DEBUG" ] && echo "[DEBUG] $*" >&2
}

execute() {
    local cmd="$*"

    log_debug "Command: $cmd"

    if [ -n "$DRY_RUN" ]; then
        echo "[DRY-RUN] Would execute: $cmd"
        return 0
    fi

    "$@"
}

# Main logic
log_verbose "Starting process..."
log_debug "Working directory: $(pwd)"

execute rm -f temp.txt
execute mkdir -p output

log_verbose "Process complete"

# Usage:
# ./script.sh                           # Normal mode
# VERBOSE=1 ./script.sh                 # Verbose mode
# DEBUG=1 ./script.sh                   # Debug mode
# DRY_RUN=1 ./script.sh                 # Dry-run mode
# VERBOSE=1 DEBUG=1 DRY_RUN=1 ./script.sh  # All flags

Required Variables Pattern

#!/usr/bin/env bash

# Check multiple required variables
check_required() {
    local missing=()

    for var in "$@"; do
        if [ -z "${!var}" ]; then
            missing+=("$var")
        fi
    done

    if [ ${#missing[@]} -gt 0 ]; then
        echo "Error: Required variables not set:" >&2
        printf '  %s\n' "${missing[@]}" >&2
        return 1
    fi

    return 0
}

# Define required variables
REQUIRED_VARS=(
    API_KEY
    API_SECRET
    ENDPOINT_URL
)

# Check all at once
if ! check_required "${REQUIRED_VARS[@]}"; then
    echo "Please set all required environment variables" >&2
    exit 1
fi

# Or check individually with custom errors
API_KEY="${API_KEY:?Error: API_KEY is required. Get one at https://example.com/api}"
API_SECRET="${API_SECRET:?Error: API_SECRET is required}"
ENDPOINT_URL="${ENDPOINT_URL:?Error: ENDPOINT_URL is required}"

echo "Configuration valid, proceeding..."

5. Case Conversion

Bash 4.0+ provides parameter expansion operators for case conversion.

Case Conversion Operators

#!/usr/bin/env bash

# ^   Uppercase first character
# ^^  Uppercase all characters
# ,   Lowercase first character
# ,,  Lowercase all characters

string="Hello World"

# Uppercase operations
echo "${string^}"      # Hello World (first char of first word)
echo "${string^^}"     # HELLO WORLD (all chars)

# Lowercase operations
echo "${string,}"      # hello World (first char)
echo "${string,,}"     # hello world (all chars)

# Pattern-based conversion (bash 4.0+)
string="hello world"
echo "${string^^[hw]}"  # Hello World (uppercase h and w)

string="HELLO WORLD"
echo "${string,,[HW]}"  # hELLO wORLD (lowercase H and W)

Normalizing Input

#!/usr/bin/env bash

# Normalize user input to lowercase
normalize_input() {
    local input="$1"
    echo "${input,,}"
}

# Case-insensitive comparison
read -p "Continue? (yes/no): " answer
answer="$(normalize_input "$answer")"

case "$answer" in
    yes|y)
        echo "Continuing..."
        ;;
    no|n)
        echo "Aborting..."
        exit 0
        ;;
    *)
        echo "Invalid input: $answer" >&2
        exit 1
        ;;
esac

# Normalize file extensions
process_files() {
    shopt -s nullglob

    for file in *; do
        [ -f "$file" ] || continue

        # Get extension in lowercase
        ext="${file##*.}"
        ext_lower="${ext,,}"

        case "$ext_lower" in
            jpg|jpeg|png|gif)
                echo "Image: $file"
                ;;
            mp4|avi|mkv)
                echo "Video: $file"
                ;;
            txt|md|log)
                echo "Text: $file"
                ;;
            *)
                echo "Other: $file"
                ;;
        esac
    done
}

Title Case

#!/usr/bin/env bash

# Convert to title case (capitalize first letter of each word)
to_title_case() {
    local string="$1"
    local result=""
    local word

    # Convert to lowercase first
    string="${string,,}"

    # Process each word
    for word in $string; do
        # Capitalize first letter
        result+="${word^} "
    done

    # Remove trailing space
    echo "${result% }"
}

echo "$(to_title_case 'hello world from bash')"
# Hello World From Bash

echo "$(to_title_case 'THE QUICK BROWN FOX')"
# The Quick Brown Fox

# Sentence case (first letter of first word only)
to_sentence_case() {
    local string="$1"
    string="${string,,}"
    echo "${string^}"
}

echo "$(to_sentence_case 'HELLO WORLD')"
# Hello world

Practical Examples

#!/usr/bin/env bash

# Environment variable name normalization
normalize_env_var() {
    local name="$1"
    # Convert to uppercase and replace invalid chars with underscore
    name="${name^^}"
    name="${name//[^A-Z0-9_]/}"
    echo "$name"
}

echo "$(normalize_env_var 'my-app.config.port')"
# MY_APP_CONFIG_PORT

# SQL identifier quoting
quote_sql_identifier() {
    local identifier="$1"
    # Convert to lowercase (PostgreSQL convention)
    identifier="${identifier,,}"
    echo "\"$identifier\""
}

echo "SELECT * FROM $(quote_sql_identifier 'UserData')"
# SELECT * FROM "userdata"

# HTTP header normalization
normalize_http_header() {
    local header="$1"
    # Title-Case-With-Dashes

    local IFS='-'
    local words=($header)
    local result=""

    for word in "${words[@]}"; do
        word="${word,,}"
        result+="${word^}-"
    done

    echo "${result%-}"
}

echo "$(normalize_http_header 'content-type')"      # Content-Type
echo "$(normalize_http_header 'X-REQUEST-ID')"      # X-Request-Id
echo "$(normalize_http_header 'accept-encoding')"   # Accept-Encoding

6. Indirect References

Indirect references allow you to use the value of one variable as the name of another variable.

Basic Indirect Reference

#!/usr/bin/env bash

# ${!var} - indirect expansion

# Direct access
name="John"
echo "$name"      # John

# Indirect access
var_name="name"
echo "${!var_name}"  # John (value of $name)

# Set value indirectly using declare
declare "$var_name=Jane"
echo "$name"      # Jane

# More complex example
DB_HOST_DEV="localhost"
DB_HOST_PROD="db.example.com"
DB_HOST_STAGING="staging-db.example.com"

environment="PROD"
var_name="DB_HOST_${environment}"
echo "Connecting to: ${!var_name}"  # db.example.com

Variable Name Expansion

#!/usr/bin/env bash

# ${!prefix*} - expands to names of variables starting with prefix
# ${!prefix@} - same but quoted (safer)

# Set multiple related variables
DB_HOST="localhost"
DB_PORT="5432"
DB_NAME="myapp"
DB_USER="admin"

# Get all DB_* variable names
echo "Database configuration variables:"
for var_name in ${!DB_*}; do
    echo "  $var_name = ${!var_name}"
done

# Output:
# Database configuration variables:
#   DB_HOST = localhost
#   DB_NAME = myapp
#   DB_PORT = 5432
#   DB_USER = admin

# Safer version with @ (properly quoted)
for var_name in "${!DB_@}"; do
    echo "  $var_name = ${!var_name}"
done

Dynamic Configuration

#!/usr/bin/env bash

# Load configuration for specific environment
load_config() {
    local env="${1:-dev}"
    env="${env^^}"  # Uppercase

    # Define configs for each environment
    CONFIG_HOST_DEV="localhost"
    CONFIG_PORT_DEV="8080"
    CONFIG_DB_DEV="dev_database"

    CONFIG_HOST_PROD="prod.example.com"
    CONFIG_PORT_PROD="443"
    CONFIG_DB_PROD="prod_database"

    CONFIG_HOST_STAGING="staging.example.com"
    CONFIG_PORT_STAGING="8443"
    CONFIG_DB_STAGING="staging_database"

    # Load variables for selected environment
    for var in ${!CONFIG_*}; do
        if [[ "$var" == *_${env} ]]; then
            # Extract base name (CONFIG_HOST_DEV -> CONFIG_HOST)
            local base_name="${var%_*}"
            # Set variable without environment suffix
            declare -g "$base_name=${!var}"
        fi
    done
}

# Load production config
load_config "prod"

echo "Host: $CONFIG_HOST"
echo "Port: $CONFIG_PORT"
echo "Database: $CONFIG_DB"

# Load dev config
load_config "dev"

echo "Host: $CONFIG_HOST"
echo "Port: $CONFIG_PORT"
echo "Database: $CONFIG_DB"

Feature Flags

#!/usr/bin/env bash

# Feature flag system
FEATURE_NEW_UI="enabled"
FEATURE_BETA_API="disabled"
FEATURE_ANALYTICS="enabled"
FEATURE_DARK_MODE="enabled"

is_feature_enabled() {
    local feature="$1"
    local var_name="FEATURE_${feature^^}"
    local status="${!var_name:-disabled}"

    [ "$status" = "enabled" ]
}

# Usage
if is_feature_enabled "new_ui"; then
    echo "Loading new UI..."
else
    echo "Loading classic UI..."
fi

if is_feature_enabled "dark_mode"; then
    echo "Dark mode: ON"
fi

# List all features
echo "Feature flags:"
for var in ${!FEATURE_*}; do
    feature="${var#FEATURE_}"
    status="${!var}"
    echo "  $feature: $status"
done

Multi-Environment Secrets

#!/usr/bin/env bash

# Store secrets for multiple environments
SECRET_API_KEY_DEV="dev-key-12345"
SECRET_API_KEY_STAGING="staging-key-67890"
SECRET_API_KEY_PROD="prod-key-abcdef"

SECRET_DB_PASSWORD_DEV="dev-password"
SECRET_DB_PASSWORD_STAGING="staging-password"
SECRET_DB_PASSWORD_PROD="prod-password"

get_secret() {
    local secret_name="$1"
    local environment="${2:-dev}"
    environment="${environment^^}"

    local var_name="SECRET_${secret_name^^}_${environment}"
    local secret="${!var_name}"

    if [ -z "$secret" ]; then
        echo "Error: Secret $secret_name not found for $environment" >&2
        return 1
    fi

    echo "$secret"
}

# Usage
ENVIRONMENT="prod"
API_KEY="$(get_secret "api_key" "$ENVIRONMENT")"
DB_PASSWORD="$(get_secret "db_password" "$ENVIRONMENT")"

echo "API Key: ${API_KEY:0:10}..." # Show only first 10 chars
echo "Password: ${DB_PASSWORD:0:5}..."

7. declare Builtin and Variable Attributes

The declare builtin sets variable attributes and controls variable behavior.

declare Flags

#!/usr/bin/env bash

# declare -flag variable=value

# -i: Integer attribute
declare -i count=0
count=count+1        # Arithmetic without $(( ))
echo "$count"        # 1

count="5 + 3"
echo "$count"        # 8 (evaluated as arithmetic)

count="hello"        # Invalid, becomes 0
echo "$count"        # 0

# -r: Readonly (const)
declare -r PI=3.14159
# PI=3.14  # Error: PI: readonly variable

# -l: Lowercase
declare -l lowercase="HELLO WORLD"
echo "$lowercase"    # hello world
lowercase="MIXED CaSe"
echo "$lowercase"    # mixed case

# -u: Uppercase
declare -u uppercase="hello world"
echo "$uppercase"    # HELLO WORLD
uppercase="Mixed CaSe"
echo "$uppercase"    # MIXED CASE

# -n: Nameref (reference to another variable)
name="John"
declare -n name_ref=name
echo "$name_ref"     # John
name_ref="Jane"
echo "$name"         # Jane

# -a: Indexed array
declare -a array=(one two three)
echo "${array[0]}"   # one

# -A: Associative array
declare -A config=([host]=localhost [port]=8080)
echo "${config[host]}"  # localhost

# -x: Export (make available to child processes)
declare -x ENVIRONMENT="production"
bash -c 'echo $ENVIRONMENT'  # production

# -p: Print variable definition
declare -p PI
# declare -r PI="3.14159"

Declare Flags Comparison

Flag Description Example
-i Integer declare -i count=5
-r Readonly declare -r CONST=100
-l Lowercase declare -l name="JOHN"
-u Uppercase declare -u name="john"
-n Nameref declare -n ref=var
-a Indexed array declare -a arr=(1 2 3)
-A Associative array declare -A map=([key]=val)
-x Export declare -x VAR=value
-g Global (in function) declare -g GLOBAL=1
-p Print declaration declare -p VAR
-f Function names declare -f func_name
-F Function names only declare -F

Integer Variables

#!/usr/bin/env bash

# Integer variables auto-evaluate arithmetic
declare -i counter=0
declare -i result

# Arithmetic without $(( ))
counter+=1
echo "Counter: $counter"  # 1

counter=counter+10
echo "Counter: $counter"  # 11

# Expressions evaluated
result=5*3+2
echo "Result: $result"    # 17

# Division
result=20/3
echo "Result: $result"    # 6 (integer division)

# Use in loops
declare -i i
for i in {1..5}; do
    echo "i = $i, i*2 = $((i*2))"
done

# Automatic base conversion
declare -i hex
hex=0xff
echo "$hex"  # 255

declare -i octal
octal=0755
echo "$octal"  # 493

Readonly Variables

#!/usr/bin/env bash

# Readonly variables (constants)
declare -r APP_NAME="MyApp"
declare -r VERSION="1.0.0"
declare -r AUTHOR="Your Name"

# Multiple readonly declarations
declare -r \
    MAX_CONNECTIONS=100 \
    TIMEOUT=30 \
    RETRY_COUNT=3

echo "$APP_NAME v$VERSION by $AUTHOR"

# Check if variable is readonly
if declare -p APP_NAME 2>/dev/null | grep -q 'declare -r'; then
    echo "APP_NAME is readonly"
fi

# Attempt to modify causes error
# APP_NAME="NewName"  # bash: APP_NAME: readonly variable

# Make existing variable readonly
config_file="/etc/app/config.yml"
readonly config_file
# config_file="/tmp/config"  # Error: readonly variable

Case Conversion Attributes

#!/usr/bin/env bash

# Automatic lowercase
declare -l email
email="User@EXAMPLE.COM"
echo "$email"  # user@example.com

# Automatic uppercase
declare -u env_name
env_name="production"
echo "$env_name"  # PRODUCTION

# Use in functions for input normalization
process_input() {
    declare -l normalized="$1"

    case "$normalized" in
        yes|y|true|1)
            return 0
            ;;
        no|n|false|0)
            return 1
            ;;
        *)
            echo "Invalid input: $1" >&2
            return 2
            ;;
    esac
}

process_input "YES" && echo "Confirmed"
process_input "No" && echo "This won't print"

Nameref Variables

#!/usr/bin/env bash

# Nameref: reference to another variable
original="Hello"
declare -n reference=original

echo "$reference"    # Hello
reference="World"
echo "$original"     # World

# Use in functions to modify caller's variables
increment_var() {
    local -n var_ref=$1
    var_ref=$((var_ref + 1))
}

counter=10
increment_var counter
echo "$counter"  # 11

# Swap variables
swap() {
    local -n a=$1
    local -n b=$2
    local temp="$a"
    a="$b"
    b="$temp"
}

x=5
y=10
echo "Before: x=$x, y=$y"
swap x y
echo "After: x=$x, y=$y"
# Before: x=5, y=10
# After: x=10, y=5

# Pass arrays by reference
sum_array() {
    local -n arr=$1
    local sum=0
    local element

    for element in "${arr[@]}"; do
        sum=$((sum + element))
    done

    echo "$sum"
}

numbers=(1 2 3 4 5)
result=$(sum_array numbers)
echo "Sum: $result"  # 15

Inspecting Variables

#!/usr/bin/env bash

# Print variable declarations
declare -i count=5
declare -r VERSION="1.0"
declare -a files=(a.txt b.txt)
declare -A config=([host]=localhost)

# Print specific variable
declare -p count
# declare -i count="5"

declare -p VERSION
# declare -r VERSION="1.0"

# Print all variables
declare -p | head -20

# Print only exported variables
declare -px

# Print all functions
declare -F

# Print specific function
declare -f sum_array

# Check variable attributes
is_readonly() {
    declare -p "$1" 2>/dev/null | grep -q 'declare -r'
}

is_integer() {
    declare -p "$1" 2>/dev/null | grep -q 'declare -i'
}

is_readonly "VERSION" && echo "VERSION is readonly"
is_integer "count" && echo "count is integer"

8. Real-World Patterns

URL Parser

#!/usr/bin/env bash

# Complete URL parser using parameter expansion
parse_url() {
    local url="$1"

    # Extract protocol
    local protocol="${url%%://*}"
    local rest="${url#*://}"

    # Extract credentials if present
    local credentials=""
    local user=""
    local password=""
    if [[ "$rest" == *@* ]]; then
        credentials="${rest%%@*}"
        rest="${rest#*@}"
        user="${credentials%%:*}"
        password="${credentials#*:}"
    fi

    # Extract host and port
    local host_port="${rest%%/*}"
    local host="${host_port%%:*}"
    local port="${host_port#*:}"
    [ "$port" = "$host" ] && port=""  # No port specified

    # Extract path
    local path_query="${rest#*/}"
    [ "$path_query" = "$rest" ] && path_query=""  # No path
    local path="/${path_query%%\?*}"
    [ "$path" = "/$path_query" ] && path="/${path_query%%#*}"

    # Extract query string
    local query=""
    if [[ "$path_query" == *\?* ]]; then
        query="${path_query#*\?}"
        query="${query%%#*}"
    fi

    # Extract fragment
    local fragment=""
    if [[ "$url" == *#* ]]; then
        fragment="${url##*#}"
    fi

    # Print results
    echo "URL: $url"
    echo "  Protocol: $protocol"
    [ -n "$user" ] && echo "  User: $user"
    [ -n "$password" ] && echo "  Password: ${password:0:3}***"
    echo "  Host: $host"
    [ -n "$port" ] && echo "  Port: $port"
    echo "  Path: $path"
    [ -n "$query" ] && echo "  Query: $query"
    [ -n "$fragment" ] && echo "  Fragment: $fragment"
}

parse_url "https://user:pass@example.com:8080/path/to/resource.html?key=value&foo=bar#section"

Config File Parser

#!/usr/bin/env bash

# Parse key=value config files
parse_config() {
    local config_file="$1"
    local -n config_array=$2

    [ -f "$config_file" ] || return 1

    local line key value
    while IFS= read -r line; do
        # Skip empty lines and comments
        [[ "$line" =~ ^[[:space:]]*$ ]] && continue
        [[ "$line" =~ ^[[:space:]]*# ]] && continue

        # Remove inline comments
        line="${line%%#*}"

        # Trim whitespace
        line="${line#"${line%%[![:space:]]*}"}"
        line="${line%"${line##*[![:space:]]}"}"

        # Parse key=value
        key="${line%%=*}"
        value="${line#*=}"

        # Trim key and value
        key="${key#"${key%%[![:space:]]*}"}"
        key="${key%"${key##*[![:space:]]}"}"
        value="${value#"${value%%[![:space:]]*}"}"
        value="${value%"${value##*[![:space:]]}"}"

        # Remove quotes from value
        if [[ "$value" =~ ^\".*\"$ ]] || [[ "$value" =~ ^\'.*\'$ ]]; then
            value="${value:1:-1}"
        fi

        # Store in associative array
        config_array["$key"]="$value"
    done < "$config_file"
}

# Usage
declare -A config

cat > test_config.conf <<'EOF'
# Database configuration
db_host = localhost
db_port = 5432
db_name = "myapp"

# Application settings
app_name = My Application
log_level = INFO  # inline comment
timeout = 30
EOF

parse_config "test_config.conf" config

echo "Configuration loaded:"
for key in "${!config[@]}"; do
    echo "  $key = ${config[$key]}"
done

rm test_config.conf

Path Manipulation Toolkit

#!/usr/bin/env bash

# Complete path manipulation library

# Get absolute path
abspath() {
    local path="$1"

    if [[ "$path" = /* ]]; then
        echo "$path"
    else
        echo "$(pwd)/$path"
    fi
}

# Get directory name (like dirname)
dirname() {
    local path="$1"

    # Remove trailing slashes
    path="${path%/}"

    # Get directory part
    local dir="${path%/*}"

    # If no directory part, return .
    [ "$dir" = "$path" ] && dir="."

    echo "$dir"
}

# Get filename (like basename)
basename() {
    local path="$1"
    local suffix="$2"

    # Remove trailing slashes
    path="${path%/}"

    # Get filename
    local name="${path##*/}"

    # Remove suffix if provided
    if [ -n "$suffix" ]; then
        name="${name%$suffix}"
    fi

    echo "$name"
}

# Get file extension
get_extension() {
    local path="$1"
    local name="${path##*/}"

    # No extension if no dot or starts with dot
    [[ "$name" != *.* ]] && return
    [[ "$name" = .* ]] && return

    echo "${name##*.}"
}

# Remove extension
remove_extension() {
    local path="$1"
    echo "${path%.*}"
}

# Replace extension
replace_extension() {
    local path="$1"
    local new_ext="$2"
    local base="${path%.*}"
    echo "${base}.${new_ext}"
}

# Join paths
join_path() {
    local IFS='/'
    local joined="$*"

    # Remove duplicate slashes
    while [[ "$joined" =~ // ]]; do
        joined="${joined//\/\//\/}"
    done

    echo "$joined"
}

# Normalize path (remove ./ and ../)
normalize_path() {
    local path="$1"
    local IFS='/'
    local parts=($path)
    local result=()

    for part in "${parts[@]}"; do
        case "$part" in
            .|'')
                continue
                ;;
            ..)
                [ ${#result[@]} -gt 0 ] && unset 'result[-1]'
                ;;
            *)
                result+=("$part")
                ;;
        esac
    done

    local normalized="${result[*]}"
    [ "${path:0:1}" = / ] && normalized="/$normalized"
    echo "$normalized"
}

# Test the toolkit
echo "=== Path Manipulation Toolkit ==="
path="/usr/local/bin/script.sh"

echo "Original: $path"
echo "Directory: $(dirname "$path")"
echo "Filename: $(basename "$path")"
echo "Extension: $(get_extension "$path")"
echo "Without ext: $(remove_extension "$path")"
echo "Replace ext: $(replace_extension "$path" "bash")"
echo "Join: $(join_path "/usr" "local" "bin" "test.sh")"
echo "Normalize: $(normalize_path "/usr/./local/../local/bin//script.sh")"

Practice Problems

Problem 1: Advanced Filename Processor

Write a script that processes filenames with the following features: - Extract date stamps in various formats (YYYYMMDD, YYYY-MM-DD, YYYY_MM_DD) - Remove or normalize date stamps - Extract version numbers (v1.0, version-2.3.4, etc.) - Sanitize filenames (remove special characters, normalize spaces) - Generate new filenames with current date/version

Example:

process_filename "report_20240213_v1.3.pdf"
# Date: 2024-02-13
# Version: 1.3
# Base: report
# Extension: pdf
# Normalized: report_2024-02-13_v1.3.pdf

Problem 2: Environment Variable Validator

Create a script that validates environment variables: - Check required variables are set and non-empty - Validate variable types (integer, boolean, enum) - Check value ranges and constraints - Provide detailed error messages with suggestions - Support loading from .env files

Example:

validate_env PORT        --type int --range 1024-65535 --required
validate_env DEBUG       --type bool --default false
validate_env ENVIRONMENT --type enum --values "dev,staging,prod" --required
validate_env API_KEY     --type string --min-length 32 --required

Problem 3: Advanced Config File Manager

Write a config file manager that: - Reads INI-style config files with sections - Supports multiple data types (string, int, bool, array) - Allows nested sections (section.subsection.key) - Provides get/set operations - Validates values against schema - Merges multiple config files

Example config:

[database]
host = localhost
port = 5432
databases = ["app", "cache", "logs"]

[database.pool]
min_size = 5
max_size = 20

Problem 4: String Template Engine

Create a simple template engine that: - Replaces variables: Hello {{name}}! - Supports defaults: {{var:default}} - Conditional sections: {{#if var}}...{{/if}} - Loops: {{#each items}}...{{/each}} - Filters: {{name | uppercase}} - Escaping: {{!raw}}

Example:

template='Hello {{name:Guest}}! {{#if premium}}Welcome premium member{{/if}}'
render "$template" name="John" premium=true
# Output: Hello John! Welcome premium member

Problem 5: Path Resolution Library

Implement a path resolution library with: - Resolve relative paths to absolute - Handle symbolic links - Detect circular symlinks - Find file in PATH - Resolve paths with variables ($HOME, ~, etc.) - Cross-platform support (handle Windows paths) - Safety checks (directory traversal prevention)

Example:

resolve_path "~/Documents/../Downloads/./file.txt"
# /home/user/Downloads/file.txt

find_in_path "python3"
# /usr/bin/python3

is_subpath "/var/www/html" "/var/www/html/../../../../etc/passwd"
# Error: Directory traversal detected

Previous: 01_Shell_Fundamentals.md | Next: 03_Arrays_and_Data.md

to navigate between lessons