Bash Scripting Basics: A Beginner's Guide
Bash scripting automates repetitive tasks on Linux. Whether you need to back up files, deploy applications, process logs, or orchestrate system maintenance, a well-written Bash script turns multi-step procedures into a single command. Bash is the default shell on most Linux distributions and macOS, making it the most portable option for cross-platform automation.
Scripting in Bash has a different feel from general-purpose languages like Python. The syntax is terse, the error messages are cryptic, and quoting rules can trip up newcomers. But once you internalize the core patterns — variables, conditionals, loops, and functions — you will be able to automate almost any command-line workflow.
Your First Script
Every Bash script starts with a shebang and a command:
#!/bin/bash
echo "Hello, World!"Save as hello.sh, then:
chmod +x hello.sh
./hello.shThe first line (#!/bin/bash) is the shebang — it tells the system which interpreter to use. Always use #!/bin/bash rather than #!/bin/sh for portability, as sh may point to dash (Debian/Ubuntu) which lacks Bash-specific features like arrays and [[ ]] conditionals. Make the script executable with chmod +x so the kernel recognizes it as an executable. If you omit the shebang, the system will try to run the script with the user’s default shell, which may differ.
Best Practices for Shebang
- Use
#!/bin/bashfor full Bash compatibility. - Use
#!/usr/bin/env bashfor environments where Bash is installed outside/bin(like some macOS setups). - Always end the shebang with a newline.
Variables
Variables in Bash are untyped — everything is a string. Assignment must have no spaces around =.
#!/bin/bash
# Assigning variables (no spaces around =)
name="Alice"
age=30
# Using variables (use quotes to handle spaces)
echo "$name is $age years old"
# Command substitution
files=$(ls)
echo "Files in current directory: $files"
# User input
read -p "Enter your name: " username
echo "Hello, $username"Double-quoting variables ("$name") is essential. Without quotes, the value is split into words and each word is expanded as a glob pattern. If name contains spaces, echo $name would pass multiple arguments and potentially expand wildcards. Always double-quote variable expansions unless you explicitly want word splitting and glob expansion.
Special Variables
| Variable | Meaning |
|---|---|
$0 | Script name |
$1–$9 | Positional arguments |
$# | Number of arguments |
$@ | All arguments as separate words |
$? | Exit status of last command |
$$ | Process ID of the script |
$! | PID of last background process |
Default Values
Set default values for variables that might be unset:
name=${1:-"World"} # Use $1 if set, otherwise "World"
echo "Hello, $name"Conditionals
Bash conditionals use the [ ] test command or the more modern [[ ]] keyword.
#!/bin/bash
# If-else
if [ "$1" = "hello" ]; then
echo "Hello to you too!"
elif [ "$1" = "bye" ]; then
echo "Goodbye!"
else
echo "I don't understand"
fi
# File checks
if [ -f "/etc/passwd" ]; then
echo "File exists"
fi
if [ -d "/home/user" ]; then
echo "Directory exists"
fi
# Numeric comparisons
count=5
if [ "$count" -gt 3 ]; then
echo "Count is greater than 3"
fiPrefer [[ ]] over [ ] for Bash scripts. The [[ ]] keyword adds several improvements: it does not word-split variables, supports regex matching with =~, and allows &&/|| operators instead of -a/-o. However, [[ ]] is a Bash extension and will not work in sh.
Comparison operators:
| Operator | Meaning |
|---|---|
-eq | Equal |
-ne | Not equal |
-gt | Greater than |
-lt | Less than |
-z | String is empty |
-n | String is not empty |
String vs Numeric Comparison
A common source of bugs: use = or == for string comparison, -eq for integer comparison. Using -eq with a non-numeric string will produce an error.
File Test Operators
| Operator | True if |
|---|---|
-f | File exists and is regular |
-d | Directory exists |
-e | File exists (any type) |
-r / -w / -x | Readable / Writable / Executable |
-s | File exists and is non-empty |
-nt / -ot | Newer than / Older than |
Loops
Bash gives you several loop constructs for iterating over lists, ranges, files, and command output.
#!/bin/bash
# For loop over a list
for name in Alice Bob Charlie; do
echo "Hello, $name"
done
# For loop with range
for i in {1..5}; do
echo "Number: $i"
done
# While loop
count=1
while [ "$count" -le 5 ]; do
echo "Count: $count"
((count++))
done
# Loop over files
for file in *.txt; do
echo "Processing $file"
doneThe {1..5} brace expansion creates a range. You can also specify a step: {0..10..2} yields 0 2 4 6 8 10. The C-style for loop (for ((i=0; i<5; i++))) is also available in Bash for more complex iteration patterns.
Loop Control
break— exit the loop immediately.continue— skip to the next iteration.- Use
((expression))for arithmetic:((count++)),((total += 10)).
Functions
Functions group reusable logic. Unlike most languages, Bash functions do not return values — they set the exit status. Use echo to output a value and capture it with command substitution.
#!/bin/bash
# Define a function
greet() {
local name="$1" # $1 is the first argument
echo "Hello, $name!"
---
# Function with return value
add() {
local sum=$(( $1 + $2 ))
echo "$sum" # Use echo to "return" a value
---
# Call functions
greet "Alice"
result=$(add 5 3)
echo "5 + 3 = $result"The local keyword scopes variables to the function, preventing accidental overwrites of global variables. Always use local inside functions. To return a numeric status code (0 for success, non-zero for error), use return.
Arguments and Special Variables
Access command-line arguments with $1, $2, etc. Shift through arguments with the shift command.
#!/bin/bash
echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "All arguments: $@"
echo "Number of arguments: $#"
echo "Exit status of last command: $?"
echo "Process ID: $$"For robust argument parsing, use a while loop with case:
while [ $# -gt 0 ]; do
case "$1" in
-f|--file) FILE="$2"; shift ;;
-v|--verbose) VERBOSE=true ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
shift
doneReading Files
Process files line by line. Always use IFS= and -r to preserve whitespace and backslashes.
#!/bin/bash
# Line by line
while IFS= read -r line; do
echo "Line: $line"
done < "input.txt"
# Read CSV
while IFS=',' read -r name age city; do
echo "$name is $age years old and lives in $city"
done < "data.csv"Setting IFS= (empty) prevents read from stripping leading/trailing whitespace. The -r flag prevents backslash interpretation. This pattern handles any text file, including those with spaces, tabs, or special characters.
Error Handling
Bash does not stop on errors by default. Enable strict mode and check exit statuses explicitly.
#!/bin/bash
# Exit on error
set -e
# Exit on undefined variable
set -u
# Print commands before executing (debug mode)
set -x
# Check exit status
if ! mkdir "/backup"; then
echo "Error: Could not create backup directory"
exit 1
fiStrict Mode
A common combination is set -euo pipefail:
-e: exit on any command failure.-u: treat unset variables as an error.-o pipefail: a pipeline fails if any command in it fails (not just the last).
Add this at the top of every script after the shebang. But be aware that set -e has surprising edge cases — for example, a command inside an if condition will not trigger an exit, and grep returning no matches exits with status 1.
The trap Command
Run cleanup code when the script exits, regardless of success or failure:
cleanup() {
rm -f /tmp/tempfile
echo "Cleaned up"
---
trap cleanup EXITUse trap to delete temporary files, kill background processes, or restore terminal settings when the script ends.
Real-World Examples
Backup Script
Creates timestamped backups of important directories:
#!/bin/bash
BACKUP_DIR="/backup/$(date +%Y-%m-%d)"
mkdir -p "$BACKUP_DIR"
tar -czf "$BACKUP_DIR/home.tar.gz" /home/user
tar -czf "$BACKUP_DIR/etc.tar.gz" /etc
echo "Backup completed: $BACKUP_DIR"For production use, add error checking, log output to a file, and set up a cron job to run this automatically. Use set -euo pipefail to catch failures early.
Log Rotation
Rotate a log file when it exceeds a size threshold:
#!/bin/bash
LOG_FILE="/var/log/app.log"
MAX_SIZE=10485760 # 10MB in bytes
file_size=$(stat -f%z "$LOG_FILE" 2>/dev/null || stat --printf="%s" "$LOG_FILE" 2>/dev/null)
if [ "$file_size" -gt "$MAX_SIZE" ]; then
mv "$LOG_FILE" "$LOG_FILE.$(date +%Y%m%d-%H%M%S)"
touch "$LOG_FILE"
echo "Log rotated"
fiThis example demonstrates cross-platform stat syntax (macOS uses -f%z, Linux uses --printf="%s"). In production, consider using logrotate instead, which handles compression, retention policies, and scheduled execution.
Debugging Techniques
When a script does not work as expected, enable debug output:
# Run with debug mode
bash -x script.sh
# Or add inside the script
set -x # start debugging
# ... code ...
set +x # stop debuggingOther useful techniques: add echo statements to print variable values, use ls -la to verify file existence, and run individual commands from the script manually in the terminal to test them in isolation.
Related: See our Linux command cheat sheet and cron jobs guide.
Frequently Asked Questions
What is the minimum system requirement for bash scripting?
System requirements vary by implementation. Most modern solutions require at least 4GB of RAM, a multi-core processor, and a stable internet connection. For specific applications, refer to the vendor documentation. Hardware requirements typically increase with scale — enterprise deployments need significantly more resources than personal or small business setups.
How does this compare to alternative approaches?
Every technology choice involves trade-offs. Some prioritize ease of use over customization, while others offer maximum control at the cost of complexity. Evaluating your specific needs, technical expertise, and growth plans helps determine the right fit. Many organizations use a combination of approaches to balance competing priorities.
What security considerations should I be aware of?
Security should be considered from the start, not as an afterthought. Keep all software updated, use strong authentication, encrypt sensitive data, and follow the principle of least privilege. Regular security audits and staying informed about emerging threats are essential practices for maintaining a secure deployment.
How do I troubleshoot common issues?
Start by isolating the problem: check logs, verify configurations, and test components individually. Common issues include network connectivity problems, permission errors, and version incompatibilities. Systematic troubleshooting — changing one variable at a time — helps identify root causes efficiently. Online communities and documentation are valuable resources when you encounter unfamiliar problems.