Bash Basics #2: Using Variables in Bash

Learn professional handling of Bash variables: declaration, assignment, string quoting, parameter expansion, arrays, environment variables, and process inheritance.

Reading time: 20 min

Welcome to the second part of our technical series on Linux and Bash programming!

After learning about the shebang mechanism, execution permissions, and Bash Strict Mode in Bash Basics #1: Create Your First Bash Shell Script, today we focus on the heart of dynamic scripts: storing and processing variables.

Static scripts that always execute the same hardcoded paths quickly reach their limits in day-to-day administration. Only through variables do your scripts become flexible, modular, and reusable. They accept user input, store paths, count iterations, capture command output, and process dynamic data structures.

In this lesson, you will learn everything about proper declaration, clean quoting to avoid security vulnerabilities, function scopes (local), environment variables (export), special positional parameters, and powerful parameter expansions that let you manipulate strings without slow utilities like sed or awk.

πŸ’‘ Note on the Bash type system: Unlike languages such as C, Rust, or Java, Bash is weakly typed by default. Every variable is initially stored internally as a plain text string. Only during certain arithmetic operations or comparisons does the shell interpret the content contextually as an integer.

Declaration and Assignment: The Fundamentals

In Bash, you declare a variable simply by specifying its name, followed by an equals sign (=) and the value to assign:


# Strings
username="admin"
server_ip="192.168.1.50"

# Integers
max_attempts=5
port=8080

⚠️ Important syntax rule: There must never be spaces between the variable name, the equals sign, and the value!


name = "Max"   # ERROR: Bash tries to execute the command 'name' with argument '='
name= "Max"    # ERROR: Sets empty variable 'name' and executes command 'Max'
name="Max"     # CORRECT!

Naming Conventions in the Shell

  • Uppercase (BACKUP_DIR, PORT, TIMEOUT): Conventionally used for global constants, system variables, and exported environment variables.
  • Lowercase (filename, index, loop_counter): Used for internal, temporary, and local script variables.
  • Allowed characters: Letters (a-z, A-Z), digits (0-9), and the underscore (_). The name must not start with a digit.

Accessing Variables: $VAR vs. ${VAR}

To read a variable's content, prefix the name with a dollar sign ($):


echo $username
echo "Connecting to $server_ip on port $port..."

For clean and unambiguous code, you should get into the habit of wrapping variable names in curly braces (${VAR}):


log_name="backup"
# Clear separation from following text:
echo "${log_name}_2024.tar.gz"  # Output: backup_2024.tar.gz

# Without braces, the shell looks for the nonexistent variable $log_name_2024:
echo "$log_name_2024.tar.gz"   # ERROR: Outputs only '.tar.gz'!

Quoting Rules: Single Quotes vs. Double Quotes

Correct quoting is the most important security aspect when working with variables in Bash:


β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 VARIABLE QUOTING BEHAVIOR                   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                             β”‚
β”‚  1. DOUBLE QUOTES ("$VAR"):                                 β”‚
β”‚     β”œβ”€β”€ Variables ARE expanded ($VAR -> content)            β”‚
β”‚     β”œβ”€β”€ Command substitution IS executed ($(date))          β”‚
β”‚     └── Protects against faulty word splitting on spaces!   β”‚
β”‚                                                             β”‚
β”‚  2. SINGLE QUOTES ('$VAR'):                                 β”‚
β”‚     β”œβ”€β”€ Strictly LITERAL: No variable expansion!            β”‚
β”‚     └── Ideal for static strings, regex & SSH commands      β”‚
β”‚                                                             β”‚
β”‚  3. NO QUOTES ($VAR):                                       β”‚
β”‚     └── DANGEROUS: Causes word splitting & globbing!        β”‚
β”‚                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Practical Comparison:


project="Admin Docs"

# Double Quotes: Variable is resolved
echo "Project: $project"
# Output: Project: Admin Docs

# Single Quotes: Text stays exactly as typed
echo 'Project: $project'
# Output: Project: $project

