Getting started

Introduction

This is a quick reference to getting started with Bash scripting.

Example

#!/usr/bin/env bash

name="John"
echo "Hello $name!"

Variables

name="John"
echo $name  # see below
echo "$name"
echo "${name}!"

Generally quote your variables unless they contain wildcards to expand or command fragments.

wildcard="*.txt"
options="iv"
cp -$options $wildcard /tmp

String quotes

name="John"
echo "Hi $name"  #=> Hi John
echo 'Hi $name'  #=> Hi $name

Shell execution

echo "I'm in $(pwd)"
echo "I'm in `pwd`"  # obsolescent
# Same

See Command substitution

Conditional execution

git commit && git push
git commit || echo "Commit failed"

Functions

get_name() {
  echo "John"
}

echo "You are $(get_name)"

See: Functions

Conditionals

if [[ -z "$string" ]]; then
  echo "String is empty"
elif [[ -n "$string" ]]; then
  echo "String is not empty"
fi

See: Conditionals

Strict mode

set -euo pipefail
IFS=$'\n\t'

See: Unofficial bash strict mode

Brace expansion

echo {A,B}.js
Expression Description
{A,B} Same as A B
{A,B}.js Same as A.js B.js
{1..5} Same as 1 2 3 4 5
{{1..3},{7..9}} Same as 1 2 3 7 8 9

See: Brace expansion

Parameter expansions

Basics

name="John"
echo "${name}"
echo "${name/J/j}"    #=> "john" (substitution)
echo "${name:0:2}"    #=> "Jo" (slicing)
echo "${name::2}"     #=> "Jo" (slicing)
echo "${name::-1}"    #=> "Joh" (slicing)
echo "${name:(-1)}"   #=> "n" (slicing from right)
echo "${name:(-2):1}" #=> "h" (slicing from right)
echo "${food:-Cake}"  #=> $food or "Cake"
length=2
echo "${name:0:length}"  #=> "Jo"

See: Parameter expansion

str="/path/to/foo.cpp"
echo "${str%.cpp}"    # /path/to/foo
echo "${str%.cpp}.o"  # /path/to/foo.o
echo "${str%/*}"      # /path/to

echo "${str##*.}"     # cpp (extension)
echo "${str##*/}"     # foo.cpp (basepath)

echo "${str#*/}"      # path/to/foo.cpp
echo "${str##*/}"     # foo.cpp

