Shell Scripting Bash Environment Basics May 2026

Shell Scripting Special Variables & Environment

Master every special bash variable — $0 through $@, $?, $$, $!, $IFS — and understand how to read, modify, and export the shell environment. These are used in virtually every production script.

Bash provides a set of built-in special variables that give you information about the script itself, its arguments, its process, and the result of the last command. Knowing these fluently is what separates beginner scripts from production-grade ones.

Positional parameters hold the arguments passed to the script or function. $0 is always the script name, $1 through $9 are the first nine arguments, and ${10} onwards need braces.

BASH
#!/usr/bin/env bash
# positional.sh — Demonstrate positional parameters

echo "Script name  : $0"
echo "Script name  : $(basename "$0")"   # Just the filename
echo "First arg    : $1"
echo "Second arg   : $2"
echo "Tenth arg    : ${10}"              # Braces required for 2+ digits
echo "Arg count    : $#"               # Number of arguments
echo "All args (@) : $@"               # All args, individually quoted
echo "All args (*) : $*"               # All args, as single string

# Loop over all arguments safely — use "$@" not "$*"
echo "Processing all arguments:"
for arg in "$@"; do
  echo "  → ${arg}"
done
bash — positional.sh
vriddh@prod-01:~/scripts$./positional.sh alpha beta "with space" delta
Script name : ./positional.sh
Script name : positional.sh
First arg : alpha
Second arg : beta
Arg count : 4
All args (@) : alpha beta with space delta
Processing all arguments:
→ alpha
→ beta
with space
→ delta
💡 $@ vs $* — Always use "$@" in loops, never "$*". With "$@", arguments containing spaces are kept as separate quoted items. With "$*", all arguments collapse into one string separated by the first character of $IFS.

$? holds the exit code of the most recently executed command. 0 means success. Any non-zero value means failure. This is how scripts communicate success or failure to each other and to cron, CI/CD, and monitoring systems.

BASH
# Check exit status after every important command
ls /etc/passwd
echo "Exit code: $?"     # 0 — success

ls /nonexistent/path
echo "Exit code: $?"     # 2 — failure (file not found)

# Use in conditionals
ping -c1 -W1 8.8.8.8 > /dev/null 2>&1
if [[ $? -eq 0 ]]; then
  echo "Network is reachable"
else
  echo "Network is unreachable"
fi

# More idiomatic — check directly in if
if ping -c1 -W1 8.8.8.8 > /dev/null 2>&1; then
  echo "Network OK"
fi

# Exit with a specific code
validate_input() {
  [[ -z "${1}" ]] && return 1    # Return 1 for failure
  return 0                         # Return 0 for success
}
validate_input ""
echo "Validation result: $?"     # 1 (failed)
BASH
# $$ — PID of the current script (current shell)
echo "My PID: $$"

# Used for unique temp files — prevents collisions in parallel runs
TMPFILE="/tmp/myapp_$$.tmp"
echo "data" > "${TMPFILE}"
# ... use file ...
rm -f "${TMPFILE}"

# $! — PID of the last background process
sleep 60 &
BG_PID=$!
echo "Background PID: ${BG_PID}"
wait "${BG_PID}"              # Wait for it to finish

# $_ — last argument of the previous command
mkdir -p /tmp/myproject/logs
cd $_                       # cd to /tmp/myproject/logs

# $PPID — Parent process ID
echo "Parent PID: $PPID"

# $BASHPID — PID in subshells (different from $$)
( echo "Subshell PID: $BASHPID, Parent $$: $$" )

$IFS controls how bash splits words when expanding unquoted variables and when reading with read. Its default value is space, tab, and newline. Changing it is powerful but requires care.

BASH
# Default IFS — splits on space, tab, newline
sentence="one two three"
for word in $sentence; do    # Splits on spaces
  echo "  → ${word}"
done

# Change IFS to split on colon — useful for PATH parsing
OLD_IFS="${IFS}"              # Save original
IFS=':'
for dir in $PATH; do
  echo "  PATH entry: ${dir}"
done
IFS="${OLD_IFS}"              # Always restore!

# Use IFS with read to parse CSV lines
while IFS=',' read -r name age city; do
  echo "Name=${name}, Age=${age}, City=${city}"
done < users.csv