# The danger of missing quotes with filenames containing spaces:
filename="my document.pdf"
rm $filename    # ERROR: Tries to delete 'my' and 'document.pdf' separately!
rm "$filename"  # CORRECT: Treats the content as exactly one file argument

πŸ’‘ Golden rule: When accessing variables in running text and in command calls, always wrap them in double quotes ("$VAR" or "${VAR}").

Scope: Local vs. Global Variables

By default, all variables defined in a Bash script are global. This means: once a variable is declared, it is readable and overwritable throughout the rest of the script – even if it was assigned inside a function.


β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 VARIABLE SCOPING IN FUNCTIONS                β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                             β”‚
β”‚  [ Main program / Script level ]                            β”‚
β”‚  GLOBAL_VAR="active"                                        β”‚
β”‚         β”‚                                                   β”‚
β”‚         β”œβ”€β”€ Function call: worker()                         β”‚
β”‚         β”‚     β”œβ”€β”€ local TEMP_VAR="local" (locally valid)    β”‚
β”‚         β”‚     └── GLOBAL_VAR="overwritten" (DANGER!)        β”‚
β”‚         β”‚                                                   β”‚
β”‚         β–Ό                                                   β”‚
β”‚  After the function call:                                   β”‚
β”‚  β”œβ”€β”€ TEMP_VAR is GONE again (empty)                         β”‚
β”‚  └── GLOBAL_VAR was changed globally                        β”‚
β”‚                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Clean Encapsulation with local in Functions

To prevent unintended side effects in larger administration scripts, you should always declare variables inside functions with the local keyword:


#!/usr/bin/env bash
set -euo pipefail

system_status="ONLINE"

check_memory() {
    # Local variable - exists ONLY within this function:
    local free_memory
    free_memory=$(df -h / | awk 'NR==2 {print $4}')
    
    echo "Free memory: $free_memory"
}

check_memory

# Attempt to access the local variable from outside:
# Thanks to 'set -u', the script aborts here with an error, because $freier_speicher is local!

Environment Variables and Process Inheritance (export)

Normal shell variables only exist in the memory of the current script instance. When a subprocess or another script is launched from your script, the child process does not know these variables.

With the export command, you promote a variable into the environment:


# Define and export variable
export DATABASE_URL="mysql://db.local:3306/production"
export APP_ENV="production"

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 PROCESS INHERITANCE WITH EXPORT              β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                             β”‚
β”‚  [ Parent Shell / Your Script ]                             β”‚
β”‚  β”œβ”€β”€ VAR_A="private"      (normal shell variable)           β”‚
β”‚  └── export VAR_B="shared" (exported environment variable)  β”‚
β”‚         β”‚                                                   β”‚
β”‚         β–Ό Launches child process: ./subscript.sh            β”‚
β”‚                                                             β”‚
β”‚  [ Child Process / Subshell ]                               β”‚
β”‚  β”œβ”€β”€ VAR_A is UNKNOWN                                       β”‚
β”‚  └── VAR_B is PRESENT ("shared")                            β”‚
β”‚                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

⚠️ Important one-way street: A child process inherits the exported variables of its parent shell, but can never modify them in the parent shell. Changes in the child process remain strictly isolated to that process.

Special Automatic Bash Variables

Bash provides a set of predefined, read-only variables that contain important information about the current script run:

Variable Meaning & Use Case
$0 Name or path of the currently executed script
$1, $2 .. $9 The passed positional parameters / arguments
${10}, ${11} Arguments from position 10 onwards (must be in curly braces!)
$# Number of all arguments passed to the script
"$@" All passed arguments as separate, quoted words
"$*" All arguments as a single concatenated string
$$ Process ID (PID) of the currently running script
$! Process ID of the last process started in the background
$? Exit status / return code of the immediately preceding command

The Crucial Difference: "$@" vs. "$*"

The distinction between "$@" and "$*" is one of the most common pitfalls when forwarding parameters to other commands:


#!/usr/bin/env bash
# Script invocation: ./test.sh "My Document.pdf" "Image 1.png"

