Variables and Environment

Summary: in this tutorial, you will learn learn to create and use bash variables, quoting rules, parameter expansion, and environment variables.

Variables and Environment

Variables are the memory of your shell—places to store data, configuration, and state. Mastering variables in Bash means understanding not just how to create and use them, but the critical nuances of quoting, expansion, and the distinction between shell variables and environment variables.

Why variables matter:

  • Configuration: Store paths, credentials, API keys, and settings
  • Automation: Calculate values, track state across script execution
  • Flexibility: Write reusable scripts that adapt to different inputs
  • Environment: Control how programs behave (PATH, EDITOR, LANG)
  • Data processing: Store and manipulate text, numbers, and collections

This tutorial covers everything from basic variable assignment through advanced parameter expansion, arrays, arithmetic, and environment management—the foundation for effective Bash scripting.

Creating and Using Variables

Variable Assignment

In Bash, creating a variable is simple—but with crucial syntax rules:

# Correct syntax (NO SPACES around =)
name="Alice"
age=30
count=0
file_path="/home/alice/documents"
today="$(date +%Y-%m-%d)"
 
# WRONG - these will all fail!
name = "Alice"      # Error: "name: command not found"
name ="Alice"       # Error: "name: command not found"
name= "Alice"       # Sets name="" then tries to run "Alice" as a command
 

🚫 No spaces around = is absolutely critical!

