Essential Bash Variables for Every Script

Overview

Quick Tip

You know what’s worse than writing scripts? Writing scripts that break every time you move them to a different machine. Let’s fix that with some built-in Bash variables that’ll make your life way easier.

The Problem with Hard-coded Paths

We’ve all been there. You hard-code /home/john/.config in your script, and it works great on your laptop. Then you run it on the server, and boom everything breaks because the server uses a different username or directory structure. When you’ve got dozens (or hundreds) of scripts, tracking down all these hard-coded values becomes a real headache.

The Solution: Built-in Bash Variables

Bash already knows about your system. It’s got built-in variables for script paths, user IDs, home directories, and more. Instead of guessing or hard-coding, just ask Bash. Your scripts will work everywhere without modification.

Key Bash Variables Summary

TL;DR

  • Use $BASH_SOURCE (or $0) to get the script path reliably
  • Check exit codes with $? to handle errors properly
  • Access arguments with $1, $2, $@, and $*
  • Get user IDs with $UID and $EUID for permission checks
  • Use XDG variables for standard user directories instead of hard-coding paths

Special Parameters

Get the Script Path

Sometimes you need to know where your script lives. Maybe you’re building a help menu and want to show the script name, or you need to reference files relative to the script’s location. Here’s how you grab that info.

get_script_path.sh
#!/usr/bin/env bash
# $BASH_SOURCE gives you the full path to this script
SCRIPT_PATH="$BASH_SOURCE"
echo "Full path: $SCRIPT_PATH"
# Output: /home/user/scripts/my_script.sh
# Use basename to strip the directory and keep just the filename
SCRIPT_NAME="$(basename "$BASH_SOURCE")"
echo "Script name: $SCRIPT_NAME"
# Output: my_script.sh
# Here's a practical use case: building a help menu
cat <<EOF
Usage: $SCRIPT_NAME [OPTIONS]
Options:
-h, --help Show this help message
-v, --version Show version information
EOF

Tip

There’s also $0, which works similarly but has a quirk. If someone sources your script (like source my_script.sh), $0 returns β€œbash” instead of the script name. $BASH_SOURCE doesn’t have this issue, so use that if you’re writing Bash-specific code.

Check Exit Status

When a command finishes, it leaves behind an exit code kind of like a status report. If everything went fine, you get 0. If something went wrong, you get a number between 1 and 255. The variable $? holds this exit code, and you can use it to figure out what happened and respond accordingly.

check_exit_status.sh
#!/usr/bin/env bash
# Method 1: Explicit check using $?
ls /nonexistent_directory
if [[ $? -ne 0 ]]; then
echo "Error: Directory does not exist"
exit 1
fi
# Method 2: Cleaner approach test the command directly
# If ls succeeds, run the 'then' block. If it fails, run 'else'
if ls /home; then
echo "Directory exists"
else
echo "Directory does not exist"
fi
# Method 3: One-liner with logical operators
# && means "run this if the previous command succeeded"
# || means "run this if the previous command failed"
ls /home && echo "Success!" || echo "Failed!"
# Method 4: Handle specific error codes
# Some commands return different numbers for different errors
grep "pattern" file.txt
case $? in
0) echo "Pattern found";;
1) echo "Pattern not found";;
2) echo "File error or syntax problem";;
*) echo "Unknown error";;
esac

Important

Here’s the catch $? only remembers the last command. If you run another command, the old exit code is gone. So if you need it later, save it right away:

Terminal window
some_command
exit_code=$? # Save it now!
echo "Did some other stuff"
# Now we can still check the original exit code
if [[ $exit_code -ne 0 ]]; then
echo "Original command failed"
fi

Access Script Arguments

You’ll want your scripts to accept arguments things like filenames, options, or configuration values. Bash makes this pretty straightforward, though the syntax looks a bit weird at first. Let’s break it down.