# 1. Using "$@": Receives each argument individually quoted:
for file in "$@"; do
    echo "Processing file: $file"
done
# Iteration 1: My Document.pdf
# Iteration 2: Image 1.png

# 2. Using "$*": Joins all arguments into ONE string:
for file in "$*"; do
    echo "Processing file: $file"
done
# Iteration 1: My Document.pdf Image 1.png (WRONG!)

πŸ’‘ Best practice: When forwarding or iterating over script arguments, always use "$@".

Parameter Expansion: String Manipulation Without Utilities

Bash features an extremely powerful internal string manipulation mechanism called parameter expansion. This saves you from slow subprocess calls like sed, awk, or cut.

1. Fallback Values and Error Checking


# 1. Use fallback value when variable is empty or unset (${VAR:-default}):
USER="${1:-guest}"
echo "Logged in as: $USER"

# 2. Permanently assign default value to variable if unset (${VAR:=default}):
echo "Storage location: ${LOG_DIR:=/var/log/app}"

# 3. Abort script with error message if variable is missing (${VAR:?error}):
# Prevents fatal disasters like rm -rf /* !
TARGET_DIR="${1:?Error: A target directory must be specified!}"

2. String Length and Slicing (Substrings)


TEXT="Linux-Administration"

# Determine string length (${#VAR})
echo "Character count: ${#TEXT}"  # Output: 20

# Extract substring from position (${VAR:offset:length})
echo "${TEXT:0:5}"   # Output: Linux
echo "${TEXT:6}"     # Output: Administration (from index 6 to end)

3. Search and Replace


PATH="/var/www/html/app/config.php"

# Replace first occurrence (${VAR/search/replacement}):
echo "${PATH/html/public}"     # /var/www/public/app/config.php

# Replace all occurrences (${VAR//search/replacement}):
IP="192.168.1.1"
echo "${IP//./-}"              # 192-168-1-1

4. Prefix and Suffix Stripping (Separating file extensions and paths)

This is one of the most indispensable tools for automation scripts:


FILEPATH="/var/backups/webserver_database.tar.gz"

# Extract filename without directory path (remove largest prefix up to /: ##*/)
FILENAME="${FILEPATH##*/}"
echo "$FILENAME"  # Output: webserver_database.tar.gz

# Extract directory path (remove largest suffix from /: %/*)
DIRECTORY="${FILEPATH%/*}"
echo "$DIRECTORY"  # Output: /var/backups

# Remove file extension (remove shortest suffix .gz: %.gz)
echo "${FILENAME%.gz}"  # Output: webserver_database.tar

5. Case Conversion (Bash 4+)


MODE="production"
echo "${MODE^^}"  # Output: PRODUCTION (all uppercase)

TITLE="DATABASE"
echo "${TITLE,,}"  # Output: database (all lowercase)

Arrays in Bash: Lists and Key-Value Stores

Starting with Bash version 4, you have fully-fledged indexed and associative arrays at your disposal:

1. Indexed Arrays (Numbered Lists)


# Declare and populate an array
SERVER_LIST=("web01" "web02" "db01" "cache01")

# Retrieve first element (0-indexed):
echo "First server: ${SERVER_LIST[0]}"

# Output all elements (${ARRAY[@]}):
echo "All servers: ${SERVER_LIST[@]}"

# Determine element count (${#ARRAY[@]}):
echo "Server count: ${#SERVER_LIST[@]}"

# Append new element:
SERVER_LIST+=("mail01")

# Iterate over the array:
for server in "${SERVER_LIST[@]}"; do
    echo "Pinging server: $server..."
done

2. Associative Arrays (Key-Value Hashes)

Associative arrays must be explicitly declared with declare -A before use:


# Declare associative array
declare -A SERVER_PORTS

# Assign values
SERVER_PORTS["http"]=80
SERVER_PORTS["https"]=443
SERVER_PORTS["ssh"]=22

# Retrieve value by key
echo "HTTPS port is: ${SERVER_PORTS["https"]}"

