Bash Script for Beginners: First Script, Variables, Conditions, Loops and cron
A bash script is a text file of shell commands that starts with the line #!/bin/bash, gets execute permission through chmod +x and is run as ./script.sh. Scripts use variables, the arguments $1 and $2, if conditions, for and while loops, and regular scheduled runs are set up with crontab -e.
A bash script is a plain text file with commands that you would otherwise type into the terminal one by one. The script starts with the shebang line #!/bin/bash, gets execute permission through chmod +x and is run as ./script.sh. This guide is a bash script for beginners from the first line to a scheduled run: variables, arguments, conditions, loops, error handling and cron.
Your first bash script in a minute
Create a file in any editor, for example nano:
nano hello.sh
#!/bin/bash
# First script: a greeting and some system information
echo "Hello, $USER!"
echo "Today: $(date +%Y-%m-%d)"
echo "You are in: $(pwd)"
Save it (Ctrl+O, Enter, Ctrl+X), make it executable and run it:
$ chmod +x hello.sh
$ ./hello.sh
Hello, alice!
Today: 2026-10-12
You are in: /home/alice
Lines starting with # are comments. $(command) inserts the output of a command directly into a line.
Shebang: why the first line matters
The line #!/bin/bash tells the system which interpreter should run the file. Without it the script runs in whatever shell is at hand, and on some systems /bin/sh is not bash but the simpler dash, where some constructs do not work.
The variant #!/usr/bin/env bash is also common: it looks for bash in PATH, which is handy when bash is not installed in /bin.
How to run a script and why ./
There are two ways:
./hello.sh— needs the shebang and thexpermission. The./prefix is required, because the current directory is not inPATH.bash hello.sh— no execute permission needed, the interpreter is specified explicitly.
If you see command not found without ./, or Permission denied without chmod +x, these are exactly the two causes. To call your scripts by name from anywhere, put them in ~/bin or /usr/local/bin.
Variables
A variable is assigned without spaces around =, and read with $:
#!/bin/bash
name="backup"
dir="/var/backups"
today=$(date +%F)
echo "File: ${dir}/${name}-${today}.tar.gz"
Curly braces separate the variable name from the text around it. The line name = "backup" with spaces is an error: bash will try to run a command called name.
Quotes matter
Inside double quotes variables are expanded; inside single quotes they are not. Variables should almost always be put in double quotes, otherwise a file name with a space will be split into two arguments.
file="my report.txt"
rm $file # error: tries to delete "my" and "report.txt"
rm "$file" # correct
echo '$HOME' # prints literally $HOME
echo "$HOME" # prints /home/alice
Environment variables
Variables such as $HOME, $USER and $PATH are set by the system. Your own variable can be passed to child processes with export. Permanent environment settings go into ~/.bashrc or ~/.profile.
Script arguments
Values passed when the script is started are available as $1, $2 and so on.
| Variable | What it contains |
|---|---|
$0 | the script name |
$1, $2… | the first, second argument |
$# | the number of arguments |
"$@" | all arguments, each separately |
$? | exit code of the last command |
$$ | PID of the current script |
#!/bin/bash
echo "Script $0 received $# argument(s)"
echo "First: $1"
$ ./args.sh nginx 80
Script ./args.sh received 2 argument(s)
First: nginx
User input is read with the read command: read -p "Enter name: " name.
if conditions
Every command returns an exit code: 0 means success, anything else means an error. The if construct checks exactly this. For comparisons use [[ ... ]]; spaces inside the brackets are required.
#!/bin/bash
if [[ $# -eq 0 ]]; then
echo "Usage: $0 <directory>"
exit 1
fi
if [[ -d "$1" ]]; then
echo "Directory $1 exists"
elif [[ -f "$1" ]]; then
echo "$1 is a file, not a directory"
else
echo "$1 not found"
fi
| Check | Meaning |
|---|---|
-f file | a regular file exists |
-d dir | a directory exists |
-e path | the path exists |
-x file | the file can be executed |
-z "$var" / -n "$var" | the string is empty / not empty |
"$a" == "$b", != | string comparison |
-eq -ne -lt -le -gt -ge | numeric comparison |
You can also test any command directly:
if systemctl is-active --quiet nginx; then
echo "nginx is running"
else
echo "nginx is stopped"
fi
For many options, case is convenient:
case "$1" in
start) echo "Starting" ;;
stop) echo "Stopping" ;;
*) echo "Unknown command: $1"; exit 1 ;;
esac
for and while loops
for: iterate over a list
for host in web1 web2 db1; do
echo "Checking $host"
ping -c 1 -W 1 "$host" > /dev/null && echo " reachable" || echo " unreachable"
done
for file in /var/log/*.log; do
echo "$file: $(wc -l < "$file") lines"
done
for i in {1..5}; do
echo "Attempt $i"
done
while: as long as the condition holds
A classic example is reading a file line by line:
while read -r user; do
echo "User: $user"
done < users.txt
And waiting until a service comes up:
count=0
until curl -sf http://localhost:8080/health > /dev/null; do
count=$((count + 1))
if [[ $count -ge 10 ]]; then
echo "The service did not start"
exit 1
fi
sleep 3
done
echo "Service is ready"
Arithmetic in bash is written in double parentheses: $((a + b)). It works only with integers.
Functions
Repeated code is moved into functions. Inside a function the arguments are the same $1, $2, and local keeps a variable from leaking outside.
log() {
local level="$1"; shift
echo "$(date '+%F %T') [$level] $*"
}
log INFO "Starting the backup"
log ERROR "Out of disk space"
Error handling: set -euo pipefail
By default bash keeps going after a failed command — so a backup script can "successfully" finish without copying anything. It is common to add strict mode at the top of scripts:
#!/bin/bash
set -euo pipefail
-e— stop at the first error.-u— treat use of an undefined variable as an error (protects againstrm -rf "$DIR/"with an emptyDIR).-o pipefail— a pipeline counts as failed if any of its parts fails.
For debugging, run the script as bash -x script.sh: bash prints each command before executing it. The utility shellcheck helps find typical mistakes before running.
Practical example: a backup script
#!/bin/bash
set -euo pipefail
SRC="${1:-/var/www}"
DST="/var/backups/site"
KEEP_DAYS=7
STAMP=$(date +%F_%H-%M)
mkdir -p "$DST"
tar -czf "$DST/site-$STAMP.tar.gz" -C "$(dirname "$SRC")" "$(basename "$SRC")"
echo "Archive created: $DST/site-$STAMP.tar.gz"
# delete archives older than KEEP_DAYS days
find "$DST" -name "site-*.tar.gz" -mtime +"$KEEP_DAYS" -delete
The expression ${1:-/var/www} means "the first argument, or /var/www if there is none". Save the script as /usr/local/bin/backup-site.sh and make it executable: sudo chmod +x /usr/local/bin/backup-site.sh.
Scheduled runs: cron
Cron is the service that runs commands on a schedule. A user's jobs are edited with crontab -e, listed with crontab -l, and root's jobs are edited with sudo crontab -e.
A job line consists of five time fields and the command:
# minute hour day_of_month month day_of_week command
30 2 * * * /usr/local/bin/backup-site.sh >> /var/log/backup.log 2>&1
| Schedule | When it runs |
|---|---|
*/5 * * * * | every 5 minutes |
0 * * * * | at the start of every hour |
30 2 * * * | every day at 02:30 |
0 9 * * 1-5 | on weekdays at 09:00 |
0 3 1 * * | on the 1st of each month at 03:00 |
@reboot | once after boot |
Why a script works by hand but not in cron
- Different environment. Cron runs jobs with a minimal
PATHand without your~/.bashrc. Write full paths to commands and files, or setPATHat the top of the crontab. - Different current directory. A job starts in the home directory, not where the script is. Use absolute paths.
- The % character. In a crontab the
%sign means a newline, sodate +%Fwritten directly in a job line must be escaped as\%. It is simpler to move the command into a script. - Output is lost. Redirect output to a log (
>> file 2>&1), otherwise you will not see the errors. Cron runs are visible injournalctl -u cron(on RHEL-like systems the service is calledcrond).
On systems with systemd, timers are an alternative to cron: they write output to the journal and can catch up on missed runs.
Summary
A bash script for beginners is a file with the shebang #!/bin/bash, execute permission and a sequence of commands. Add variables in quotes, argument checks with if, for and while loops, and strict mode set -euo pipefail — and you get a reliable automation tool that cron can run on a schedule. Bash scripts, the environment and cron are covered step by step in our Linux course.
FAQ
What is the difference between bash and sh?
sh is the standard POSIX shell, while bash is its extended version with arrays, double brackets and other conveniences. A script that uses bash constructs must be run with bash, not sh.
What file extension should a bash script have?
Linux does not look at the extension: the interpreter is determined by the shebang. The .sh extension is just a convention that tells a human the file is a script.
How do I pass a password to a script safely?
Do not pass it as an argument — arguments are visible to other users in ps. Use an environment variable from a file with 600 permissions, or read -s to input it without echoing.
How do I check whether a script runs as root?
Compare the user ID with zero: if [[ $EUID -ne 0 ]]; then echo "Root is required"; exit 1; fi.
How do I run a script in the background so it does not stop after I log out of SSH?
Use nohup ./script.sh &, a tmux or screen session, and for permanent operation wrap the script in a systemd service.
This topic is covered in depth with practice in the course “Linux: с нуля до уверенного администрирования”.
- Bash-скрипты: от команды в терминале до рабочего скрипта автоматизации
- Пакеты, окружение и первая автоматизация: apt, профиль и cron