Home/Learn/Linux/Shell Scripting — Bash Fundamentals

Shell Scripting — Bash Fundamentals

Intermediate
Shell

Bash scripts automate repetitive tasks, deploy software, and glue tools together. Master variables, conditionals, loops, functions, and error handling to write production-grade scripts.

Overview

Bash is the default shell on most Linux systems and the de-facto language for DevOps automation. Shell scripts are executed top-to-bottom by the bash interpreter. Unlike compiled languages, bash is dynamically typed (all values are strings), weak on error handling by default (set -e helps), and glues Unix tools together via pipes and redirects. The goal of a shell script is not to rewrite Python — it is to orchestrate existing CLI tools efficiently. Understanding when to use bash vs Python is as important as knowing bash syntax.

Script Skeleton & Error Handling

Every production shell script should start with a shebang and safety flags. set -euo pipefail is the standard "strict mode" that catches common errors.

bash — script skeleton with strict mode
#!/usr/bin/env bash
# ↑ shebang: tells OS which interpreter to use (/usr/bin/env finds bash in PATH)

set -e          # exit immediately on any error (non-zero exit code)
set -u          # treat unset variables as errors (instead of empty string)
set -o pipefail # pipe fails if ANY command in the pipe fails (not just last)
# Shorthand: set -euo pipefail

# Debugging mode (print every command before executing)
set -x          # enable debug tracing
set +x          # disable debug tracing

# Script info
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPT_NAME="$(basename "$0")"

# Trap for cleanup on exit
cleanup() {
  echo "Cleaning up..."
  rm -f /tmp/my-lock-file
}
trap cleanup EXIT    # runs cleanup() on any exit (normal or error)
trap 'echo "Error at line $LINENO"' ERR  # print line number on error

echo "Script started from: $SCRIPT_DIR"

Variables, Conditionals & Loops

Bash variables are untyped strings. Conditionals use [ ] or [[ ]] (prefer [[ ]]). Loops iterate over arrays, ranges, and command output.

bash — variables, conditionals, loops
#!/usr/bin/env bash
set -euo pipefail

# Variables (no spaces around =)
NAME="Akshay"
AGE=30
GREETING="Hello, $NAME! You are $AGE."
echo "$GREETING"

# Command substitution
CURRENT_DATE=$(date +%Y-%m-%d)
FILE_COUNT=$(ls -1 /var/log/*.log | wc -l)

# Conditionals — prefer [[ ]] over [ ] (supports && || without escaping)
if [[ "$NAME" == "Akshay" ]]; then
  echo "Hello, Akshay"
elif [[ $AGE -gt 18 ]]; then   # -gt (>), -lt (<), -eq (==), -ne (!=) for numbers
  echo "Adult"
else
  echo "Other"
fi

# File tests
if [[ -f "/etc/nginx/nginx.conf" ]]; then   # -f: file exists
  echo "nginx.conf found"
fi
# -d: directory  -e: exists  -r: readable  -w: writable  -x: executable
# -z: string empty  -n: string non-empty

# For loops
for i in 1 2 3 4 5; do
  echo "Item: $i"
done

for file in /var/log/*.log; do
  echo "Processing: $file"
done

for i in $(seq 1 10); do    # seq generates a range
  echo "$i"
done

# While loop
COUNT=0
while [[ $COUNT -lt 5 ]]; do
  echo "Count: $COUNT"
  ((COUNT++))
done

Functions, Arguments & Real-World Pattern

Functions promote reuse and readability. Arguments ($1, $@) make scripts configurable. Here is a real deploy script pattern.

bash — functions, arguments, real deploy script
#!/usr/bin/env bash
set -euo pipefail

# Functions
log_info()  { echo "[INFO]  $(date +%H:%M:%S) $*"; }
log_error() { echo "[ERROR] $(date +%H:%M:%S) $*" >&2; }  # >&2 = stderr

check_requirements() {
  local -a required=("docker" "git" "curl")
  for cmd in "${required[@]}"; do
    if ! command -v "$cmd" &>/dev/null; then
      log_error "Required command not found: $cmd"
      exit 1
    fi
  done
  log_info "All requirements met"
}

# Script arguments
# $0 = script name, $1 $2 ... = positional args
# $# = number of args, $@ = all args, $* = all args as one string
usage() {
  echo "Usage: $0 <environment> <image-tag>"
  echo "  environment: staging | production"
  echo "  image-tag:   docker image tag to deploy"
  exit 1
}

[[ $# -lt 2 ]] && usage    # exit with usage if fewer than 2 args

ENV="$1"
TAG="$2"

deploy() {
  local env="$1"
  local tag="$2"
  log_info "Deploying $tag to $env..."
  docker pull "myapp:$tag"
  docker stop "myapp-$env" 2>/dev/null || true   # ignore if not running
  docker run -d --name "myapp-$env" --restart=unless-stopped "myapp:$tag"
  log_info "Deploy complete"
}

check_requirements
deploy "$ENV" "$TAG"

Key Points to Remember

  • 1Always start scripts with #!/usr/bin/env bash and set -euo pipefail.
  • 2Use [[ ]] instead of [ ] — it supports &&, ||, regex, and does not split on spaces.
  • 3Quote all variable expansions: "$VAR" not $VAR — prevents word splitting on spaces.
  • 4Use local keyword inside functions to avoid polluting global scope.
  • 5$? is the exit code of the last command; 0 = success, non-zero = failure.
  • 6Redirect errors to stderr with >&2 so they can be separated from normal output.

Interview Questions

Sign in to ask Aria
1

What does set -euo pipefail do in a bash script?

2

What is the difference between $@ and $* in bash?

Ask Aria about Shell Scripting — Bash Fundamentals

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…