What is Bash?
Bash (Bourne Again SHell) is the default shell on most Linux distributions and macOS. It's a command processor that runs in a text window where users type commands, and it can also read and execute commands from scripts.
Scripting
Automate repetitive tasks, system administration, and build complex workflows with shell scripts.
Portable
Available on virtually every Unix-like system. Write once, run anywhere (with minor adjustments).
Powerful Pipes
Chain commands together with pipes and redirections to build complex data processing pipelines.
Glue Language
Perfect for orchestrating other programs and tools. Let each tool do what it does best.
Basics
Hello World
#!/bin/bash # This is a comment echo "Hello, World!"
Running Scripts
# Make script executable chmod +x script.sh # Run the script ./script.sh # Or run with bash explicitly bash script.sh # Source (run in current shell, keeps variables) source script.sh . script.sh # same thing
Command Substitution
# Modern syntax (preferred) today=$(date +%Y-%m-%d) files=$(ls *.txt) # Can nest cleanly result=$(echo $(cat file.txt) | wc -w) # Legacy syntax (avoid) today=`date +%Y-%m-%d`
Arithmetic
# Arithmetic expansion result=$(( 5 + 3 )) result=$(( a + b )) # variables don't need $ result=$(( 10 / 3 )) # integer division = 3 result=$(( 2 ** 8 )) # exponentiation = 256 # Compound arithmetic (no output, use in conditions) (( counter++ )) (( total += 10 )) # Arithmetic condition if (( a > b )); then echo "a is greater" fi # Number bases echo $(( 0xFF )) # hex: 255 echo $(( 2#1010 )) # binary: 10 echo $(( 8#77 )) # octal: 63
Bash only supports integer arithmetic. For floating point, use
bc or awk.
Variables
Declaration & Assignment
# No spaces around = name="Alice" count=42 # Use the variable echo "Hello, $name" echo "Hello, ${name}" # braces for clarity # Readonly (constant) readonly PI=3.14159 # Export to child processes export PATH="$PATH:/usr/local/bin" # Unset a variable unset name
Special Variables
| Variable | Description |
|---|---|
$0 |
Script name |
$1, $2 ... $9 |
Positional arguments (use ${10} for 10+) |
$# |
Number of arguments |
$@ |
All arguments as separate words (use "$@") |
$* |
All arguments as single string |
$? |
Exit status of last command (0 = success) |
$$ |
PID of current shell |
$! |
PID of last background process |
$_ |
Last argument of previous command |
Parameter Expansion (Defaults)
# Use default if unset or empty echo "${name:-Guest}" # Assign default if unset or empty echo "${name:=Guest}" # Error if unset or empty echo "${name:?'name is required'}" # Use alternate value if set and non-empty echo "${name:+Hello $name}"
Declare with Types
# Integer declare -i num=42 num=num+1 # arithmetic automatic # Lowercase declare -l lower="HELLO" # stored as "hello" # Uppercase declare -u upper="hello" # stored as "HELLO" # Readonly declare -r constant="value" # Export declare -x exported="value"
Quoting
Single vs Double Quotes
name="Alice" # Double quotes: variables expand echo "Hello, $name" # Hello, Alice echo "Today is $(date)" # command substitution works # Single quotes: everything literal echo 'Hello, $name' # Hello, $name echo 'No $(expansion)' # No $(expansion) # Escaping in double quotes echo "The price is \$100" echo "She said \"Hello\"" # Single quote in single quotes (workaround) echo 'It'\''s working' # It's working
ANSI-C Quoting
# $'...' enables escape sequences echo $'Line1\nLine2' # actual newline echo $'Tab\there' # actual tab echo $'Ring the \a bell' # bell character
Always quote your variables:
"$var". Unquoted variables undergo word splitting and glob expansion, leading to subtle bugs.
Conditionals
if/elif/else
if [[ "$name" == "Alice" ]]; then echo "Hello, Alice!" elif [[ "$name" == "Bob" ]]; then echo "Hello, Bob!" else echo "Hello, stranger!" fi
Test Commands: [ ] vs [[ ]]
# [ ] - POSIX compatible, more portable if [ "$a" = "$b" ]; then ...; fi # [[ ]] - Bash extended test (preferred) # - No word splitting, safer without quotes # - Supports && || directly # - Pattern matching with == # - Regex matching with =~ if [[ $a == $b ]]; then ...; fi if [[ $file == *.txt ]]; then ...; fi if [[ $str =~ ^[0-9]+$ ]]; then ...; fi
String Comparisons
| Operator | Description |
|---|---|
== or = |
Equal |
!= |
Not equal |
-z "$str" |
String is empty |
-n "$str" |
String is not empty |
< |
Less than (alphabetically) |
> |
Greater than (alphabetically) |
Integer Comparisons
| Operator | Description |
|---|---|
-eq |
Equal |
-ne |
Not equal |
-lt |
Less than |
-le |
Less than or equal |
-gt |
Greater than |
-ge |
Greater than or equal |
File Tests
if [[ -f "$file" ]]; then ...; fi # regular file exists if [[ -d "$dir" ]]; then ...; fi # directory exists if [[ -e "$path" ]]; then ...; fi # exists (any type) if [[ -r "$file" ]]; then ...; fi # readable if [[ -w "$file" ]]; then ...; fi # writable if [[ -x "$file" ]]; then ...; fi # executable if [[ -s "$file" ]]; then ...; fi # size > 0 if [[ -L "$file" ]]; then ...; fi # symbolic link
Logical Operators
# In [[ ]] if [[ $a -gt 0 && $a -lt 100 ]]; then ...; fi if [[ $a == "yes" || $a == "y" ]]; then ...; fi if [[ ! -f "$file" ]]; then ...; fi # Command chaining command1 && command2 # run command2 only if command1 succeeds command1 || command2 # run command2 only if command1 fails
case Statement
case "$answer" in yes|y|Y) echo "Confirmed" ;; no|n|N) echo "Declined" ;; *) echo "Unknown response" ;; esac # Pattern matching case "$filename" in *.txt) echo "Text file" ;; *.jpg|*.png) echo "Image" ;; *) echo "Unknown" ;; esac
Loops
for Loop
# Iterate over list for name in Alice Bob Charlie; do echo "Hello, $name" done # Iterate over files for file in *.txt; do echo "Processing $file" done # Iterate over command output for user in $(cat users.txt); do echo "User: $user" done # C-style for loop for (( i=0; i<10; i++ )); do echo "$i" done # Range with brace expansion for i in {1..5}; do echo "$i" done # Range with step for i in {0..10..2}; do echo "$i" # 0, 2, 4, 6, 8, 10 done
while Loop
counter=0 while [[ $counter -lt 5 ]]; do echo "Count: $counter" (( counter++ )) done # Read file line by line while read -r line; do echo "Line: $line" done < file.txt # Process command output line by line ls -la | while read -r line; do echo "$line" done # Infinite loop while true; do # ... sleep 1 done
until Loop
# Run until condition is true (opposite of while) counter=0 until [[ $counter -ge 5 ]]; do echo "Count: $counter" (( counter++ )) done
Loop Control
# break - exit loop for i in {1..10}; do if [[ $i -eq 5 ]]; then break fi echo "$i" done # continue - skip to next iteration for i in {1..10}; do if [[ $((i % 2)) -eq 0 ]]; then continue # skip even numbers fi echo "$i" done # break/continue with nesting break 2 # break out of 2 nested loops continue 2 # continue in outer loop
Functions
Defining Functions
# Standard syntax greet() { echo "Hello, $1!" } # Alternative syntax function greet { echo "Hello, $1!" } # Call the function greet "Alice"
Arguments & Return Values
add() { local a=$1 local b=$2 local sum=$(( a + b )) echo "$sum" # output is the "return value" } # Capture output result=$(add 5 3) echo "Sum: $result" # Return status (0-255) is_even() { if [[ $(($1 % 2)) -eq 0 ]]; then return 0 # success/true else return 1 # failure/false fi } if is_even 42; then echo "It's even" fi
Local Variables
my_function() { local local_var="only here" # scoped to function global_var="everywhere" # global (avoid this) } my_function echo "$local_var" # empty - not accessible echo "$global_var" # "everywhere"
Function with All Args
print_all() { echo "Number of args: $#" for arg in "$@"; do echo " Arg: $arg" done } print_all "one" "two" "three"
Always use
local for variables inside functions to avoid polluting the global scope.
Arrays
Indexed Arrays
# Create array fruits=("apple" "banana" "cherry") numbers=(1 2 3 4 5) # Access elements echo "${fruits[0]}" # apple echo "${fruits[1]}" # banana echo "${fruits[-1]}" # cherry (last element) # All elements echo "${fruits[@]}" # apple banana cherry # Array length echo "${#fruits[@]}" # 3 # All indices echo "${!fruits[@]}" # 0 1 2 # Modify fruits[1]="blueberry" # Append fruits+=("date") # Slice echo "${fruits[@]:1:2}" # 2 elements starting at index 1
Iterating Arrays
# Iterate over values for fruit in "${fruits[@]}"; do echo "$fruit" done # Iterate with index for i in "${!fruits[@]}"; do echo "$i: ${fruits[$i]}" done
Associative Arrays (Bash 4+)
# Must declare first declare -A colors # Assign colors["apple"]="red" colors["banana"]="yellow" colors["grape"]="purple" # Or inline declare -A sizes=(["small"]=1 ["medium"]=2 ["large"]=3) # Access echo "${colors[apple]}" # red # All keys echo "${!colors[@]}" # apple banana grape # All values echo "${colors[@]}" # red yellow purple # Iterate for key in "${!colors[@]}"; do echo "$key: ${colors[$key]}" done # Check if key exists if [[ -v colors["apple"] ]]; then echo "Key exists" fi
Always quote array expansions:
"${array[@]}". Without quotes, elements with spaces will be split.
String Manipulation
String Length
str="Hello, World!" echo "${#str}" # 13
Substring Extraction
str="Hello, World!" # ${string:offset:length} echo "${str:0:5}" # Hello echo "${str:7}" # World! echo "${str: -6}" # World! (space before -) echo "${str: -6:5}" # World
Search & Replace
str="hello world world" # Replace first match echo "${str/world/there}" # hello there world # Replace all matches echo "${str//world/there}" # hello there there # Replace at start echo "${str/#hello/hi}" # hi world world # Replace at end echo "${str/%world/earth}" # hello world earth # Delete pattern (replace with nothing) echo "${str// world/}" # hello
Pattern Deletion
path="/home/user/docs/file.txt" # Remove shortest match from start echo "${path#*/}" # home/user/docs/file.txt # Remove longest match from start echo "${path##*/}" # file.txt (basename) # Remove shortest match from end echo "${path%/*}" # /home/user/docs (dirname) # Remove longest match from end echo "${path%%/*}" # (empty - removes everything) # Get extension file="archive.tar.gz" echo "${file#*.}" # tar.gz (first .) echo "${file##*.}" # gz (last .) # Remove extension echo "${file%.*}" # archive.tar echo "${file%%.*}" # archive
Case Conversion (Bash 4+)
str="Hello World" # Lowercase echo "${str,}" # hello World (first char) echo "${str,,}" # hello world (all) # Uppercase echo "${str^}" # Hello World (first char) echo "${str^^}" # HELLO WORLD (all) # Toggle case echo "${str~}" # hELLO World (first char) echo "${str~~}" # hELLO wORLD (all)
I/O & Redirection
Output: echo & printf
# echo echo "Hello" # with newline echo -n "Hello" # without newline echo -e "Line1\nLine2" # interpret escapes # printf (more control) printf "%s is %d years old\n" "Alice" 30 printf "%-10s %5d\n" "Item" 42 # padded printf "%05d\n" 42 # zero-padded: 00042 printf "%.2f\n" 3.14159 # 3.14
Input: read
# Basic read read name echo "Hello, $name" # With prompt read -p "Enter your name: " name # Silent (for passwords) read -sp "Password: " password echo # newline after hidden input # Read into array read -a words <<< "one two three" # Read with timeout read -t 5 -p "Quick! (5s): " answer # Read n characters read -n 1 -p "Press any key: " key # Raw input (no backslash interpretation) read -r line # always use -r!
Redirection
# Output redirection command > file.txt # overwrite command >> file.txt # append # Input redirection command < file.txt # Stderr redirection command 2> errors.txt # stderr to file command 2>> errors.txt # append stderr command 2>/dev/null # discard stderr # Combine stdout and stderr command &> output.txt # both to file command > file.txt 2>&1 # stderr to stdout command 2>&1 | less # both through pipe # Discard all output command >/dev/null 2>&1
Here Documents & Here Strings
# Here document (multi-line input) cat <<EOF This is line 1 Variable: $name This is line 3 EOF # Literal (no expansion) cat <<'EOF' $variables are literal $(commands) too EOF # Strip leading tabs cat <<-EOF Indented with tabs They will be stripped EOF # Here string (single-line input) grep "pattern" <<< "$variable" bc <<< "5 + 3" # 8
Pipes
# Chain commands cat file.txt | grep "pattern" | sort | uniq # Process substitution diff <(sort file1) <(sort file2) # Tee - write to file AND stdout command | tee output.txt | less command | tee -a output.txt # append
Error Handling
Exit Codes
# 0 = success, 1-255 = failure command echo "Exit code: $?" # Exit with specific code exit 0 # success exit 1 # generic failure # Check command success if command; then echo "Success" else echo "Failed" fi
Strict Mode
#!/bin/bash set -euo pipefail # -e: exit on any command failure # -u: error on undefined variables # -o pipefail: pipe fails if any command fails # Optional: safer word splitting IFS=$'\n\t'
Error Handling Patterns
# Handle individual command failure command || { echo "Failed" >&2; exit 1; } # Command chain with fallback command1 && command2 || echo "Something failed" # Explicit check if ! command; then echo "Command failed" >&2 exit 1 fi # Check command exists if ! command -v jq >/dev/null 2>&1; then echo "jq is required but not installed" >&2 exit 1 fi
trap - Signal Handling & Cleanup
# Cleanup on exit cleanup() { echo "Cleaning up..." rm -f "$tmpfile" } trap cleanup EXIT tmpfile=$(mktemp) # ... script continues, cleanup runs on exit # Handle Ctrl+C (SIGINT) trap 'echo "Interrupted"; exit 1' INT # Error with line number trap 'echo "Error on line $LINENO"' ERR # Ignore signal trap '' SIGINT # ignore Ctrl+C # Reset to default trap - SIGINT
Input Validation
# Check argument count if [[ $# -lt 1 ]]; then echo "Usage: $0 <filename>" >&2 exit 1 fi # Check file exists if [[ ! -f "$1" ]]; then echo "Error: File '$1' not found" >&2 exit 1 fi # Validate numeric input if ! [[ "$1" =~ ^[0-9]+$ ]]; then echo "Error: '$1' is not a number" >&2 exit 1 fi
getopts - Parse Options
usage() { echo "Usage: $0 [-v] [-f file] [-n count] arg" } verbose=0 file="" count=1 while getopts "hvf:n:" opt; do case "$opt" in h) usage; exit 0 ;; v) verbose=1 ;; f) file="$OPTARG" ;; n) count="$OPTARG" ;; ?) usage; exit 1 ;; esac done # Shift past options shift $(( OPTIND - 1 )) # Remaining args in $@ echo "Positional args: $@"
Write to stderr for errors:
echo "Error" >&2. This keeps error messages separate from normal output.