# Best practice — use IFS=$'\n\t' at script top
# to prevent word splitting on spaces in filenames
IFS=$'\n\t'
BASH
# ── Navigation ─────────────────────────────────────────────
echo "Home dir    : $HOME"
echo "Current dir : $PWD"
echo "Previous dir: $OLDPWD"
echo "PATH        : $PATH"

# ── User info ──────────────────────────────────────────────
echo "Username    : $USER"
echo "Effective UID: $EUID"    # 0 = root
echo "Hostname    : $HOSTNAME"

# ── Shell info ─────────────────────────────────────────────
echo "Shell       : $SHELL"
echo "Bash version: $BASH_VERSION"
echo "Bash source : ${BASH_SOURCE[0]}"
echo "Line number : $LINENO"    # Current line (useful in debug)

# ── Terminal ───────────────────────────────────────────────
echo "Term type   : $TERM"
echo "Term width  : $COLUMNS"
echo "Term height : $LINES"

# ── Runtime ────────────────────────────────────────────────
echo "Seconds up  : $SECONDS"   # Seconds since shell started
echo "Random num  : $RANDOM"    # Random int 0-32767

# Check if running as root
if [[ "${EUID}" -ne 0 ]]; then
  echo "ERROR: This script must be run as root" >&2
  exit 1
fi
bash — env variables
vriddh@prod-01:~/scripts$bash env_demo.sh
Home dir : /home/vriddh
Current dir : /home/vriddh/scripts
Username : vriddh
Effective UID: 1001
Hostname : prod-01
Bash version: 5.2.15(1)-release
Seconds up : 3842
Random num : 17293
BASH
#!/usr/bin/env bash
# Using BASH_SOURCE and LINENO in error messages

die() {
  # Prints: [filename:linenum] ERROR: message
  echo "[${BASH_SOURCE[1]}:${BASH_LINENO[0]}] ERROR: $*" >&2
  exit 1
}

check_file() {
  local file="${1}"
  [[ -f "${file}" ]] || die "File not found: ${file}"
  echo "File OK: ${file}"
}

check_file "/etc/passwd"    # OK
check_file "/no/such/file"  # Fails with location

# BASH_SOURCE array — useful in sourced libraries
# BASH_SOURCE[0] = current file
# BASH_SOURCE[1] = file that sourced this one
SCRIPT_DIR=$( cd "$(dirname "${BASH_SOURCE[0]}")" && pwd )
echo "This script lives in: ${SCRIPT_DIR}"
BASH
#!/usr/bin/env bash
# show_special_vars.sh — Print all special variables

echo "═══════════ POSITIONAL ═══════════"
echo "\$0  Script name   : $0"
echo "\$1  First arg     : ${1:-'(none)'}"
echo "\$#  Arg count     : $#"
echo "\$@  All args      : $@"

echo "═══════════ PROCESS ══════════════"
echo "\$$  Current PID   : $$"
echo "\$PPID Parent PID  : $PPID"

echo "═══════════ STATUS ═══════════════"
true
echo "\$?  After true    : $?"
false
echo "\$?  After false   : $?"

echo "═══════════ ENVIRONMENT ══════════"
echo "\$USER    : $USER"
echo "\$HOME    : $HOME"
echo "\$PWD     : $PWD"
echo "\$SHELL   : $SHELL"
echo "\$RANDOM  : $RANDOM"
echo "\$SECONDS : $SECONDS"
echo "\$LINENO  : $LINENO"
Key Success (exit 0) Failure (exit non-0) PID / info
bash — show_special_vars.sh prod
vriddh@prod-01:~/scripts$./show_special_vars.sh prod
═══════════ POSITIONAL ═══════════
$0 Script name : ./show_special_vars.sh
$1 First arg : prod
$# Arg count : 1
$@ All args : prod
═══════════ PROCESS ══════════════
$$ Current PID : 18432
$PPID Parent PID : 18215
═══════════ STATUS ═══════════════
$? After true : 0
$? After false : 1
═══════════ ENVIRONMENT ══════════
$USER : vriddh
$HOME : /home/vriddh
$PWD : /home/vriddh/scripts
$SHELL : /bin/bash
$RANDOM : 24871
$SECONDS : 0
✔ Quick reference$0 script name · $1-$9 arguments · $# arg count · "$@" all args safely · $? last exit code · $$ script PID · $! last background PID · $IFS word split char · $EUID effective user ID · $SECONDS elapsed time. Memorise these — they appear in virtually every production script.