---
id: bash-basics-2-using-variables-in-bash
slug: bash-basics-2-using-variables-in-bash
title: "Bash Basics #2: Using Variables in Bash"
excerpt: "Learn professional handling of Bash variables: declaration, assignment, string quoting, parameter expansion, arrays, environment variables, and process inheritance."
date: "2024-10-23T09:00:00+01:00"
updated: "2024-11-10T09:00:00+01:00"
author:
  name: "Sebastian Palencsar"
  handle: "spalencsar"
category: "bash-basics"
tags: ["bash", "linux", "shell", "scripting", "variables", "basics"]
reading_time: 20
toc: true
---

**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](/en/bash-basics/bash-basics-1-create-your-first-bash-shell-script){.badge-link-text}, 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`.

<blockquote class="infobox infobox--info">
💡 **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.
</blockquote>

## 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:

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

# Integers
max_attempts=5
port=8080
```

<blockquote class="infobox infobox--warn">
⚠️ **Important syntax rule:** There must **never** be spaces between the variable name, the equals sign, and the value!
</blockquote>

```bash
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 (`$`):

```bash
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}`):

```bash
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:

```markdown
┌─────────────────────────────────────────────────────────────┐
│                 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:

```bash
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
```

<blockquote class="infobox infobox--info">
💡 **Golden rule:** When accessing variables in running text and in command calls, **always wrap them in double quotes** (`"$VAR"` or `"${VAR}"`).
</blockquote>

## 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.

```markdown
┌─────────────────────────────────────────────────────────────┐
│                 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:

```bash
#!/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**:

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

```markdown
┌─────────────────────────────────────────────────────────────┐
│                 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")                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘
```

<blockquote class="infobox infobox--warn">
⚠️ **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.
</blockquote>

## 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:

```bash
#!/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!)
```

<blockquote class="infobox infobox--info">
💡 **Best practice:** When forwarding or iterating over script arguments, always use `"$@"`.
</blockquote>

## 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

```bash
# 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)

```bash
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

```bash
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:

```bash
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+)

```bash
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)

```bash
# 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:

```bash
# 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`:

```bash
# 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

<blockquote class="infobox infobox--practice">
❗ **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.
2. **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.
3. **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}`.
</blockquote>

### Sample Solution for Task 1:

```bash
#!/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](https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html){.badge-link-text} | Official GNU reference for all string and expansion operators |
| [Bash Basics #1: First Script](/en/bash-basics/bash-basics-1-create-your-first-bash-shell-script){.badge-link-text} | The first part of our basics course: Shebang, permissions, and Strict Mode |
| [Linux Command Line Processor Guide](/en/linux-beginners/command-line-processor-in-linux){.badge-link-text} | Fundamental knowledge on shells, I/O streams, and pipes |
| [chmod and File Permissions](/en/linux-beginners/basics-to-best-practices-all-about-chmod-in-linux){.badge-link-text} | 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.

<blockquote class="infobox infobox--info">
💡 **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.
</blockquote>

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](/en/bash-basics/bash-basics-control-structures-in-bash){.badge-link-text}

👉 **Course overview:** [All lessons of the Bash basics course](/en/category/bash-basics){.badge-link-text}
