---
title: "Essential Bash Variables for Every Script"
description: "Master the most useful Bash special parameters and environment variables. Learn how to use $0, $?, $@, $UID, $EUID, and XDG variables to write more robust and portable scripts."
author: "Mohammad Abu Mattar"
canonical: https://mkabumattar.com/codesnippets/post/essential-bash-variables
---

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

```shell title="get_script_path.sh" showLineNumbers
#!/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 <

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.

```shell title="check_exit_status.sh" showLineNumbers
#!/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
```

<br />

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:

```shell
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.

```shell title="handle_arguments.sh" showLineNumbers
#!/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
```

<br />

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.

```shell title="check_user_permissions.sh" showLineNumbers
#!/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
```

<br />

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.

```shell title="use_xdg_paths.sh" showLineNumbers
#!/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"

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

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.

```shell title="production_script.sh" showLineNumbers
#!/usr/bin/env bash

# Strict mode: exit on errors, undefined variables, and pipe failures
set -euo pipefail

# Set up XDG directories with sensible defaults

# 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

| Variable               | Description                 | Example                          |
| ---------------------- | --------------------------- | -------------------------------- |
| `$0` or `$BASH_SOURCE` | Script path                 | `/home/user/script.sh`           |
| `$(basename $0)`       | Script name only            | `script.sh`                      |
| `$?`                   | Exit status of last command | `0` (success) or `1-255` (error) |
| `$1`, `$2`, `$3`...    | Positional parameters       | First, second, third argument    |
| `$@`                   | All arguments as array      | Each argument separate           |
| `$*`                   | All arguments as string     | All arguments in one string      |
| `${#@}`                | Number of arguments         | `3`                              |
| `$UID`                 | Real user ID                | `1000`                           |
| `$EUID`                | Effective user ID           | `0` (when using sudo)            |
| `$USER`                | Username                    | `john`                           |
| `$HOME`                | Home directory              | `/home/john`                     |
| `$XDG_CONFIG_HOME`     | Config directory            | `~/.config`                      |
| `$XDG_CACHE_HOME`      | Cache directory             | `~/.cache`                       |
| `$XDG_DATA_HOME`       | Data 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**

- [GNU Bash Manual - Special Parameters](https://www.gnu.org/software/bash/manual/html_node/Special-Parameters.html)
- [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir/latest/)
- [Advanced Bash-Scripting Guide](https://tldp.org/LDP/abs/html/)
