Bash scripts glue the system's utilities together, and mastering variables, control flow, and exit handling unlocks real automation. Variables are assigned without spaces around = (e.g., NAME="Linux") and read with a leading dollar sign (\$NAME or the safer "\${NAME}"); readonly PI=3.14 creates a constant. Special positional and status variables are critical: \$0 holds the script name, \$1, \$2, ... are positional arguments, \$# counts them, \$@ expands them all, \$? exposes the last command's exit code, and \$\$ returns the current PID. Environment variables, set per session with export VAR="value" or for a single command with VAR=value command, are visible across the process tree; env and printenv list them. The most important is PATH, a colon-separated list of executable directories; append export PATH="\$PATH:/new/dir" for the current shell, persist it by editing ~/.bashrc or ~/.profile, and then run source ~/.bashrc to activate. Common built-ins like HOME, USER, SHELL, and LANG are populated automatically.
Control flow lets scripts react to data. The for loop iterates over a list (for i in 1 2 3; do echo \$i; done), over a glob (for f in *.txt; do wc -l "\$f"; done), or with a C-style counter (for ((i=0; i<10; i++)); do echo \$i; done). The while loop runs as long as a condition holds — a count loop (while [ \$count -lt 10 ]; do echo \$count; ((count++)); done) or a file consumer (while IFS= read -r line; do echo "\$line"; done < file.txt) — and conditional logic uses if/elif/else with the classic test command ([) and operators like -eq, -f file, -d dir, -z for empty string, -n for non-empty, and = for string equality; the modern [[ ]] supports advanced pattern matching. Functions defined as greet() { echo "Hello, \$1!"; return 0; } take arguments through \$1, \$2, return status codes in 0–255 via return, and benefit from local variables for scoping; their output is captured with command substitution as result=\$(greet "World"), and a function must be defined before it is called.
Exit codes transform scripts into reliable building blocks. A zero status means success, anything else flags failure, and \$? exposes the most recent status. Scripts exit explicitly with exit 1, and pipelines can short-circuit with cmd1 && cmd2 (run cmd2 only if cmd1 succeeded) or cmd1 || cmd2 (run cmd2 only on failure). Defensive practice includes set -e (exit on first error) and set -o pipefail (catch failures anywhere in a pipeline). Redirection complements errors: > file and >> file handle stdout, 2> captures stderr, 2>&1 merges stderr into stdout, and command > /dev/null 2>&1 silences everything — a common idiom for "run but don't pollute the console."
Package management differs by family. Debian and Ubuntu systems use apt: apt update refreshes the package index, apt upgrade applies upgrades, apt install nginx installs a package, apt remove nginx keeps configuration while removing binaries, and apt purge nginx removes configuration too. apt search keyword finds packages by name or description, apt list --installed audits the system, and dpkg -i package.deb installs a local archive. RHEL-family distributions use dnf (the successor to yum): dnf install httpd, dnf remove httpd, dnf update, dnf search keyword, dnf info httpd, and dnf list installed cover the same workflow, with rpm -ivh package.rpm for local files and rpm -qa for installed-package inventory.