handle_arguments.sh
#!/usr/bin/env bash
# Individual arguments are numbered: $1, $2, $3, etc.
echo "First argument: $1"
echo "Second argument: $2"
echo "Third argument: $3"
# Run like: ./script.sh hello world test
# Output: hello, world, test
# Count how many arguments were passed
echo "Total arguments: ${#@}"
# Loop through all arguments
# $@ treats each argument separately, which is usually what you want
echo -e "\nProcessing all arguments:"
for arg in "$@"; do
echo " - $arg"
done
# The difference between $@ and $*
# This matters when your arguments contain spaces
function demo_at {
# Each argument stays separate
printf '@: [%s]\n' "$@"
}
function demo_star {
# All arguments merge into one string
printf '*: [%s]\n' "$*"
}
echo -e "\nDifference between \$@ and \$*:"
demo_at "arg one" "arg two" # Prints: @: [arg one] and @: [arg two]
demo_star "arg one" "arg two" # Prints: *: [arg one arg two] (all together)
# Real-world example: A function that processes files
function process_files() {
# First, make sure we got at least one file
if [[ ${#@} -eq 0 ]]; then
echo "Error: No files specified"
echo "Usage: process_files file1 file2 ..."
return 1
fi
# Process each file
for file in "$@"; do
if [[ -f "$file" ]]; then
echo "Processing: $file"
# Your actual file processing logic goes here
# wc -l "$file" # Example: count lines
else
echo "Warning: File not found: $file"
fi
done
}
# You'd call it like this:
# process_files report.txt data.csv notes.md

Tip

Always use quotes around "$@". If you don’t, filenames with spaces will break. For example, without quotes, "my file.txt" becomes two separate arguments: "my" and "file.txt". With quotes, it stays as one argument.

Environment Variables

Get User ID

Sometimes you need to know who’s running your script. Maybe you’re creating user-specific temp files, or you need to check if someone’s running as root. That’s where $UID and $EUID come in.

check_user_permissions.sh
#!/usr/bin/env bash
# Root check: A common pattern for admin scripts
# Root user always has UID 0
if [[ $EUID -eq 0 ]]; then
echo "Running with root privileges"
# Safe to do system-level stuff here
else
echo "Running as regular user (UID: $UID)"
echo "Some operations may require sudo"
# Maybe prompt for sudo, or just warn and continue
fi
# Build paths specific to the current user
# This is useful for multi-user systems
USER_CACHE_DIR="/run/user/$UID/cache"
echo "User cache directory: $USER_CACHE_DIR"
# Example: /run/user/1000/cache
# Practical example: Create a temp directory that's unique per user
# This prevents conflicts when multiple users run your script
function create_user_temp() {
local temp_dir="/tmp/myapp-$UID"
if [[ ! -d "$temp_dir" ]]; then
mkdir -p "$temp_dir"
echo "Created temp directory: $temp_dir"
fi
echo "$temp_dir"
}
# Now each user gets their own isolated temp space
TEMP_DIR=$(create_user_temp)
echo "Using temp directory: $TEMP_DIR"
# User 1000: /tmp/myapp-1000
# User 1001: /tmp/myapp-1001

Note

You might be wondering about the difference between $UID and $EUID. Honestly, for most scripts, they’re the same. They only differ in weird edge cases involving sudo or special setuid programs. When in doubt, just use $EUID for permission checks it’s what matters.

Use XDG Directory Specification

Here’s a question: where should your script save config files? Or cache data? Or store downloaded files? You might think β€œjust use ~/.config” but what if a user wants to organize things differently? That’s where XDG variables come in they’re the standard way Linux systems handle user directories.

use_xdg_paths.sh
#!/usr/bin/env bash
# Set XDG variables if they're not already set
# The syntax ${VAR:=default} means "use VAR if it exists, otherwise use default"
export "${XDG_CONFIG_HOME:=$HOME/.config}" # Config files
export "${XDG_CACHE_HOME:=$HOME/.cache}" # Temporary cache data
export "${XDG_DATA_HOME:=$HOME/.local/share}" # Application data
export "${XDG_STATE_HOME:=$HOME/.local/state}" # State files (logs, history, etc.)
# Now build your app's specific directories
APP_NAME="myapp"
APP_CONFIG_DIR="$XDG_CONFIG_HOME/$APP_NAME" # ~/.config/myapp
APP_CACHE_DIR="$XDG_CACHE_HOME/$APP_NAME" # ~/.cache/myapp
APP_DATA_DIR="$XDG_DATA_HOME/$APP_NAME" # ~/.local/share/myapp
# Create these directories if they don't exist yet
function initialize_app_directories() {
mkdir -p "$APP_CONFIG_DIR"
mkdir -p "$APP_CACHE_DIR"
mkdir -p "$APP_DATA_DIR"
echo "Initialized application directories:"
echo " Config: $APP_CONFIG_DIR"
echo " Cache: $APP_CACHE_DIR"
echo " Data: $APP_DATA_DIR"
}
initialize_app_directories
# Example: Save settings to the config directory
function save_config() {
local config_file="$APP_CONFIG_DIR/settings.conf"
cat > "$config_file" <<EOF
# Application settings
debug_mode=false
log_level=info
max_connections=100
EOF
echo "Saved config to: $config_file"
}
# Example: Load settings from the config directory
function load_config() {
local config_file="$APP_CONFIG_DIR/settings.conf"
if [[ -f "$config_file" ]]; then
# Source the config file to load variables
source "$config_file"
echo "Loaded config from: $config_file"
echo "Debug mode: $debug_mode"
echo "Log level: $log_level"
else
echo "Config file not found: $config_file"
return 1
fi
}
save_config
load_config

Tip

This isn’t just about being β€œcorrect” it’s about respecting how users organize their systems. Some people put config files on a separate partition, or use custom paths for backups. By using XDG variables, your script just works in all these scenarios.

Complete Example: Production-Ready Script

Alright, let’s put it all together. Here’s what a professional script looks like when you use all these variables properly. It’s got error handling, logging, argument processing, and follows XDG standards. Feel free to use this as a template for your own scripts.

production_script.sh
#!/usr/bin/env bash
# Strict mode: exit on errors, undefined variables, and pipe failures
set -euo pipefail
# Set up XDG directories with sensible defaults
export "${XDG_CONFIG_HOME:=$HOME/.config}"
export "${XDG_CACHE_HOME:=$HOME/.cache}"
export "${XDG_DATA_HOME:=$HOME/.local/share}"
# Grab script info using the variables we learned about
SCRIPT_NAME="$(basename "$BASH_SOURCE")"
SCRIPT_DIR="$(cd "$(dirname "$BASH_SOURCE")" && pwd)"
APP_NAME="myapp"
# Build our application's directory structure
APP_CONFIG_DIR="$XDG_CONFIG_HOME/$APP_NAME"
APP_CACHE_DIR="$XDG_CACHE_HOME/$APP_NAME"
APP_LOG_FILE="$APP_CACHE_DIR/app.log"
# ANSI color codes for pretty output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # Reset to no color
# Simple logging function with timestamps
# Usage: log "INFO" "Something happened"
function log() {
local level="$1"
shift
local message="$*"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
# Print to screen AND save to log file
echo "[$timestamp] [$level] $message" | tee -a "$APP_LOG_FILE"
}
# Centralized error handling
# This logs the error and exits with a non-zero status
function error_exit() {
log "ERROR" "$*" >&2
exit 1
}
# Check if we have everything we need to run
function check_prerequisites() {
# Example: Some scripts need root privileges
if [[ $EUID -ne 0 ]]; then
error_exit "This script must be run as root (current UID: $UID)"
fi
# Verify required commands are installed
local required_commands=("curl" "jq" "aws")
for cmd in "${required_commands[@]}"; do
if ! command -v "$cmd" &> /dev/null; then
error_exit "Required command not found: $cmd. Please install it first."
fi
done
log "INFO" "All prerequisites satisfied"
}
# Set up directories and initialize the app
function initialize() {
# Create necessary directories
mkdir -p "$APP_CONFIG_DIR" "$APP_CACHE_DIR"
touch "$APP_LOG_FILE"
# Log some useful info for debugging
log "INFO" "Initialized $APP_NAME"
log "INFO" "Script: $SCRIPT_NAME (located at $SCRIPT_DIR)"
log "INFO" "User: $USER (UID: $UID)"
}
# Parse command-line arguments
function process_arguments() {
# If no arguments, show help and exit
if [[ ${#@} -eq 0 ]]; then
cat <<EOF
Usage: $SCRIPT_NAME [OPTIONS] [FILES...]
Options:
-h, --help Show this help message
-v, --verbose Enable verbose logging
-d, --debug Enable debug mode
Examples:
$SCRIPT_NAME file1.txt file2.txt
$SCRIPT_NAME --verbose *.log
EOF
exit 0
fi
# Set up some flags
local verbose=false
local debug=false
local files=()
# Loop through all arguments
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
process_arguments # Recursively call to show help
;;
-v|--verbose)
verbose=true
log "INFO" "Verbose mode enabled"
shift
;;
-d|--debug)
debug=true
set -x # This makes Bash print every command before running it
shift
;;
-*)
error_exit "Unknown option: $1"
;;
*)
# Anything that doesn't start with - is treated as a file
files+=("$1")
shift
;;
esac
done
log "INFO" "Processing ${#files[@]} file(s)"
# Process each file
for file in "${files[@]}"; do
if [[ -f "$file" ]]; then
log "INFO" "Processing file: $file"
# Your actual file processing logic would go here
# Example: wc -l "$file"
else
log "WARN" "File not found: $file"
fi
done
}
# Main entry point
function main() {
initialize
check_prerequisites || exit $?
process_arguments "$@" || exit $?
log "INFO" "Script completed successfully"
}
# This is where everything starts
# We pass all command-line arguments to main using "$@"
main "$@"

Quick Reference

VariableDescriptionExample
$0 or $BASH_SOURCEScript path/home/user/script.sh
$(basename $0)Script name onlyscript.sh
$?Exit status of last command0 (success) or 1-255 (error)
$1, $2, $3…Positional parametersFirst, second, third argument
$@All arguments as arrayEach argument separate
$*All arguments as stringAll arguments in one string
${#@}Number of arguments3
$UIDReal user ID1000
$EUIDEffective user ID0 (when using sudo)
$USERUsernamejohn
$HOMEHome directory/home/john
$XDG_CONFIG_HOMEConfig directory~/.config
$XDG_CACHE_HOMECache directory~/.cache
$XDG_DATA_HOMEData directory~/.local/share

Best Practices

Do these things:

  • Always quote your variables: Use "$var" instead of $var. This prevents weird issues when variables contain spaces or special characters.
  • Set defaults for XDG variables: Use ${XDG_CONFIG_HOME:=$HOME/.config} so your script works even if the variable isn’t set.
  • Prefer $BASH_SOURCE over $0: It’s more reliable, especially when your script might be sourced instead of executed.
  • Check exit codes for important operations: Don’t just assume things worked. Check $? or use if statements.
  • Quote "$@" when passing arguments: This preserves spaces in filenames and arguments.

Avoid these mistakes:

  • Don’t hard-code paths: Never write /home/john/.config directly. Use variables so your script works for everyone.
  • Don’t use $* by default: It merges all arguments into one string, which usually isn’t what you want. Stick with "$@".
  • Don’t assume $? sticks around: It changes with every command. Save it immediately if you need it later.
  • Don’t forget to quote path variables: Unquoted variables break when paths have spaces.
  • Don’t use $UID for permission checks: Use $EUID instead it’s what actually matters for access control.

References

Related Posts

Check out some of our other posts

Why printf Beats echo in Linux Scripts

Scripting Tip You know that feeling when a script works perfectly on your machine but fails miserably somewhere else? That's probably because you're using echo for output. Let me show you why pri

Check S3 Bucket Existence

'Cloud' 'Automation' isCodeSnippet: true draft: falseQuick Tip Don’t let your deployment blow up because of a missing S3 bucket. This Bash script lets you check if a bucket exists

Per-App Shell History for Bash

Terminal Chaos? Organize Your Bash History! Ever jumped between iTerm2, Ghostty, and VS Code's terminal only to have your command history get all mixed up? This Bash snippet keeps things clean by

Per-App Shell History for Zsh

Terminal Chaos? Organize Your Shell History! Ever jumped between iTerm2, Ghostty, and VS Code's terminal only to have your command history get all mixed up? This Zsh snippet keeps things clean by

List S3 Buckets

Overview Multi-Profile S3 Management Multi-Profile S3 Safari! Ever juggled multiple AWS accounts and needed a quick S3 bucket inventory across all of them? This Python script is your guid

AWS Secrets Manager

Need to load secrets in your Node.js app without exposing them? Here's how. If you're still storing API keys or database credentials in .env files or hardcoding them into your codebase, it's ti