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
$UIDand$EUIDfor 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.
#!/usr/bin/env bash
# $BASH_SOURCE gives you the full path to this scriptSCRIPT_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 filenameSCRIPT_NAME="$(basename "$BASH_SOURCE")"echo "Script name: $SCRIPT_NAME"# Output: my_script.sh
# Here's a practical use case: building a help menucat <<EOFUsage: $SCRIPT_NAME [OPTIONS]
Options: -h, --help Show this help message -v, --version Show version informationEOFTip
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.
#!/usr/bin/env bash
# Method 1: Explicit check using $?ls /nonexistent_directoryif [[ $? -ne 0 ]]; then echo "Error: Directory does not exist" exit 1fi
# 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 errorsgrep "pattern" file.txtcase $? in 0) echo "Pattern found";; 1) echo "Pattern not found";; 2) echo "File error or syntax problem";; *) echo "Unknown error";;esacImportant
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:
some_commandexit_code=$? # Save it now!echo "Did some other stuff"# Now we can still check the original exit codeif [[ $exit_code -ne 0 ]]; then echo "Original command failed"fiAccess 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.
#!/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 passedecho "Total arguments: ${#@}"
# Loop through all arguments# $@ treats each argument separately, which is usually what you wantecho -e "\nProcessing all arguments:"for arg in "$@"; do echo " - $arg"done
# The difference between $@ and $*# This matters when your arguments contain spacesfunction 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 filesfunction 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.mdTip
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.
#!/usr/bin/env bash
# Root check: A common pattern for admin scripts# Root user always has UID 0if [[ $EUID -eq 0 ]]; then echo "Running with root privileges" # Safe to do system-level stuff hereelse echo "Running as regular user (UID: $UID)" echo "Some operations may require sudo" # Maybe prompt for sudo, or just warn and continuefi
# Build paths specific to the current user# This is useful for multi-user systemsUSER_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 scriptfunction 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 spaceTEMP_DIR=$(create_user_temp)echo "Using temp directory: $TEMP_DIR"# User 1000: /tmp/myapp-1000# User 1001: /tmp/myapp-1001Note
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.
#!/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 filesexport "${XDG_CACHE_HOME:=$HOME/.cache}" # Temporary cache dataexport "${XDG_DATA_HOME:=$HOME/.local/share}" # Application dataexport "${XDG_STATE_HOME:=$HOME/.local/state}" # State files (logs, history, etc.)
# Now build your app's specific directoriesAPP_NAME="myapp"APP_CONFIG_DIR="$XDG_CONFIG_HOME/$APP_NAME" # ~/.config/myappAPP_CACHE_DIR="$XDG_CACHE_HOME/$APP_NAME" # ~/.cache/myappAPP_DATA_DIR="$XDG_DATA_HOME/$APP_NAME" # ~/.local/share/myapp
# Create these directories if they don't exist yetfunction 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 directoryfunction save_config() { local config_file="$APP_CONFIG_DIR/settings.conf"
cat > "$config_file" <<EOF# Application settingsdebug_mode=falselog_level=infomax_connections=100EOF
echo "Saved config to: $config_file"}
# Example: Load settings from the config directoryfunction 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_configload_configTip
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.
#!/usr/bin/env bash
# Strict mode: exit on errors, undefined variables, and pipe failuresset -euo pipefail
# Set up XDG directories with sensible defaultsexport "${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 aboutSCRIPT_NAME="$(basename "$BASH_SOURCE")"SCRIPT_DIR="$(cd "$(dirname "$BASH_SOURCE")" && pwd)"APP_NAME="myapp"
# Build our application's directory structureAPP_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 outputRED='\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 statusfunction error_exit() { log "ERROR" "$*" >&2 exit 1}
# Check if we have everything we need to runfunction 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 appfunction 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 argumentsfunction process_arguments() { # If no arguments, show help and exit if [[ ${#@} -eq 0 ]]; then cat <<EOFUsage: $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 *.logEOF 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 pointfunction 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_SOURCEover$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 useifstatements. - Quote
"$@"when passing arguments: This preserves spaces in filenames and arguments.
Avoid these mistakes:
- Donβt hard-code paths: Never write
/home/john/.configdirectly. 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
$UIDfor permission checks: Use$EUIDinstead itβs what actually matters for access control.