If you’ve ever found yourself typing the same commands into your terminal over and over, you’re ready to learn how to write bash scripts. Trust me, I’ve been there. My first Arch install back in the day involved so much manual repetition that I swore I’d never type the same sequence twice again. Bash scripting changed everything for me, and it’ll do the same for you.
In this guide, I’ll walk you through everything you need to start writing your own scripts. We’ll cover the basics, dive into variables and loops, and I’ll share some hard-won lessons from years of breaking (and fixing) my own systems.
What is a Bash Script (And Why Every Linux User Should Learn to Write Them)
The Simple Definition: Automating Commands in a File
A bash script is just a text file containing shell commands. That’s it. When you run the script, bash executes each command in order, as if you typed them yourself. The magic is that you only write the commands once.
Think of it like a recipe. Instead of remembering every ingredient and step each time you cook, you write it down once. Then you just follow the recipe. Your script becomes the recipe for your computer.
Why Bash Scripts Matter for System Administration
Every sysadmin I know lives by one rule: automate everything. Scripts save time from the very first run. They eliminate human error. They ensure consistency across systems.
Get a VPS from as low as $11/year! WOW!
Whether you’re backing up files, monitoring disk space, or managing users, a script handles it the same way every time. No typos. No forgotten steps. Just reliable execution.
My First Script Disaster (And What It Taught Me About Testing)
I need to tell you about the time I learned why testing matters. Early in my Linux journey, I wrote a cleanup script that was supposed to remove old log files. Instead, it wiped my entire home directory. I’d forgotten to quote a variable that contained a space, and bash interpreted it as separate arguments.
The command that ran wasn’t what I intended. I lost a weekend of work. But I gained a lesson I’ve never forgotten: always test scripts on sample data first. Always quote your variables. Always.
Setting Up Your First Bash Script
The Shebang Line: Why #!/bin/bash Matters
Every script starts with a shebang. This special line tells your system which interpreter to use:
#!/bin/bash
You might also see #!/usr/bin/env bash, which is more portable. It finds bash wherever it’s installed in your PATH. Either works, but env is safer across different Linux distributions.
Making Your Script Executable with chmod
After saving your script, you need to make it executable. This is where the chmod command comes in:
chmod +x myscript.sh
Now you can run it with ./myscript.sh. Without this step, you’d get a “permission denied” error.
Choosing the Right Text Editor
Use whatever editor you’re comfortable with. I use vim (yes, I’m one of those people), but nano works great for beginners. The editor doesn’t matter nearly as much as what you write in it.
Save your scripts with a .sh extension. It’s not required, but it makes your life easier.
Bash Script Basics: Variables and User Input
Creating and Using Variables
Variables store data you want to reuse. Here’s the critical syntax rule: no spaces around the equals sign.
# Correct
name="Alexa"
# Wrong - this will fail
name = "Alexa"
Access variables with a dollar sign: $name or ${name}. The curly braces version is safer when you’re combining variables with other text.
Get in the habit of writing
"${variable}" instead of just $variable. Unquoted variables cause word splitting and glob expansion, leading to unexpected behavior when values contain spaces or special characters.
Reading User Input with read Command
Interactive scripts often need user input:
#!/bin/bash
echo "What's your name?"
read username
echo "Hello, ${username}!"
The read command pauses and waits for input. Whatever the user types gets stored in the variable.
Command Substitution and Exit Codes
You can capture command output using $(command):
current_date=$(date +%Y-%m-%d)
echo "Today is ${current_date}"
Every command returns an exit code. Zero means success. Anything else means failure. Check it with $?:
grep "error" logfile.txt
if [[ $? -eq 0 ]]; then
echo "Found errors in log"
fi
Understanding environment variables helps you build more powerful scripts that interact with your system.
Control Flow: Making Decisions in Your Scripts
If Statements and Conditional Testing
Scripts need to make decisions. The if statement handles this:
if [[ -f "/path/to/file" ]]; then
echo "File exists"
else
echo "File not found"
fi
Common test operators include:
- -f: File exists and is regular file
- -d: Directory exists
- -z: String is empty
- -n: String is not empty
- -eq, -ne, -lt, -gt: Numeric comparisons
Using [[ ]] vs [ ] for Safer Conditionals
Always use double brackets [[ ]] instead of single brackets [ ]. Double brackets are bash-specific and handle edge cases better. They prevent word splitting inside the condition, so you won’t get errors when variables contain spaces.
Case Statements for Multiple Conditions
When you have many possible values, case statements are cleaner than long if-elif chains:
case "${input}" in
start)
echo "Starting service..."
;;
stop)
echo "Stopping service..."
;;
*)
echo "Unknown command"
;;
esac
Loops: Automating Repetitive Tasks
For Loops: Iterating Over Lists
For loops process items one at a time:
for server in web1 web2 web3; do
echo "Pinging ${server}..."
ping -c 1 "${server}"
done
While Loops: Conditional Repetition
While loops continue as long as a condition is true:
counter=1
while [[ ${counter} -le 5 ]]; do
echo "Count: ${counter}"
((counter++))
done
Looping Through Files the Right Way
Here’s a beginner mistake I see constantly: using ls output in a loop. Don’t do it. Filenames with spaces break everything.
# Wrong - breaks on filenames with spaces
for file in $(ls *.txt); do
echo "${file}"
done
# Correct - use globbing
for file in *.txt; do
echo "${file}"
done
The grep command pairs well with loops for filtering and processing text files.
Functions: Writing Reusable Code
Defining and Calling Functions
Functions let you reuse code blocks:
greet_user() {
echo "Hello, $1!"
}
greet_user "Alexa"
greet_user "Reader"
Function Parameters and Return Values
Access function arguments with $1, $2, etc. Use $@ for all arguments (always quoted: "$@").
Functions can return numeric exit codes with return, or output strings with echo that you capture with command substitution.
get_count() {
local count=$(ls | wc -l)
echo "${count}"
}
file_count=$(get_count)
echo "Found ${file_count} files"
Notice the local keyword. It keeps variables inside the function, preventing them from polluting your global namespace.
When to Use Functions vs Separate Scripts
If a code block appears more than once, make it a function. If it’s a standalone utility you’ll use across multiple scripts, make it a separate script.
Error Handling and Debugging
Using set -e and set -u for Safer Scripts
Production scripts need safety guards. Add these at the top:
#!/bin/bash
set -e # Exit on error
set -u # Treat unset variables as errors
With set -e, your script stops if any command fails. With set -u, referencing an undefined variable throws an error instead of silently becoming empty.
Redirecting Errors to stderr
Error messages belong on stderr, not stdout:
echo "Error: File not found" >&2
exit 1
This lets users redirect normal output and errors separately.
Debugging with set -x
When scripts misbehave, set -x shows each command before it runs:
#!/bin/bash
set -x
# Every command will be printed before execution
This saved me countless hours troubleshooting my homelab automation scripts.
Common Mistakes Beginners Make (And How to Avoid Them)
I’ve made all of these. Learn from my pain.
Not Quoting Variables
Unquoted variables cause word splitting. A filename like “my file.txt” becomes two arguments: “my” and “file.txt”. Always use "${variable}".
Ignoring Error Checking
Never assume commands succeed. Check exit codes, especially before destructive operations:
cd "/some/directory" || { echo "Failed to cd" >&2; exit 1; }
rm -rf ./*
Without that check, if cd fails, rm runs in the wrong directory. I don’t need to explain why that’s bad.
Using Unsafe Patterns
Never use rm -rf ${dir}/* without validating that ${dir} is set and correct. Check the common bash pitfalls reference for more examples of dangerous patterns.
Best Practices for Production-Ready Scripts
Using ShellCheck to Validate Your Code
ShellCheck is essential. It catches bugs before you run your script. Install it locally or paste your code on the website. I run it on every script before deployment.
“ShellCheck finds bugs in your shell scripts. It points out and clarifies typical beginner’s syntax issues, intermediate level semantic problems that cause a shell to behave strangely and counter-intuitively, and subtle caveats, corner cases and pitfalls.”
Adding Comments and Documentation
Future you will thank present you for comments:
#!/bin/bash
# Description: Backup user home directories
# Usage: ./backup.sh [username]
# Author: Alexa
# Last updated: 2025-09-15
Comment complex logic. Explain the “why” not just the “what.”
Structuring Scripts for Maintainability
Keep scripts focused. If a script exceeds 50 lines, consider breaking it into functions. Use meaningful variable names. Consistent indentation matters.
Tools like sed and awk become powerful allies when processing text in your scripts.
Real-World Bash Script Examples
Example 1: Automated Backup Script
#!/bin/bash
set -eu
backup_dir="/backups"
source_dir="${HOME}"
timestamp=$(date +%Y%m%d_%H%M%S)
mkdir -p "${backup_dir}"
tar -czf "${backup_dir}/home_${timestamp}.tar.gz" "${source_dir}" 2>/dev/null
echo "Backup created: ${backup_dir}/home_${timestamp}.tar.gz"
Example 2: System Monitoring Script
#!/bin/bash
set -eu
threshold=80
usage=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
if [[ ${usage} -gt ${threshold} ]]; then
echo "Warning: Disk usage at ${usage}%" >&2
exit 1
fi
echo "Disk usage OK: ${usage}%"
Example 3: Batch File Renaming
#!/bin/bash
set -eu
for file in *.JPG; do
[[ -f "${file}" ]] || continue
newname="${file%.JPG}.jpg"
mv "${file}" "${newname}"
echo "Renamed: ${file} -> ${newname}"
done
Once your scripts are ready, scheduling scripts with cron lets them run automatically. Check checking system compatibility when writing scripts that need to work across different Linux distributions.
- Start with
#!/bin/bash - Add
set -eufor safety - Quote all variables:
"${var}" - Use
[[ ]]for conditionals - Run ShellCheck before deploying
- Test on sample data first
Frequently Asked Questions
How long does it take to learn bash scripting?
You can write useful scripts within a week of practice. Mastery takes longer, but functional automation is quickly achievable. Start with simple tasks and build complexity gradually.
Should I learn bash or Python for automation?
Learn both. Bash excels at quick file operations and command chaining. Python handles complex logic and data processing better. For pure system administration, bash is essential.
Where should I save my scripts?
Personal scripts go in ~/bin (add it to your PATH). System-wide scripts belong in /usr/local/bin. Keep them organized and backed up.
Start Scripting Today
You now have everything you need to write your first bash script. Start simple. Automate one task you do repeatedly. Then another. Before long, you’ll wonder how you ever managed without scripts.
For deeper reference, the official Bash reference manual covers every edge case and feature.
Want to level up your Linux skills further? Check out our guides on automating tasks with cron or working with environment variables. Each builds on the scripting foundation you’ve started here.
Now go break something. Then fix it. That’s how you learn.




