Bash — Shell Scripting and Interactive Command Line
Practical guide to Bash: variables, parameter expansion, loops, functions, redirection and robust scripts with set -euo pipefail.
Bash is the default shell on most Linux distributions and, at the same time, a full-fledged scripting language. Whether you work interactively at the command line or write automation scripts, variables, parameter expansion, loops and functions are your daily bread. This guide gathers the building blocks you actually need: from string manipulation through redirection and process control to robust scripting patterns. Keep the difference between interactive and script use in mind – both follow the same rules but place different demands on you.
Variables & Strings
VAR='value' — Assign a variable. No spaces around the equals sign.
NAME='World'echo "$VAR" — Expand variable inside double quotes. Single quotes prevent expansion.
echo "Hello $NAME"echo "${VAR}text" — Use braces to delimit variable name from surrounding text.
echo "${NAME}_backup"readonly VAR='value' — Declare a read-only (constant) variable.
readonly PI='3.14159'unset VAR — Remove a variable.
unset TEMP_FILEexport VAR='value' — Export variable to child processes (environment variable).
export PATH="$HOME/bin:$PATH"local VAR='value' — Declare a local variable inside a function.
local count=0Parameter Expansion
${VAR:-default} — Use default value if variable is unset or empty.
echo "${USER:-anonymous}"${VAR:=default} — Assign default value if variable is unset or empty.
echo "${LANG:=en_US.UTF-8}"${VAR:+alternate} — Use alternate value if variable is set and non-empty.
echo "${DEBUG:+--verbose}"${VAR:?error message} — Print error and exit if variable is unset or empty.
${API_KEY:?API_KEY must be set}${#VAR} — Get the length of a string variable.
echo "Length: ${#NAME}"${VAR^^} / ${VAR,,} — Convert to uppercase / lowercase (Bash 4+).
echo "${name^^}" # HELLOString Manipulation
${VAR#pattern} — Remove shortest match of pattern from the beginning.
FILE='archive.tar.gz'; echo "${FILE#*.}" # tar.gz${VAR##pattern} — Remove longest match of pattern from the beginning.
PATH='/home/user/file.txt'; echo "${PATH##*/}" # file.txt${VAR%pattern} — Remove shortest match of pattern from the end.
FILE='photo.jpg'; echo "${FILE%.*}" # photo${VAR%%pattern} — Remove longest match of pattern from the end.
FILE='backup.tar.gz'; echo "${FILE%%.*}" # backup${VAR/pattern/replacement} — Replace first occurrence of pattern.
echo "${STR/foo/bar}"${VAR//pattern/replacement} — Replace all occurrences of pattern.
echo "${PATH//:/ }"${VAR:offset:length} — Extract substring starting at offset with given length.
STR='Hello World'; echo "${STR:6:5}" # WorldArrays
ARR=(val1 val2 val3) — Declare an indexed array.
COLORS=(red green blue)echo "${ARR[0]}" — Access array element by index (0-based).
echo "${COLORS[1]}" # greenecho "${ARR[@]}" — Expand all array elements.
echo "${COLORS[@]}"echo "${#ARR[@]}" — Get the number of elements in an array.
echo "${#COLORS[@]}" # 3ARR+=(val4) — Append an element to an array.
COLORS+=(yellow)declare -A MAP — Declare an associative array (Bash 4+).
declare -A PORTS; PORTS[http]=80; PORTS[https]=443for item in "${ARR[@]}"; do ... done — Iterate over all array elements.
for c in "${COLORS[@]}"; do echo "$c"; doneConditionals
if [[ condition ]]; then ... fi — Basic if statement with extended test brackets.
if [[ -f 'config.txt' ]]; then echo 'Found'; fiif [[ cond ]]; then ... else ... fi — If-else statement.
if [[ $# -gt 0 ]]; then echo "$1"; else echo 'No args'; fi[[ -f <file> ]] — Test if file exists and is a regular file.
[[ -f /etc/hosts ]] && echo 'Exists'[[ -d <dir> ]] — Test if directory exists.
[[ -d /tmp ]] && echo 'Directory exists'[[ -z "$VAR" ]] — Test if string is empty (zero length).
[[ -z "$INPUT" ]] && echo 'Empty input'[[ -n "$VAR" ]] — Test if string is non-empty.
[[ -n "$USER" ]] && echo "User: $USER"[[ "$A" == "$B" ]] — String equality comparison.
[[ "$answer" == 'yes' ]] && proceed[[ "$A" =~ regex ]] — Regex match test (extended regex).
[[ "$email" =~ ^[a-z]+@[a-z]+\.[a-z]+$ ]] && echo 'Valid'Loops
for VAR in list; do ... done — Iterate over a list of items.
for f in *.txt; do echo "$f"; donefor (( i=0; i<n; i++ )); do ... done — C-style for loop with counter.
for (( i=1; i<=5; i++ )); do echo "$i"; donefor VAR in {start..end}; do ... done — Iterate over a range of numbers using brace expansion.
for i in {1..10}; do echo "$i"; donewhile [[ condition ]]; do ... done — Loop while condition is true.
while [[ $count -lt 10 ]]; do ((count++)); donewhile read -r line; do ... done < <file> — Read a file line by line.
while read -r line; do echo "$line"; done < data.txt<command> | while read -r line; do ... done — Process command output line by line.
find . -name '*.log' | while read -r f; do wc -l "$f"; doneuntil [[ condition ]]; do ... done — Loop until condition becomes true.
until [[ -f /tmp/ready ]]; do sleep 1; doneFunctions
fname() { ... } — Define a function. Preferred syntax over function keyword.
greet() { echo "Hello, $1"; }$1, $2, ..., $@, $# — Access function arguments. $@ is all args, $# is argument count.
add() { echo $(( $1 + $2 )); }; add 3 5return <n> — Return an exit status from a function (0-255). Use echo for values.
is_root() { [[ $EUID -eq 0 ]] && return 0 || return 1; }result=$(fname arg) — Capture function output in a variable using command substitution.
upper() { echo "${1^^}"; }; NAME=$(upper 'hello')Arithmetic
$(( expression )) — Arithmetic expansion. Supports +, -, *, /, %, **.
echo "$(( 5 + 3 ))" # 8(( VAR++ )) / (( VAR-- )) — Increment / decrement a variable.
count=0; (( count++ )); echo "$count" # 1(( VAR += n )) — Compound assignment operators (+=, -=, *=, /=, %=).
total=100; (( total -= 25 )); echo "$total" # 75$(( RANDOM % n )) — Generate a random number between 0 and n-1.
echo "Dice: $(( RANDOM % 6 + 1 ))"Redirection & Pipes
cmd > file — Redirect stdout to a file (overwrite).
echo 'hello' > output.txtcmd >> file — Redirect stdout to a file (append).
echo 'line' >> log.txtcmd 2> file — Redirect stderr to a file.
find / -name '*.conf' 2> /dev/nullcmd &> file — Redirect both stdout and stderr to a file.
make &> build.logcmd 2>&1 — Redirect stderr to stdout (combine streams).
command 2>&1 | tee output.logcmd1 | cmd2 — Pipe stdout of cmd1 to stdin of cmd2.
ps aux | grep nginxcmd1 |& cmd2 — Pipe both stdout and stderr to cmd2.
make |& lesscmd <<< 'string' — Here-string. Pass a string as stdin to a command.
grep 'error' <<< "$log_output"Process Control
cmd & — Run command in the background.
sleep 60 &jobs — List background jobs in the current shell.
jobs -lfg %<n> — Bring background job number n to the foreground.
fg %1bg %<n> — Resume a stopped job in the background.
bg %1wait [pid] — Wait for background process to finish. No argument waits for all.
cmd1 & cmd2 &; wait$! — PID of the last background process.
server & echo "PID: $!"$$ — PID of the current shell.
echo "Script PID: $$"trap 'cmd' SIGNAL — Execute command when a signal is received.
trap 'rm -f /tmp/lockfile; exit' EXIT INT TERMCommand Chaining
cmd1 && cmd2 — Run cmd2 only if cmd1 succeeds (exit code 0).
mkdir build && cd buildcmd1 || cmd2 — Run cmd2 only if cmd1 fails (non-zero exit code).
grep -q 'error' log.txt || echo 'No errors'cmd1; cmd2 — Run cmd2 after cmd1 regardless of exit status.
echo 'Start'; date; echo 'End'{ cmd1; cmd2; } — Group commands in the current shell (note trailing semicolon and spaces).
{ echo 'Header'; cat data.txt; } > report.txt(cmd1; cmd2) — Run commands in a subshell (does not affect parent).
(cd /tmp && tar xzf archive.tar.gz)Special Variables
$? — Exit status of the last command (0 = success).
grep -q 'root' /etc/passwd; echo "$?"$0 — Name of the current script or shell.
echo "Script: $0"$# / $@ / $* — Number of arguments / all arguments (as separate words / as single word).
echo "$# arguments: $@"$_ — Last argument of the previous command.
mkdir mydir; cd "$_"$RANDOM — Random integer between 0 and 32767.
echo "$RANDOM"$SECONDS — Seconds since shell was started. Can be reset.
SECONDS=0; sleep 2; echo "Elapsed: ${SECONDS}s"$LINENO — Current line number in the script.
echo "Error at line $LINENO"Keyboard Shortcuts
Ctrl+R — Reverse search through command history.
Ctrl+R, then type part of a previous commandCtrl+A / Ctrl+E — Move cursor to beginning / end of line.
Ctrl+A to jump to startCtrl+W / Alt+D — Delete word before / after cursor.
Ctrl+W to delete previous wordCtrl+U / Ctrl+K — Delete from cursor to beginning / end of line.
Ctrl+U to clear everything before cursorCtrl+Y — Paste (yank) text previously cut with Ctrl+U/K/W.
Ctrl+U then Ctrl+Y to restoreCtrl+L — Clear the screen (same as clear command).
Ctrl+LCtrl+Z — Suspend the current foreground process.
Ctrl+Z, then bg to resume in background!! / !<n> / !<string> — Re-run last command / command #n / last command starting with string.
sudo !!Common Patterns
set -euo pipefail — Strict mode: exit on error, undefined vars, and pipe failures.
#!/bin/bash
set -euo pipefailDIR="$(cd "$(dirname "$0")" && pwd)" — Get the directory of the current script reliably.
DIR="$(cd "$(dirname "$0")" && pwd)"[[ -f file ]] || { echo 'Missing'; exit 1; } — Guard clause: exit early if a required file is missing.
[[ -f config.yml ]] || { echo 'Config not found'; exit 1; }while IFS=',' read -r col1 col2; do ... done < file — Parse CSV or delimited data line by line.
while IFS=',' read -r name age; do echo "$name is $age"; done < people.csvcommand -v <cmd> &> /dev/null — Check if a command is available on the system.
command -v docker &> /dev/null || echo 'Docker not installed'mktemp — Create a temporary file safely and print its path.
TMP=$(mktemp); echo 'data' > "$TMP"; rm "$TMP" Conclusion
Bash carries you from a quick one-liner pipeline all the way to a full-blown deployment script. For interactive work, keyboard shortcuts, history expansion and pipes matter most; for scripts, parameter expansion, arrays and clean functions make the difference. Enable set -euo pipefail in every serious script so errors don't slip through silently, and quote your variables consistently ("$VAR") to avoid word-splitting and globbing surprises. Remember where your configuration lives, too: ~/.bashrc applies to interactive non-login shells, ~/.bash_profile to login shells – mixing the two leaves you puzzled over missing aliases or PATH entries.
Further Reading
- GNU Bash Reference Manual – the official, complete reference for every Bash feature
- BashGuide (Greg's Wiki) – a well-regarded, practical introduction to Bash scripting
- ShellCheck – online linter that catches common mistakes in shell scripts