This is the #1 syntax error for Bash beginners. Why? Because Bash interprets spaces as command separators.

  • name="Alice" → Assignment: create variable name with value Alice
  • name = "Alice" → Command: run program name with arguments = and Alice
  • name ="Alice" → Same as above (space before = doesn't help)
  • name= "Alice" → Set name to empty string, then run Alice as a command

Remember: In Bash, variable=value (no spaces) is NOT the same as other languages where variable = value works.

Accessing Variable Values

Use $ to read a variable's value:

name="Alice"
 
# Basic access with $
echo $name                    # Alice
 
# Explicit braces with ${}
echo ${name}                  # Alice (identical to $name)
 
# Braces are required when adjacent to other text
greeting="Hello"
echo "${greeting}World"       # HelloWorld
echo "$greetingWorld"         # (empty! Bash looks for variable 'greetingWorld')
 
# In strings
echo "Welcome, $name"         # Welcome, Alice
echo "User: ${name}"          # User: Alice
 
# Concatenation
full_name="$name Smith"       # Alice Smith
path="${HOME}/documents"      # /home/alice/documents
 

When to use ${} vs $:

  • Use ${} when the variable name is adjacent to other characters
  • Use ${} for advanced parameter expansion (covered later)
  • Use simple $ for basic variable access in clear contexts
  • When in doubt, ${} is always safe

Variable Naming Rules

# Valid variable names (letters, numbers, underscores)
user_name="Alice"
count1=42
_private_var="secret"
CamelCase="valid"
ALL_CAPS="also_valid"
 
# Invalid variable names
2user="Alice"         # Can't start with number
user-name="Alice"     # Hyphens not allowed
user.name="Alice"     # Dots not allowed
user name="Alice"     # Spaces not allowed
 

Naming conventions:

  • Lowercase for local variables: file_name, user_count
  • Uppercase for environment variables: PATH, HOME, EDITOR
  • Underscores for readability: max_retry_count
  • Descriptive names: database_url not d

Unsetting Variables

Remove a variable entirely:

name="Alice"
echo $name            # Alice
 
unset name
echo $name            # (empty output)
 
# Check if variable is set
if [ -z ${name+x} ]; then
    echo "name is unset"
fi
 

Read-Only Variables (Constants)

Protect variables from modification:

readonly PI=3.14159
readonly APP_VERSION="1.0.2"
readonly CONFIG_PATH="/etc/myapp/config"
 
# Attempt to change triggers error
PI=3.0                    # Error: PI: readonly variable
unset PI                  # Error: PI: readonly variable
 
# Check if readonly
readonly -p               # List all readonly variables
 

Use cases for readonly:

  • Mathematical constants (PI, E)
  • Application version numbers
  • Critical paths that shouldn't change
  • Configuration values loaded at startup

Quoting Rules: The Most Important Bash Concept

Quoting determines how Bash interprets your text. Understanding quoting prevents 90% of common Bash bugs.

Double Quotes ("...")

Double quotes preserve literal text except for $, `, \, and !:

name="Alice"
count=5
 
# Variable expansion happens inside double quotes
echo "Hello, $name"                # Hello, Alice
echo "Count: $count"               # Count: 5
 
# Command substitution happens
echo "Today is $(date +%A)"        # Today is Monday
echo "Files: $(ls | wc -l)"        # Files: 42
 
# Arithmetic expansion happens
echo "Total: $((10 + 5))"          # Total: 15
 
# Spaces are preserved (critical!)
message="  lots    of     spaces  "
echo $message                      # lots of spaces (spaces collapsed!)
echo "$message"                    # "  lots    of     spaces  " (preserved)
 
# Wildcards (globs) are NOT expanded
echo "Files: *.txt"                # Files: *.txt (literal asterisk)
echo Files: *.txt                  # Files: file1.txt file2.txt file3.txt (expanded!)
 
# Special characters are literal
echo "Price: $10"                  # Price:  (tries to expand $1 and 0)
echo "Price: \$10"                 # Price: $10 (backslash escapes $)
 

ℹ️ Why double quotes preserve spaces

Without quotes, Bash performs word splitting on spaces, tabs, and newlines. This turns your data into separate arguments:

filename="my important document.txt"
 
# WITHOUT quotes (DANGEROUS!)
rm $filename
# Bash sees: rm my important document.txt
# Tries to delete three files: "my", "important", "document.txt"
 
# WITH quotes (SAFE!)
rm "$filename"
# Bash sees: rm "my important document.txt"
# Correctly deletes one file with spaces in the name
 

Golden rule: Always quote your variables unless you specifically need word splitting or glob expansion.

Single Quotes ('...')

Single quotes preserve everything literally. No expansion or interpretation whatsoever:

name="Alice"
 
# Variables are NOT expanded
echo 'Hello, $name'                  # Hello, $name (literal text)
 
# Commands are NOT executed
echo 'Today is $(date)'              # Today is $(date) (literal text)
 
# Backslashes are literal
echo 'Path: C:\Users\Alice'          # Path: C:\Users\Alice (no escape needed)
 
# No escape sequences
echo 'New line: \n\t'                # New line: \n\t (literal text)
 
# Wildcards are literal
echo '*.txt'                         # *.txt (literal)
 
# Perfect for literal regex patterns
grep '^[0-9]+$' file.txt             # WRONG: shell expands [0-9]
grep '^[0-9]+$' file.txt             # RIGHT: regex protected from shell
 

When to use single quotes:

  • Literal text with special characters ($, *, ?, [, ])
  • Regular expressions (protect from shell interpretation)
  • Code snippets that shouldn't be executed
  • Any text where you want NO processing whatsoever

Limitation: You cannot include a single quote inside single quotes:

# This fails:
echo 'It's a test'                   # Error: unexpected end of file
 
# Solutions:
echo 'It'\''s a test'                # Escape by ending/starting quotes
echo "It's a test"                   # Use double quotes instead
echo $'It\'s a test'                 # Use $'...' (see below)
 

No Quotes (Unquoted)

Without quotes, Bash performs word splitting and glob expansion:

# Word splitting on whitespace
files="file1.txt file2.txt file3.txt"
echo $files                          # file1.txt file2.txt file3.txt (three separate words)
 
for f in $files; do                  # Three iterations
    echo "File: $f"
done
 
# Glob expansion (wildcards)
echo *.txt                           # Expands to: file1.txt file2.txt file3.txt
echo *.log                           # Expands to matching .log files
 
# DANGER: Unquoted variables with spaces
filename="my document.txt"
rm $filename                         # Tries to delete "my" and "document.txt" separately!
rm "$filename"                       # Correct: deletes "my document.txt"
 
# DANGER: Unquoted variables with glob characters
pattern="*.txt"
echo $pattern                        # Expands to all .txt files!
echo "$pattern"                      # Prints: *.txt (literal)
 

⚠️ Unquoted variables are a major source of bugs

Common pitfalls:

# Filenames with spaces
file="my data.csv"
cat $file                            # Error: cat: my: No such file
cat "$file"                          # Correct
 
# Empty variables
config_file=""
cat $config_file                     # Error: cat: missing operand
cat "$config_file"                   # Error: cat: : No such file (better error)
 
# Variables with glob characters
search="*.log"
ls $search                           # Lists all .log files (glob expanded)
ls "$search"                         # Error: ls: *.log: No such file (literal)
 
# Variables with special characters
data="Price: $100"
echo $data                           # Price:  (tries to expand $1)
echo "$data"                         # Price: $100 (literal $)
 

Default habit: Always quote your variables ("$var") unless you specifically need word splitting or globbing.

Backslash Escaping

The backslash \ escapes the next character, making it literal:

# Escape special characters
echo "Price: \$10"                   # Price: $10
echo "She said \"hello\""            # She said "hello"
echo "Path: C:\\Windows"             # Path: C:\Windows
 
# Escape newlines (line continuation)
long_command="This is a very \
long string that spans \
multiple lines"
echo "$long_command"                 # All on one line
 
# Escape in commands
grep "pattern with \"quotes\"" file.txt
 

Dollar-Sign Quoting ($'...')

$'...' enables ANSI-C quoting with escape sequences:

# Escape sequences work
echo $'Hello\tWorld'                 # Hello    World (tab)
echo $'Line 1\nLine 2'               # Two lines
echo $'Column1\tColumn2\nRow1\tRow2' # Formatted text
 
# Include single quotes
echo $'It\'s working!'               # It's working!
 
# Special characters
echo $'Bell: \a'                     # Triggers bell/beep
echo $'Red text: \e[31mERROR\e[0m'   # ANSI color codes
 
# Unicode
echo $'Emoji: \u2764'                # ❤
echo $'Greek: \u03B1\u03B2\u03B3'    # αβγ
 

Common escape sequences:

SequenceMeaning
\nNewline
\tTab
\\Backslash
\'Single quote
\"Double quote
\aAlert (bell)
\bBackspace
\eEscape character
\rCarriage return
\uXXXXUnicode character (4 hex digits)
\UXXXXXXXXUnicode character (8 hex digits)

Quoting Summary Table

Quote TypeVariablesCommandsWildcardsEscapesSpacesUse Case
"double"✅ Expanded✅ Expanded❌ Literal✅ Some✅ PreservedDefault choice
'single'❌ Literal❌ Literal❌ Literal❌ None✅ PreservedLiteral text, regex
No quotes✅ Expanded✅ Expanded✅ Expanded❌ None❌ SplitIntentional expansion
$'ANSI'❌ Literal❌ Literal❌ Literal✅ ANSI✅ PreservedEscape sequences

💡 Golden rule of quoting

When in doubt, use double quotes around variables: "$variable" instead of $variable.

This prevents:

  • Word splitting on spaces
  • Glob expansion on *, ?, [
  • Interpretation of special characters

Only use unquoted variables when you specifically need word splitting or glob expansion—and document why!

Parameter Expansion (Variable Manipulation)

Bash has powerful built-in string manipulation through parameter expansion—operations that happen inside ${}.

Default Values and Alternatives

Provide fallback values when variables are unset or empty:

# Use default if unset or empty
echo ${name:-"Unknown"}              # "Unknown" if $name is unset/empty
echo ${count:-0}                     # 0 if $count is unset/empty
echo ${config:-"/etc/default.conf"}  # Default path
 
# Assign default if unset or empty
echo ${database:="localhost"}        # Sets $database to "localhost" if unset/empty
echo "DB: $database"                 # Variable is now set
 
# Error if unset or empty
echo ${API_KEY:?"API_KEY is required!"}  # Prints error and exits if unset
# Error: API_KEY: API_KEY is required!
 
# Use alternative if variable IS set
echo ${name:+"User: $name"}          # "User: Alice" if set, "" if unset
 

Subtle difference: : operator:

# With colon (: # treats empty string as unset
var=""
echo ${var:-"default"}               # "default" (empty treated as unset)
echo ${var-"default"}                # "" (empty is set, so use it)
 
# Practical example:
config_file="${CONFIG_FILE:-/etc/app/config.yml}"
# Uses CONFIG_FILE if set and non-empty, otherwise uses default path
 

String Length

name="Alice"
echo ${#name}                        # 5
 
sentence="The quick brown fox"
echo ${#sentence}                    # 19
 
path="/home/alice/documents/report.pdf"
echo ${#path}                        # 34
 
# Empty variable
empty=""
echo ${#empty}                       # 0
 
# Unset variable
echo ${#nonexistent}                 # 0
 

Substring Extraction

text="Hello, World!"
 
# Syntax: ${var:offset:length}
 
# From position 0, length 5
echo ${text:0:5}                     # Hello
 
# From position 7 to end
echo ${text:7}                       # World!
 
# From position 7, length 5
echo ${text:7:5}                     # World
 
# Last 6 characters (note the space before -)
echo ${text: -6}                     # World!
 
# Last 6 characters, but only take 5
echo ${text: -6:5}                   # World
 
# From end going backwards
echo ${text: -6:-1}                  # World (all but last character)
 

Practical examples:

# Extract date components
date_string="2024-01-15"
year=${date_string:0:4}              # 2024
month=${date_string:5:2}             # 01
day=${date_string:8:2}               # 15
 
# Extract filename from path
filepath="/home/alice/documents/report.pdf"
# (Better to use parameter expansion, shown next)
 
# Get file extension
filename="archive.tar.gz"
extension=${filename: -3}            # .gz
 

Pattern Removal (Prefix and Suffix)

Remove patterns from the beginning or end of strings:

filename="report.2024.01.15.tar.gz"
 
# Remove shortest match from beginning (#)
echo ${filename#*.}                  # 2024.01.15.tar.gz (removes "report.")
 
# Remove longest match from beginning (##)
echo ${filename##*.}                 # gz (removes everything up to last .)
 
# Remove shortest match from end (%)
echo ${filename%.*}                  # report.2024.01.15.tar (removes ".gz")
 
# Remove longest match from end (%%)
echo ${filename%%.*}                 # report (removes everything from first .)
 
# Practical: Extract filename and extension
path="/home/alice/documents/report.pdf"
filename=${path##*/}                 # report.pdf (remove path)
basename=${filename%.*}              # report (remove extension)
extension=${filename##*.}            # pdf (get extension)
 

💡 Memory trick for # and %

  • # removes from the beginning (left side)

    • # is on the left side of $ on the keyboard
    • Single # = shortest match (greedy)
    • Double ## = longest match (greedy)
  • % removes from the end (right side)

    • % is on the right side of $ on the keyboard
    • Single % = shortest match (non-greedy)
    • Double %% = longest match (greedy)

Mnemonic: "Hash tags at the start, percent at the end"

More practical examples:

# Remove file extension
filename="document.pdf"
echo ${filename%.*}                  # document
 
# Remove double extension
archive="backup.tar.gz"
echo ${archive%.*}                   # backup.tar
echo ${archive%%.*}                  # backup
 
# Extract URL components
url="https://www.example.com/path/to/page.html"
protocol=${url%%://*}                # https
domain=${url#*://}
domain=${domain%%/*}                 # www.example.com
path=${url#*://*/}                   # path/to/page.html
 
# Remove leading path
fullpath="/var/log/nginx/access.log"
logfile=${fullpath##*/}              # access.log
 

Pattern Substitution (Find and Replace)

Replace text within strings:

text="Hello World Hello Bash"
 
# Replace first occurrence (/)
echo ${text/Hello/Hi}                # Hi World Hello Bash
 
# Replace all occurrences (//)
echo ${text//Hello/Hi}               # Hi World Hi Bash
 
# Replace at beginning (#)
echo ${text/#Hello/Hi}               # Hi World Hello Bash
 
# Replace at end (%)
echo ${text/%Bash/Shell}             # Hello World Hello Shell
 
# Delete a pattern (replace with nothing)
echo ${text//Hello/}                 # " World  Bash" (removes "Hello")
echo ${text// /}                     # HelloWorldHelloBash (removes spaces)
 
# Replace spaces with underscores
filename="My Important Document.txt"
slug=${filename// /_}                # My_Important_Document.txt
slug=${slug,,}                       # my_important_document.txt (lowercase)
 

Practical examples:

# Convert path separators
windows_path="C:\\Users\\Alice\\Documents"
unix_path=${windows_path//\\//}     # C:/Users/Alice/Documents
 
# Sanitize filenames
filename="Report: Q1 2024 (Final).pdf"
safe=${filename//[: ()]/}            # Wait, this doesn't work...
safe=${filename//: /_}               # Report_ Q1 2024 (Final).pdf
safe=${safe//[()]/}                  # Report_ Q1 2024 Final.pdf
 
# Remove all non-alphanumeric
text="Hello123!@#$World456"
clean=${text//[^a-zA-Z0-9]/}        # Hello123World456
 
# Replace environment variables in config
template="Database: \$DB_HOST:\$DB_PORT"
config=${template//\$DB_HOST/$DB_HOST}
config=${config//\$DB_PORT/$DB_PORT}
 

Case Conversion (Bash 4+)

text="Hello World"
 
# Uppercase all
echo ${text^^}                       # HELLO WORLD
 
# Uppercase first character only
echo ${text^}                        # Hello World
 
# Uppercase specific character
echo ${text^^o}                      # HellO WOrld
 
# Lowercase all
echo ${text,,}                       # hello world
 
# Lowercase first character only
echo ${text,}                        # hello World
 
# Lowercase specific character
echo ${text,,O}                      # hello World (no 'O' to convert)
 

Practical examples:

# Normalize input
read -p "Enter yes or no: " answer
answer=${answer,,}                   # Convert to lowercase
if [[ $answer == "yes" ]]; then
    echo "You said yes!"
fi
 
# Generate environment variable names
app_name="my-cool-app"
env_var=${app_name^^}
env_var=${env_var//-/_}              # MY_COOL_APP
 
# Title case (first letter of each word)
title="the quick brown fox"
title=${title[@]^}                   # The Quick Brown Fox
 

Environment Variables

Environment variables are special variables that are inherited by child processes. They configure system behavior and provide information about the environment.

Shell Variables vs Environment Variables

# Shell variable (local to current shell)
my_var="Hello"
bash -c 'echo $my_var'               # (empty—child shell can't see it)
 
# Environment variable (inherited by child processes)
export MY_VAR="Hello"
bash -c 'echo $MY_VAR'               # Hello
 
# Make existing variable an environment variable
database_url="localhost:5432"
export database_url
bash -c 'echo $database_url'         # localhost:5432
 

ℹ️ What export actually does

export marks a variable for automatic export to the environment of child processes. Without export, variables are local to the current shell.

Use cases for environment variables:

  • Configuration that programs need (PATH, EDITOR, LANG)
  • API keys and credentials
  • Application settings
  • Build and deployment configuration

Use cases for shell variables:

  • Temporary values within a script
  • Loop counters and internal state
  • Data that shouldn't leak to child processes

Viewing Environment Variables

# See all environment variables
env
printenv
 
# See a specific variable
echo $HOME
echo $PATH
printenv HOME
printenv PATH
 
# See all variables (shell + environment)
set
 
# See exported variables
export -p
 
# Search for specific variables
env | grep -i python
printenv | grep PATH
 

Essential Environment Variables

VariableDescriptionTypical Value
$HOMEUser's home directory/home/alice
$USERCurrent usernamealice
$LOGNAMELogin namealice
$SHELLDefault shell/bin/bash
$PATHCommand search path/usr/local/bin:/usr/bin:/bin
$PWDCurrent working directory/home/alice/projects
$OLDPWDPrevious directory/home/alice
$HOSTNAMEMachine hostnamelaptop.local
$LANGSystem language/localeen_US.UTF-8
$LC_ALLOverride all locale settingsen_US.UTF-8
$EDITORDefault text editorvim, nano, code
$VISUALVisual editor (for GUIs)code, gedit
$PAGERDefault pagerless, more
$TERMTerminal typexterm-256color
$DISPLAYX11 display:0
$PS1Primary prompt\u@\h:\w\$
$PS2Secondary prompt>
$IFSInternal Field Separator \t\n (space, tab, newline)

The PATH Variable

PATH is one of the most important environment variables—it tells Bash where to find commands:

# View your PATH
echo $PATH
# /usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin
 
# How Bash uses PATH:
# When you type "python", Bash searches each directory in order:
# 1. /usr/local/bin/python - if found, execute this
# 2. /usr/bin/python - if found, execute this
# 3. /bin/python - if found, execute this
# ... and so on
# If not found anywhere: "command not found"
 
# Add a directory to the beginning of PATH
export PATH="$HOME/.local/bin:$PATH"
# Now ~/.local/bin is searched FIRST
 
# Add to the end of PATH
export PATH="$PATH:$HOME/scripts"
# Now ~/scripts is searched LAST
 
# Remove a directory from PATH (complex)
PATH=$(echo $PATH | sed 's|:/home/alice/.local/bin||g')
# Better: just reset PATH in your shell config
 

PATH best practices:

# In ~/.bashrc or ~/.bash_profile:
 
# Add local bin directory
export PATH="$HOME/.local/bin:$PATH"
 
# Add custom scripts
export PATH="$HOME/scripts:$PATH"
 
# Check if directory exists before adding
if [ -d "$HOME/.cargo/bin" ]; then
    export PATH="$HOME/.cargo/bin:$PATH"
fi
 
# Avoid duplicates
addtopath() {
    if [[ ":$PATH:" != *":$1:"* ]]; then
        export PATH="$1:$PATH"
    fi
}
addtopath "$HOME/.local/bin"
addtopath "$HOME/scripts"
 

Setting Environment Variables

# Temporarily (current shell only)
export DATABASE_URL="postgresql://localhost/mydb"
export API_KEY="abc123xyz"
export LOG_LEVEL="debug"
 
# For one command only (doesn't affect current shell)
DATABASE_URL="test_db" ./run_tests.sh
 
# Multiple variables for one command
PORT=8080 DEBUG=true node server.js
 
# Permanently (add to ~/.bashrc or ~/.bash_profile)
echo 'export EDITOR=vim' >> ~/.bashrc
source ~/.bashrc
 
# System-wide (requires root, in /etc/environment)
sudo sh -c 'echo "JAVA_HOME=/usr/lib/jvm/java-11" >> /etc/environment'
 

Managing Configuration with .env Files

A common pattern for managing environment variables:

# Create .env file
cat > .env << 'EOF'
DATABASE_URL=postgresql://localhost/mydb
API_KEY=secret123
DEBUG=true
PORT=3000
EOF
 
# Load .env file (various methods)
 
# Method 1: source it (if it contains export statements)
export $(cat .env | xargs)
 
# Method 2: source with export
set -a  # Automatically export variables
source .env
set +a
 
# Method 3: in a script
while IFS='=' read -r key value; do
    export "$key"="$value"
done < .env
 
# Method 4: Use a function (safest)
load_env() {
    set -a
    source "$1"
    set +a
}
load_env .env
 
Was this page helpful?
SR

Written by the ShellRAG Team

The ShellRAG editorial team writes practical, beginner-friendly Bash Shell tutorials with tested code examples and real-world use cases. Every article is technically reviewed for accuracy and updated regularly.

Learn more about us →