# Read all keys (${!ARRAY[@]}):
for service in "${!SERVER_PORTS[@]}"; do
    echo "Service $service runs on port ${SERVER_PORTS[$service]}"
done

Constants and Typed Variables with readonly and declare

When variables need to be protected from accidental overwriting or should be typed as pure numbers, use readonly or declare:


# Declare immutable constant
readonly BACKUP_DIR="/mnt/secure_backups"
# Any subsequent assignment attempt (BACKUP_DIR="/tmp") causes the script to abort!

# Type variable as integer (declare -i)
declare -i COUNTER=10
COUNTER+=5  # Automatically performs mathematical addition (15), not string concatenation!
echo "Counter: $COUNTER"

Exercises & Practice Check

❗ Practice tasks for lesson #2:

  1. Task 1 (Path and Name Parser):

Write a script parse_file.sh that accepts a full file path (e.g., /var/log/nginx/access.log.gz) as $1 and uses parameter expansion to extract and output the directory, the pure filename, and the file extension.

  1. Task 2 (Array Server Check):

Create a script that defines an array of 3 domain names (heise.de, github.com, admindocs.de), iterates over them, and checks with ping -c 1 whether the host is reachable.

  1. Task 3 (Safe Backup Directory):

Write a script that enforces the target directory with ${1:?Error: No target path provided!} and sets a fallback username with ${2:-admin}.

Sample Solution for Task 1:


#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

INPUT_PATH="${1:?Please provide a file path as parameter!}"

DIRECTORY="${INPUT_PATH%/*}"
FULL_NAME="${INPUT_PATH##*/}"
FILE_EXT="${FULL_NAME##*.}"
BASENAME="${FULL_NAME%.*}"

echo "=== FILE ANALYSIS ==="
echo "Input path  : $INPUT_PATH"
echo "Directory   : $DIRECTORY"
echo "Filename    : $FULL_NAME"
echo "Basename    : $BASENAME"
echo "Extension   : $FILE_EXT"
echo "====================="

exit 0

Command Reference (Cheatsheet)

Syntax / Command Function & Effect
VAR="value" Assigns a value to the variable (no spaces!)
"${VAR}" Safely reads the variable's content
local VAR="x" Restricts the variable's validity to the local function
export VAR="x" Makes the variable available to launched child processes
readonly VAR="x" Marks the variable as an immutable constant
declare -A HASH Declares an associative array (key-value)
"${VAR:-default}" Uses default value if VAR is empty or unset
"${VAR:?error}" Aborts with error message if VAR is empty or unset
"${#VAR}" Returns the number of characters in the string
"${VAR##*/}" Removes the entire path up to the last slash (filename)
"${VAR%/*}" Removes the filename from the last slash onwards (directory path)
"${VAR^^}" Converts all characters to uppercase
"${VAR,,}" Converts all characters to lowercase
"$@" All script arguments as separately protected parameters
"$#" Number of parameters passed to the script

Further Resources

Resource Description
GNU Bash Parameter Expansion Official GNU reference for all string and expansion operators
Bash Basics #1: First Script The first part of our basics course: Shebang, permissions, and Strict Mode
Linux Command Line Processor Guide Fundamental knowledge on shells, I/O streams, and pipes
chmod and File Permissions Linux permission concepts in detail

Conclusion

Variables and parameter expansions are the foundation for transforming shell scripts from simple command chains into intelligent, flexible administration tools. Anyone who resolves string manipulations natively in Bash using ${VAR##*/} or ${VAR:-default} not only writes more robust scripts but also benefits from noticeably faster execution times.

πŸ’‘ Tip: Always wrap variable access in quotes ("${VAR}") and use ${VAR:?} for critical operations like rm -rf. Consistently use local within functions to rule out unnoticed global variable overwrites.

In the next lesson, we bring decision logic into our scripts and learn how to control branches and loops: πŸ‘‰ Next up: Bash Basics #3: Control Structures in Bash

πŸ‘‰ Course overview: All lessons of the Bash basics course

Share & export

Export as Markdown