A bash script is the most practical way to take the commands you type one by one in the terminal and run them all at once from a single file. Doing repetitive jobs by hand — cleaning logs on a server, taking backups, deploying a project, or renaming dozens of files — is both tedious and a source of mistakes. In this article you will learn to write small but genuinely useful automations using the core building blocks: variables, conditionals, and loops.
Your first script: the shebang and execute permission
Everything starts with a file. Create a hello.sh and put a shebang on the first line. This line tells the system which interpreter should run the file:
#!/usr/bin/env bash
echo "Hello, automation!"
You then need to make the file executable, grant it permission, and call the script:
chmod +x hello.sh
./hello.sh
Using #!/usr/bin/env bash finds the correct interpreter without hard-coding where bash happens to be installed. Don't forget that first line; otherwise the script falls back to whatever shell happens to invoke it.
Variables and user input
When you define a variable there must be no spaces around the equals sign. To read the value you prefix it with $ and almost always wrap it in double quotes:
#!/usr/bin/env bash
name="Aslain"
echo "Welcome, $name"
# Reading input from the user
read -r -p "What is your name? " answer
echo "Hi $answer"
The double quotes matter: writing "$file" prevents file names that contain spaces from being split apart. Leaving them off ($file) is a classic source of bugs. To capture command output into a variable, use $(...):
today="$(date +%Y-%m-%d)"
echo "Today's date: $today"
Conditionals: if, test, and comparisons
Automation is incomplete without "do this in that case" logic. In bash you build conditions with if and square brackets. For numeric comparisons use -eq, -gt, -lt; for strings use = and !=:
#!/usr/bin/env bash
read -r -p "Enter a number: " n
if [ "$n" -gt 10 ]; then
echo "$n is greater than ten"
elif [ "$n" -eq 10 ]; then
echo "Exactly ten"
else
echo "$n is less than ten"
fi
File and directory checks are the backbone of everyday scripts: -f asks whether a file exists, -d whether a directory exists, and -z whether a variable is empty:
if [ -d "/var/backups" ]; then
echo "Backup directory is ready"
else
mkdir -p /var/backups
fi
Loops: processing many files
Loops are where bash saves the most time. To iterate over the files in a folder you use for:
#!/usr/bin/env bash
for file in *.log; do
echo "Processing: $file"
gzip "$file"
done
You can also give it a numeric range to repeat a fixed number of times. When you want to loop until a condition is met, reach for while:
# From 1 to 5
for i in {1..5}; do
echo "Attempt $i"
done
# Reading a file line by line
while IFS= read -r line; do
echo "Line: $line"
done < list.txt
The IFS= read -r pattern is the safe way to read lines: it preserves leading and trailing whitespace and does not mangle backslashes.
Functions and arguments
As a script grows, turning repeated parts into functions improves readability. You access arguments passed to the script with $1, $2, and all of them at once with $@:
#!/usr/bin/env bash
log() {
echo "[$(date +%H:%M:%S)] $1"
}
backup() {
local source="$1"
local target="$2"
cp -r "$source" "$target" && log "Backed up: $target"
}
backup "$1" "/var/backups"
Using local inside a function confines variables to that function, so you don't accidentally clobber the rest of the script.
Robust scripts: error handling
Professional scripts don't quietly produce wrong results. Adding this line near the top should become a habit:
set -euo pipefail
These three settings work together: -e stops the script when a command fails, -u treats the use of an undefined variable as an error, and pipefail catches a failure in any command of a pipeline. Running your script through a static analysis tool such as ShellCheck before shipping it also catches quoting and variable mistakes early.
Frequently Asked Questions
What is the difference between bash and sh?
sh refers to the older, more limited POSIX shell; bash is a superset that offers arrays, [[ ]] tests, and richer syntax. If your script starts with #!/usr/bin/env bash, you can use bash features with confidence.
How do I debug my script?
The quickest method is to run it with bash -x script.sh, which prints each command before it executes. Alternatively you can put set -x inside the script and turn it off with set +x, tracing only the suspicious section.
Is bash enough for large automations?
Bash is excellent for file operations and chaining commands. But when you need complex data structures, JSON processing, or heavy logic, moving to a language like Python is more maintainable. A good rule of thumb: if you pass 100 lines with lots of branching logic, consider switching languages.
If you want to automate repetitive work but aren't sure where to start, from server scripts to deploy pipelines, get in touch with me for a practical setup.