echo "${str/foo/bar}" # /path/to/bar.cpp
str="Hello world"
echo "${str:6:5}"    # "world"
echo "${str: -5:5}"  # "world"
src="/path/to/foo.cpp"
base=${src##*/}   #=> "foo.cpp" (basepath)
dir=${src%$base}  #=> "/path/to/" (dirpath)

Prefix name expansion

prefix_a=one
prefix_b=two
echo ${!prefix_*}  # all variables names starting with `prefix_`
prefix_a prefix_b

Indirection

name=joe
pointer=name
echo ${!pointer}
joe

Substitution

Code Description
${foo%suffix} Remove suffix
${foo#prefix} Remove prefix
${foo%%suffix} Remove long suffix
${foo/%suffix} Remove long suffix
${foo##prefix} Remove long prefix
${foo/#prefix} Remove long prefix
${foo/from/to} Replace first match
${foo//from/to} Replace all
${foo/%from/to} Replace suffix
${foo/#from/to} Replace prefix

Comments

# Single line comment
: '
This is a
multi line
comment
'

Substrings

Expression Description
${foo:0:3} Substring (position, length)
${foo:(-3):3} Substring from the right

Length

Expression Description
${#foo} Length of $foo

Manipulation

str="HELLO WORLD!"
echo "${str,}"   #=> "hELLO WORLD!" (lowercase 1st letter)
echo "${str,,}"  #=> "hello world!" (all lowercase)

str="hello world!"
echo "${str^}"   #=> "Hello world!" (uppercase 1st letter)
echo "${str^^}"  #=> "HELLO WORLD!" (all uppercase)

Default values

Expression Description
${foo:-val} $foo, or val if unset (or null)
${foo:=val} Set $foo to val if unset (or null)
${foo:+val} val if $foo is set (and not null)
${foo:?message} Show error message and exit if $foo is unset (or null)

Omitting the : removes the (non)nullity checks, e.g. ${foo-val} expands to val if unset otherwise $foo.

Loops

Basic for loop

for i in /etc/rc.*; do
  echo "$i"
done

C-like for loop

for ((i = 0 ; i < 100 ; i++)); do
  echo "$i"
done

Ranges

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

With step size

for i in {5..50..5}; do
    echo "Welcome $i"
done

Reading lines

while read -r line; do
  echo "$line"
done <file.txt

Forever

while true; do
  ···
done

Functions

Defining functions

myfunc() {
    echo "hello $1"
}
# Same as above (alternate syntax)
function myfunc {
    echo "hello $1"
}
myfunc "John"

Returning values

myfunc() {
    local myresult='some value'
    echo "$myresult"
}
result=$(myfunc)

Raising errors

myfunc() {
  return 1
}
if myfunc; then
  echo "success"
else
  echo "failure"
fi

Arguments

Expression Description
$# Number of arguments
$* All positional arguments (as a single word)
$@ All positional arguments (as separate strings)
$1 First argument
$_ Last argument of the previous command

Note: $@ and $* must be quoted in order to perform as described. Otherwise, they do exactly the same thing (arguments as separate strings).

See Special parameters.

Conditionals

Conditions

Note that [[ is actually a command/program that returns either 0 (true) or 1 (false). Any program that obeys the same logic (like all base utils, such as grep(1) or ping(1)) can be used as condition, see examples.

Condition Description
[[ -z STRING ]] Empty string
[[ -n STRING ]] Not empty string
[[ STRING == STRING ]] Equal
[[ STRING != STRING ]] Not Equal
[[ NUM -eq NUM ]] Equal
[[ NUM -ne NUM ]] Not equal
[[ NUM -lt NUM ]] Less than
[[ NUM -le NUM ]] Less than or equal
[[ NUM -gt NUM ]] Greater than
[[ NUM -ge NUM ]] Greater than or equal
[[ STRING =~ STRING ]] Regexp
(( NUM < NUM )) Numeric conditions

More conditions

Condition Description
[[ -o noclobber ]] If OPTIONNAME is enabled
[[ ! EXPR ]] Not
[[ X && Y ]] And
[[ X || Y ]] Or

File conditions

Condition Description
[[ -e FILE ]] Exists
[[ -r FILE ]] Readable
[[ -h FILE ]] Symlink
[[ -d FILE ]] Directory
[[ -w FILE ]] Writable
[[ -s FILE ]] Size is > 0 bytes
[[ -f FILE ]] File
[[ -x FILE ]] Executable
[[ FILE1 -nt FILE2 ]] 1 is more recent than 2
[[ FILE1 -ot FILE2 ]] 2 is more recent than 1
[[ FILE1 -ef FILE2 ]] Same files

Example

# String
if [[ -z "$string" ]]; then
  echo "String is empty"
elif [[ -n "$string" ]]; then
  echo "String is not empty"
else
  echo "This never happens"
fi
# Combinations
if [[ X && Y ]]; then
  ...
fi
# Equal
if [[ "$A" == "$B" ]]
# Regex
if [[ "A" =~ . ]]
if (( $a < $b )); then
   echo "$a is smaller than $b"
fi
if [[ -e "file.txt" ]]; then
  echo "file exists"
fi

Arrays

Defining arrays

Fruits=('Apple' 'Banana' 'Orange')
Fruits[0]="Apple"
Fruits[1]="Banana"
Fruits[2]="Orange"

Working with arrays

echo "${Fruits[0]}"           # Element #0
echo "${Fruits[-1]}"          # Last element
echo "${Fruits[@]}"           # All elements, space-separated
echo "${#Fruits[@]}"          # Number of elements
echo "${#Fruits}"             # String length of the 1st element
echo "${#Fruits[3]}"          # String length of the Nth element
echo "${Fruits[@]:3:2}"       # Range (from position 3, length 2)
echo "${!Fruits[@]}"          # Keys of all elements, space-separated

Operations

Fruits=("${Fruits[@]}" "Watermelon")    # Push
Fruits+=('Watermelon')                  # Also Push
Fruits=( "${Fruits[@]/Ap*/}" )          # Remove by regex match
unset Fruits[2]                         # Remove one item
Fruits=("${Fruits[@]}")                 # Duplicate
Fruits=("${Fruits[@]}" "${Veggies[@]}") # Concatenate
lines=(`cat "logfile"`)                 # Read from file

Iteration

for i in "${arrayName[@]}"; do
  echo "$i"
done

Dictionaries

Defining

declare -A sounds
sounds[dog]="bark"
sounds[cow]="moo"
sounds[bird]="tweet"
sounds[wolf]="howl"

Declares sound as a Dictionary object (aka associative array).

Working with dictionaries

echo "${sounds[dog]}" # Dog's sound
echo "${sounds[@]}"   # All values
echo "${!sounds[@]}"  # All keys
echo "${#sounds[@]}"  # Number of elements
unset sounds[dog]     # Delete dog

Iteration

Iterate over values

for val in "${sounds[@]}"; do
  echo "$val"
done

Iterate over keys

for key in "${!sounds[@]}"; do
  echo "$key"
done

Options

Options

set -o noclobber  # Avoid overlay files (echo "hi" > foo)
set -o errexit    # Used to exit upon error, avoiding cascading errors
set -o pipefail   # Unveils hidden failures
set -o nounset    # Exposes unset variables

Glob options

shopt -s nullglob    # Non-matching globs are removed  ('*.foo' => '')
shopt -s failglob    # Non-matching globs throw errors
shopt -s nocaseglob  # Case insensitive globs
shopt -s dotglob     # Wildcards match dotfiles ("*.sh" => ".foo.sh")
shopt -s globstar    # Allow ** for recursive matches ('lib/**/*.rb' => 'lib/a/b/c.rb')

Set GLOBIGNORE as a colon-separated list of patterns to be removed from glob matches.

History

Commands

Command Description
history Show history
shopt -s histverify Don’t execute expanded result immediately

Expansions

Expression Description
!$ Expand last parameter of most recent command
!* Expand all parameters of most recent command
!-n Expand nth most recent command
!n Expand nth command in history
!<command> Expand most recent invocation of command <command>

Operations

Code Description
!! Execute last command again
!!:s/<FROM>/<TO>/ Replace first occurrence of <FROM> to <TO> in most recent command
!!:gs/<FROM>/<TO>/ Replace all occurrences of <FROM> to <TO> in most recent command
!$:t Expand only basename from last parameter of most recent command
!$:h Expand only directory from last parameter of most recent command

!! and !$ can be replaced with any valid expansion.

Slices

Code Description
!!:n Expand only nth token from most recent command (command is 0; first argument is 1)
!^ Expand first argument from most recent command
!$ Expand last token from most recent command
!!:n-m Expand range of tokens from most recent command
!!:n-$ Expand nth token to last from most recent command

!! can be replaced with any valid expansion i.e. !cat, !-2, !42, etc.

Miscellaneous

Numeric calculations

$((a + 200))      # Add 200 to $a
$(($RANDOM%200))  # Random number 0..199
declare -i count  # Declare as type integer
count+=1          # Increment

Subshells

(cd somedir; echo "I'm now in $PWD")
pwd # still in first directory

Redirection

python hello.py > output.txt            # stdout to (file)
python hello.py >> output.txt           # stdout to (file), append
python hello.py 2> error.log            # stderr to (file)
python hello.py 2>&1                    # stderr to stdout
python hello.py 2>/dev/null             # stderr to (null)
python hello.py >output.txt 2>&1        # stdout and stderr to (file), equivalent to &>
python hello.py &>/dev/null             # stdout and stderr to (null)
echo "$0: warning: too many users" >&2  # print diagnostic message to stderr
python hello.py < foo.txt      # feed foo.txt to stdin for python
diff <(ls -r) <(ls)            # Compare two stdout without files

Inspecting commands

command -V cd
#=> "cd is a function/alias/whatever"

Trap errors

trap 'echo Error at about $LINENO' ERR

or

traperr() {
  echo "ERROR: ${BASH_SOURCE[1]} at about ${BASH_LINENO[0]}"
}

set -o errtrace
trap traperr ERR

Case/switch

case "$1" in
  start | up)
    vagrant up
    ;;

  *)
    echo "Usage: $0 {start|stop|ssh}"
    ;;
esac

Source relative

source "${0%/*}/../share/foo.sh"

printf

printf "Hello %s, I'm %s" Sven Olga
#=> "Hello Sven, I'm Olga

printf "1 + 1 = %d" 2
#=> "1 + 1 = 2"

printf "This is how you print a float: %f" 2
#=> "This is how you print a float: 2.000000"

printf '%s\n' '#!/bin/bash' 'echo hello' >file
# format string is applied to each group of arguments
printf '%i+%i=%i\n' 1 2 3  4 5 9

Transform strings

Command option Description
-c Operations apply to characters not in the given set
-d Delete characters
-s Replaces repeated characters with single occurrence
-t Truncates
[:upper:] All upper case letters
[:lower:] All lower case letters
[:digit:] All digits
[:space:] All whitespace
[:alpha:] All letters
[:alnum:] All letters and digits

Example

echo "Welcome To HHFCheatSheets" | tr '[:lower:]' '[:upper:]'
WELCOME TO HHFCheatSheets

Directory of script

dir=${0%/*}

Getting options

while [[ "$1" =~ ^- && ! "$1" == "--" ]]; do case $1 in
  -V | --version )
    echo "$version"
    exit
    ;;
  -s | --string )
    shift; string=$1
    ;;
  -f | --flag )
    flag=1
    ;;
esac; shift; done
if [[ "$1" == '--' ]]; then shift; fi

Heredoc

cat <<END
hello world
END

Reading input

echo -n "Proceed? [y/n]: "
read -r ans
echo "$ans"

The -r option disables a peculiar legacy behavior with backslashes.

read -n 1 ans    # Just one character

Special variables

Expression Description
$? Exit status of last task
$! PID of last background task
$$ PID of shell
$0 Filename of the shell script
$_ Last argument of the previous command
${PIPESTATUS[n]} return value of piped commands (array)

See Special parameters.

Go to previous directory

pwd # /home/user/foo
cd bar/
pwd # /home/user/foo/bar
cd -
pwd # /home/user/foo

Check for command’s result

if ping -c 1 google.com; then
  echo "It appears you have a working internet connection"
fi

Grep check

if grep -q 'foo' ~/.bash_history; then
  echo "You appear to have typed 'foo' in the past"
fi

Getting Started With In-depth Bash

Understanding the Basics in detail

Bash (Bourne Again Shell) is both a command interpreter and a scripting language. Let’s start with a comprehensive example that demonstrates several core concepts:

#!/usr/bin/env bash

# Enable strict mode for better error handling
set -euo pipefail
IFS=$'\n\t'

# Define script configuration
readonly MAX_RETRIES=3
readonly TIMEOUT=5

# Function to log messages with timestamps
log_message() {
    local level="$1"
    local message="$2"
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $message"
}

# Function to handle cleanup on script exit
cleanup() {
    log_message "INFO" "Performing cleanup..."
    # Add your cleanup code here
}

# Register the cleanup function to run on script exit
trap cleanup EXIT

# Main script execution
main() {
    log_message "INFO" "Script started"
    
    # Example of error handling
    if [[ $# -lt 1 ]]; then
        log_message "ERROR" "No arguments provided"
        exit 1
    }
    
    # Process each argument
    for arg in "$@"; do
        log_message "INFO" "Processing argument: $arg"
        # Add your processing logic here
    done
}

# Execute main function
main "$@"

This example demonstrates several important concepts:

  • Shebang line for portability
  • Strict mode settings
  • Function definitions
  • Error handling
  • Logging
  • Cleanup operations
  • Command line argument processing

Advanced Variable Usage

Here’s a comprehensive example of variable manipulation:

# String manipulation
text="Hello World"
echo "${text,,}"      # Convert to lowercase: hello world
echo "${text^^}"      # Convert to uppercase: HELLO WORLD
echo "${text:0:5}"    # Substring: Hello
echo "${#text}"       # Length: 11

# Array operations with explanation
declare -A fruits
fruits=(
    ["apple"]="red"
    ["banana"]="yellow"
    ["grape"]="purple"
)

# Iterate with both key and value
for fruit in "${!fruits[@]}"; do
    echo "The $fruit is ${fruits[$fruit]}"
done

# Pattern matching and substitution
filename="script.txt.bak"
echo "${filename%.bak}"    # Remove .bak suffix
echo "${filename#*.}"      # Remove everything before first dot

Flow Control and Error Handling

Advanced Flow Control

Here’s a more sophisticated example of flow control:

#!/usr/bin/env bash

process_file() {
    local file="$1"
    local -i retries=0
    local -i max_retries=3
    
    while (( retries < max_retries )); do
        if [[ -f "$file" ]]; then
            case "$file" in
                *.txt)
                    echo "Processing text file: $file"
                    # Add text processing logic
                    ;;
                *.json)
                    echo "Processing JSON file: $file"
                    # Add JSON processing logic
                    ;;
                *.csv)
                    echo "Processing CSV file: $file"
                    # Add CSV processing logic
                    ;;
                *)
                    echo "Unsupported file type: $file"
                    return 1
                    ;;
            esac
            return 0
        else
            ((retries++))
            echo "File not found, attempt $retries of $max_retries"
            sleep 1
        fi
    done
    
    echo "Failed to process file after $max_retries attempts"
    return 1
}

# Example usage
process_file "data.txt" || echo "Processing failed"

Advanced File Operations

Here’s a comprehensive example of file operations:

#!/usr/bin/env bash

# Function to process a directory recursively
process_directory() {
    local dir="$1"
    local depth="${2:-0}"
    local indent="$(printf '%*s' "$depth" '')"
    
    # Check if directory exists and is readable
    if [[ ! -d "$dir" ]] || [[ ! -r "$dir" ]]; then
        echo "Error: Cannot access directory $dir"
        return 1
    }
    
    # Process all files in the directory
    while IFS= read -r -d '' file; do
        if [[ -f "$file" ]]; then
            # Get file information
            local size="$(stat -f %z "$file")"
            local modified="$(stat -f %Sm "$file")"
            echo "${indent}File: $file"
            echo "${indent}Size: $size bytes"
            echo "${indent}Modified: $modified"
            
            # Process based on file type
            case "$(file -b "$file")" in
                *text*)
                    echo "${indent}Type: Text file"
                    # Add text processing logic
                    ;;
                *image*)
                    echo "${indent}Type: Image file"
                    # Add image processing logic
                    ;;
                *)
                    echo "${indent}Type: Other"
                    ;;
            esac
        elif [[ -d "$file" ]]; then
            echo "${indent}Directory: $file"
            # Recursively process subdirectories
            process_directory "$file" "$((depth + 2))"
        fi
    done < <(find "$dir" -print0)
}

# Example usage
process_directory "/path/to/directory"

Advanced Error Handling

Here’s a robust example of error handling in Bash:

#!/usr/bin/env bash

# Error handling function
handle_error() {
    local line_num="$1"
    local error_code="$2"
    local error_msg="$3"
    
    echo "Error occurred in script $0 at line $line_num"
    echo "Exit code: $error_code"
    echo "Error message: $error_msg"
    
    # Send error notification (example)
    if command -v mailx >/dev/null 2>&1; then
        echo "Script error in $0" | mailx -s "Script Error" [email protected]
    fi
    
    # Cleanup any temporary files
    cleanup
    
    exit "$error_code"
}

# Set error trap
trap 'handle_error ${LINENO} $? "$BASH_COMMAND"' ERR

# Example function that might fail
risky_operation() {
    local input="$1"
    
    if [[ -z "$input" ]]; then
        return 1
    fi
    
    # Simulate an operation that might fail
    if ! some_command "$input"; then
        return 2
    fi
    
    return 0
}

# Main script with error handling
main() {
    local result
    
    # Try risky operation
    if ! result=$(risky_operation "test input"); then
        echo "Operation failed: $result"
        return 1
    fi
    
    echo "Operation succeeded: $result"
    return 0
}

# Run main function
main "$@"

Network Operations

Here’s an example of network operations in Bash:

#!/usr/bin/env bash

# Function to check network connectivity
check_connectivity() {
    local host="$1"
    local timeout="${2:-5}"
    
    if ping -c 1 -W "$timeout" "$host" >/dev/null 2>&1; then
        echo "Host $host is reachable"
        return 0
    else
        echo "Host $host is not reachable"
        return 1
    fi
}

# Function to download file with retry
download_with_retry() {
    local url="$1"
    local output="$2"
    local max_retries="${3:-3}"
    local retry_count=0
    
    while (( retry_count < max_retries )); do
        if curl -L -o "$output" "$url"; then
            echo "Download successful: $output"
            return 0
        else
            ((retry_count++))
            echo "Download failed, attempt $retry_count of $max_retries"
            sleep $((retry_count * 2))
        fi
    done
    
    echo "Failed to download after $max_retries attempts"
    return 1
}

# Example usage
check_connectivity "google.com" || exit 1
download_with_retry "https://example.com/file.txt" "output.txt" 5

Best Practices and Tips

Code Organization

    1. Use functions to organize code into logical units
    1. Keep functions small and focused on a single task
    1. Use meaningful variable and function names
    1. Add comments to explain complex logic
    1. Use consistent indentation and formatting
    1. Follow the DRY (Don’t Repeat Yourself) principle

Security Considerations

    1. Always quote variables to prevent word splitting
    1. Use restricted paths when executing commands
    1. Validate user input before processing
    1. Avoid using eval unless absolutely necessary
    1. Use proper file permissions
    1. Be careful with temporary files

Performance Optimization

    1. Use built-in commands when possible
    1. Avoid unnecessary subshells
    1. Use arrays instead of string manipulation when working with lists
    1. Minimize external command calls
    1. Use proper exit codes and error handling

Common Patterns and Solutions

Configuration File Handling

#!/usr/bin/env bash

# Load configuration from file
load_config() {
    local config_file="$1"
    
    if [[ ! -f "$config_file" ]]; then
        echo "Config file not found: $config_file"
        return 1
    fi
    
    # Source the config file
    source "$config_file"
    
    # Validate required settings
    local required_vars=("API_KEY" "API_URL" "TIMEOUT")
    for var in "${required_vars[@]}"; do
        if [[ -z "${!var}" ]]; then
            echo "Missing required config variable: $var"
            return 1
        fi
    done
    
    return 0
}

# Example usage
load_config "/path/to/config.sh" || exit 1

Log Rotation

#!/usr/bin/env bash

# Log rotation function
rotate_logs() {
    local log_file="$1"
    local max_size="${2:-1048576}" # 1MB default
    local backup_count="${3:-5}"    # Keep 5 backups by default
    
    # Check if log file exists and exceeds max size
    if [[ -f "$log_file" ]] && [[ "$(stat -f %z "$log_file")" -gt "$max_size" ]]; then
        # Rotate existing backup logs
        for (( i=backup_count; i>0; i-- )); do
            [[ -f "${log_file}.$((i-1))" ]] && mv "${log_file}.$((i-1))" "${log_file}.$i"
        done
        
        # Move current log to .1
        mv "$log_file" "${log_file}.1"
        
        # Create new empty log file
        touch "$log_file"
        
        return 0
    fi
    
    return 1
}

# Example usage
rotate_logs "/var/log/myapp.log" 1048576 5

This enhanced guide provides more comprehensive examples and explanations of advanced Bash scripting concepts. Each section includes practical examples that can be used as templates for real-world applications.

Also see

0 Comments for this cheatsheet. Write yours!