Bash Scripting Cheatsheet
Master Bash scripting essentials for automation, system administration, and command-line tasks, from variables to control flow.
· 12 min read · AI-reviewed
## Quick Overview
Bash scripting provides a powerful way to automate repetitive tasks, perform system administration, and extend the capabilities of your command line. It's the default shell for most Linux distributions and macOS, making it a universal tool for developers. You'll reach for Bash when you need to orchestrate commands, process files, manage processes, or build simple CLI tools. This cheatsheet covers the core concepts and practical syntax you'll need to write effective Bash scripts. Bash 5.x is commonly available and offers a rich set of features, though many concepts apply broadly across versions. Most systems come with Bash pre-installed.
## Getting Started
To write and run your first Bash script, you need a text editor and a terminal.
1. **Create a script file**:
```bash
# Create a new file named my_script.sh
touch my_script.sh
```
2. **Add a Shebang and content**: The `#!` (shebang) line tells your system which interpreter to use.
```bash
#!/bin/bash
# my_script.sh
echo "Hello, Z2H!" # Print a string to standard output
```
3. **Make it executable**:
```bash
# Grant execute permissions to the script owner
chmod +x my_script.sh
```
4. **Run the script**:
```bash
# Execute the script
./my_script.sh
```
## Core Concepts
| Concept | Description | Example |
| :---------------- | :------------------------------------------------------------------------ | :---------------------------------------- |
| **Shebang** | `#!/bin/bash` specifies the interpreter. Always the first line. | `#!/bin/bash` |
| **Permissions** | `chmod +x` makes a script executable. `chmod 755` is common. | `chmod +x script.sh` |
| **Variables** | Stores data. Referenced with `$`. No spaces around `=`. | `NAME="Hero"` <br> `echo $NAME` |
| **Input/Output** | `stdin` (0), `stdout` (1), `stderr` (2). Redirected using `>`, `<`, `2>`. | `ls > files.txt` <br> `cat < input.txt` |
| **Control Flow** | `if`, `for`, `while`, `case` statements for conditional execution/loops. | `if [ -f "file" ]; then ... fi` |
| **Functions** | Reusable blocks of code. Defined with `function_name () { ... }`. | `greet() { echo "Hello"; }` |
| **Exit Codes** | A number (0-255) returned by a command/script indicating success (0) or failure (non-zero). | `echo $?` (last command's exit code) |
| **Quoting** | Single (`''`) prevents expansion; Double (`""`) allows expansion. | `echo '$VAR'` vs `echo "$VAR"` |
| **Piping** | `|` connects `stdout` of one command to `stdin` of another. | `ls -l | grep ".txt"` |
## Essential Commands / Syntax
### Variables
```bash
#!/bin/bash
# Assigning a variable (no spaces around '=')
NAME="Alice"
AGE=30
# Accessing a variable
echo "Hello, $NAME. You are $AGE years old." # Output: Hello, Alice. You are 30 years old.
# Read-only variable
readonly PI=3.14159
# PI=3.0 # This would cause an error
# Unsetting a variable
unset AGE
# echo "Age after unset: $AGE" # Output: Age after unset:
# Command substitution: capture output of a command
CURRENT_DIR=$(pwd)
echo "Current directory: $CURRENT_DIR"
# Arithmetic expansion
NUM1=10
NUM2=5
SUM=$((NUM1 + NUM2))
echo "Sum: $SUM" # Output: Sum: 15
# Arrays (indexed from 0)
FRUITS=("Apple" "Banana" "Cherry")
echo "First fruit: ${FRUITS[0]}" # Output: First fruit: Apple
echo "All fruits: ${FRUITS[@]}" # Output: All fruits: Apple Banana Cherry
echo "Number of fruits: ${#FRUITS[@]}" # Output: Number of fruits: 3
Script Arguments
#!/bin/bash
# script_args.sh
echo "Script name: $0" # The name of the script itself
echo "First argument: $1"
echo "Second argument: $2"
echo "All arguments as a single string: $*"
echo "All arguments as separate strings: $@"
echo "Number of arguments: $#"
echo "Last command's exit status: $?" # The exit status of the previously executed command
echo "Process ID of the current shell: $$"
echo "Process ID of the last background command: $!"
# How to run:
# ./script_args.sh hello world
# Output:
# Script name: ./script_args.sh
# First argument: hello
# Second argument: world
# All arguments as a single string: hello world
# All arguments as separate strings: hello world
# Number of arguments: 2
# Last command's exit status: 0
# Process ID of the current shell: <PID>
# Process ID of the last background command:
Conditionals (if, test, [[ ]])
#!/bin/bash
# Basic if statement
if [ "$1" == "hello" ]; then
echo "Argument is hello!"
fi
# if-else statement
if [ -f "$1" ]; then # Checks if $1 is a file
echo "$1 is a regular file."
else
echo "$1 is NOT a regular file."
fi
# if-elif-else statement
if [ "$2" -lt 10 ]; then # Checks if $2 is less than 10 (numeric comparison)
echo "$2 is less than 10."
elif [ "$2" -eq 10 ]; then
echo "$2 is equal to 10."
else
echo "$2 is greater than 10."
fi
# [[ ]] for more advanced conditional expressions (Bash specific)
# Supports regex matching, logical AND/OR (`&&`, `||`)
if [[ "$1" =~ ^[Hh]ello$ && "$#" -eq 1 ]]; then # Regex match and argument count
echo "Greeting pattern matched with one argument."
fi
# Common test operators:
# -f FILE: True if FILE exists and is a regular file.
# -d FILE: True if FILE exists and is a directory.
# -z STRING: True if the length of STRING is zero.
# -n STRING: True if the length of STRING is non-zero.
# STRING1 == STRING2: True if the strings are equal.
# STRING1 != STRING2: True if the strings are not equal.
# INT1 -eq INT2: True if INT1 is equal to INT2.
# INT1 -ne INT2: True if INT1 is not equal to INT2.
# INT1 -gt INT2: True if INT1 is greater than INT2.
# INT1 -ge INT2: True if INT1 is greater than or equal to INT2.
# INT1 -lt INT2: True if INT1 is less than INT2.
# INT1 -le INT2: True if INT1 is less than or equal to INT2.
Loops (for, while, until)
#!/bin/bash
# For loop (C-style) - New in Bash 4.x, older Bash versions may not support
echo "C-style for loop:"
for (( i=0; i<3; i++ )); do
echo "Index: $i"
done
# For loop (iterate over a list/array)
echo "Iterating over a list:"
for ITEM in "apple" "banana" "cherry"; do
echo "Fruit: $ITEM"
done
# For loop (iterate over command output)
echo "Iterating over files in current directory:"
for FILE in *.txt; do
echo "Processing $FILE"
done
# While loop
COUNT=0
echo "While loop:"
while [ $COUNT -lt 3 ]; do
echo "Count: $COUNT"
COUNT=$((COUNT + 1))
done
# Until loop (runs as long as condition is FALSE)
NUMBER=5
echo "Until loop:"
until [ $NUMBER -eq 0 ]; do
echo "Number: $NUMBER"
NUMBER=$((NUMBER - 1))
done
Functions
#!/bin/bash
# Define a function
greet_user() {
echo "Hello, $1!"
return 0 # 0 for success
}
# Call a function
greet_user "World" # Output: Hello, World!
# Function with local variables
calculate_sum() {
local A=$1 # 'local' keyword limits variable scope to the function
local B=$2
local RESULT=$((A + B))
echo "The sum is: $RESULT"
return $RESULT # Functions can return an exit status (0-255)
}
calculate_sum 10 20 # Output: The sum is: 30
echo "Exit status of calculate_sum: $?" # Output: Exit status of calculate_sum: 30 (if sum < 256)
Input/Output Redirection & Piping
#!/bin/bash
# Redirect stdout to a file (overwrites if exists)
echo "This will be in output.txt" > output.txt
# Redirect stdout to a file (appends if exists)
echo "This will be appended" >> output.txt
# Redirect stderr to a file
ls non_existent_file 2> error.log
# Redirect both stdout and stderr to a file (Bash 4+)
# Newer syntax: command &> file
# Older syntax: command > file 2>&1
echo "Both stdout and stderr" &> combined.log
# Example for error & stdout
ls -l /tmp/doesnotexist /tmp 2>&1 | grep -i "no such file\|total"
# Here document for multi-line input
cat << EOF > multi_line.txt
This is the first line.
This is the second line.
EOF
# Pipe output of one command to input of another
ls -l | grep ".sh" # Lists .sh files in current directory
Common Patterns
Reading User Input
#!/bin/bash
echo "Please enter your name:"
read USER_NAME # Reads input into USER_NAME
echo "Hello, $USER_NAME!"
read -p "Enter your age: " USER_AGE # -p for prompt
echo "You are $USER_AGE years old."
read -s -p "Enter a secret password: " USER_PASS # -s for silent (no echo)
echo # Newline after silent input
echo "Password received (but not displayed)."
Error Handling with set -e
Use set -e at the top of your script to exit immediately if any command fails (returns a non-zero exit status). This is crucial for robust scripts.
#!/bin/bash
set -e # Exit immediately if a command exits with a non-zero status.
echo "Starting script..."
# This command will succeed
cp /etc/hosts /tmp/hosts_copy
echo "Copied hosts file."
# This command will fail, and the script will exit here
cp non_existent_file /tmp/backup_fail
# This line will never be reached because the script exited
echo "This line will not be printed."
Logging to File and Console
#!/bin/bash
LOG_FILE="script.log"
# Function to log messages
log_message() {
local MESSAGE="$1"
echo "$(date '+%Y-%m-%d %H:%M:%S') - $MESSAGE" | tee -a "$LOG_FILE"
}
log_message "Script started."
log_message "Performing task 1..."
# Simulate a task
sleep 1
log_message "Task 1 completed."
# Example with an error
if [ ! -d "/nonexistent/path" ]; then
log_message "ERROR: Directory '/nonexistent/path' does not exist."
exit 1
fi
log_message "Script finished."
Gotchas & Tips
- Quoting is Critical: Always quote your variables (
"$VAR") unless you specifically want word splitting and globbing. Unquoted variables are a common source of bugs and security vulnerabilities."$VAR": Preserves spaces, prevents globbing, and treats the variable as a single argument.'$VAR': Treats$VARas a literal string.
- Word Splitting: When an unquoted variable is expanded, Bash splits the resulting string into words based on the
IFS(Internal Field Separator) variable (default: space, tab, newline).MY_VAR="one two three" for WORD in $MY_VAR; do echo "$WORD"; done # Output: one, two, three (not "one two three") for WORD in "$MY_VAR"; do echo "$WORD"; done # Output: one two three - Globbing (Filename Expansion): Unquoted variables containing
*,?, or[]can be expanded into filenames.LS_OPT="*.txt" # ls $LS_OPT # Expands to all .txt files # ls "$LS_OPT" # Tries to list a file named "*.txt" set -u(orset -o nounset): Exits the script if an unset variable is used. Good for catching typos.#!/bin/bash set -u # echo "$UNSET_VAR" # This would cause the script to exit with an error. echo "Script continues if no unset variables are accessed."set -o pipefail: Ensures that a pipeline’s return status is the rightmost command that exited with a non-zero status, or zero if all commands exited successfully. Without this, a pipeline returns the exit status of the last command, even if an earlier command failed.#!/bin/bash set -e set -o pipefail # This will fail and exit the script due to `pipefail` # if ! grep "non-existent-pattern" /etc/hosts | cat > /dev/null; then # echo "Grep failed, and script exited." # fi echo "No grep failed."- Bash vs. POSIX Shell: Many features like
[[ ]],&>, arrays, and C-styleforloops are Bash-specific. If portability to other shells (likeshordash) is required, stick to POSIX shell features (e.g.,test,[ ]). evalis Dangerous: Avoid usingevalunless absolutely necessary and you fully control its input. It executes arbitrary strings as commands, posing a major security risk.- Command Substitution
$(...)vs. Backticks`...`:$(...)is generally preferred as it’s nestable and easier to read. - Exit Codes: Always check exit codes (
$?) after critical commands. A non-zero code means something went wrong.
Next Steps
- Official Bash Manual: The definitive guide for all Bash features. GNU Bash Manual
- ShellCheck: A static analysis tool for shell scripts that finds common problems. Essential for writing robust scripts. ShellCheck.net
- Advanced Bash-Scripting Guide: A comprehensive resource for learning more in-depth Bash concepts and patterns. Search for “Advanced Bash-Scripting Guide” online.
- z2h.fyi/cheatsheets/linux-cli: For more general Linux command line utilities that often integrate with Bash scripts.
Source: z2h.fyi/cheatsheets/bash-scripting-cheatsheet — Zero to Hero cheatsheets for developers.