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_FILE

export 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=0

Parameter 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^^}"  # HELLO

String 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}"  # World

Arrays

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]}"  # green

echo "${ARR[@]}" — Expand all array elements.

echo "${COLORS[@]}"

echo "${#ARR[@]}" — Get the number of elements in an array.

echo "${#COLORS[@]}"  # 3

ARR+=(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]=443

for item in "${ARR[@]}"; do ... done — Iterate over all array elements.

for c in "${COLORS[@]}"; do echo "$c"; done

Conditionals

if [[ condition ]]; then ... fi — Basic if statement with extended test brackets.

if [[ -f 'config.txt' ]]; then echo 'Found'; fi

if [[ 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"; done

for (( i=0; i<n; i++ )); do ... done — C-style for loop with counter.

for (( i=1; i<=5; i++ )); do echo "$i"; done

for VAR in {start..end}; do ... done — Iterate over a range of numbers using brace expansion.

for i in {1..10}; do echo "$i"; done

while [[ condition ]]; do ... done — Loop while condition is true.

while [[ $count -lt 10 ]]; do ((count++)); done

while 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"; done

until [[ condition ]]; do ... done — Loop until condition becomes true.

until [[ -f /tmp/ready ]]; do sleep 1; done

Functions

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 5

return <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.txt

cmd >> file — Redirect stdout to a file (append).

echo 'line' >> log.txt

cmd 2> file — Redirect stderr to a file.

find / -name '*.conf' 2> /dev/null

cmd &> file — Redirect both stdout and stderr to a file.

make &> build.log

cmd 2>&1 — Redirect stderr to stdout (combine streams).

command 2>&1 | tee output.log

cmd1 | cmd2 — Pipe stdout of cmd1 to stdin of cmd2.

ps aux | grep nginx

cmd1 |& cmd2 — Pipe both stdout and stderr to cmd2.

make |& less

cmd <<< '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 -l

fg %<n> — Bring background job number n to the foreground.

fg %1

bg %<n> — Resume a stopped job in the background.

bg %1

wait [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 TERM

Command Chaining

cmd1 && cmd2 — Run cmd2 only if cmd1 succeeds (exit code 0).

mkdir build && cd build

cmd1 || 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 command

Ctrl+A / Ctrl+E — Move cursor to beginning / end of line.

Ctrl+A to jump to start

Ctrl+W / Alt+D — Delete word before / after cursor.

Ctrl+W to delete previous word

Ctrl+U / Ctrl+K — Delete from cursor to beginning / end of line.

Ctrl+U to clear everything before cursor

Ctrl+Y — Paste (yank) text previously cut with Ctrl+U/K/W.

Ctrl+U then Ctrl+Y to restore

Ctrl+L — Clear the screen (same as clear command).

Ctrl+L

Ctrl+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 pipefail

DIR="$(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.csv

command -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

  • zsh – the Z shell, an extended interactive shell with rich completion
  • fish – the user-friendly shell with syntax highlighting out of the box
  • jobs – manage background and stopped jobs in the current shell