Shell Script Tutorial for Beginners - Bash Variables, If, and Loops
What you'll be able to do
- Create and run your own scripts with a shebang and execute permission
- Automate work by combining variables, conditionals, and loops
- Isolate and fix the causes of common syntax errors
Prerequisites (read these first)
A shell script is a file that collects the commands you would type in a terminal so they can be run together. "Shell script", "bash script", and "shell program" all mean the same thing.
Put work you used to type by hand into one file, and next time a single run does it for you. This basics edition covers Bash syntax from variables and conditionals to loops.
What You'll Learn
- You will be able to create and run your own scripts, understanding the shebang line and execute permission.
- You will be able to automate repetitive work by combining variables, conditionals, and loops.
- You will be able to isolate and fix the causes of the most common syntax errors.
Who this is for: anyone who has typed commands in a terminal and wants to automate work they keep repeating.
Prerequisites: this article reads variables such as $HOME. If that is new to you, read Environment Variables Basics first.
Terms defined up front
Each term below is defined once, here.
- A shell is the program that receives the commands you type and runs them. On Linux, Bash is the default.
- A shebang is the
#!/bin/bashline at the top of a script. It is also called a hashbang. It declares which shell should run the file. - Execute permission is the permission that allows a file to be launched as a program. A script without it stops with
Permission denied. - A variable is a box holding a value under a name.
name="value"puts a value in;$nametakes it out. - Quotes are
"and'. If a value contains spaces, the shell reads it as separate words unless you quote it. - The exit status is the number from 0 to 255 a command returns when it finishes. 0 means success and anything else means failure. It is also called the exit code or return code.
- Command substitution is the form
$(date), which embeds the output of a command in place. - Arithmetic expansion is the form
$((1 + 2)), which embeds a calculated result. Left alone, the shell treats numbers as text, so calculations need this form. It handles integers only, and fractional parts are discarded.
Your First Shell Script
Conclusion: #!/bin/bash, chmod +x, ./script.sh — three steps that start every Bash script.
Basic Structure
#!/bin/bash # This is a comment echo "Hello, Shell Script!" echo "Current date/time: $(date)" echo "Username: $USER"
Creating and Running Scripts
Step 1: Create File
$ nano hello.sh
Enter the above code and save.
Step 2: Add Execute Permission
$ chmod +x hello.sh
Step 3: Run Script
$ ./hello.sh
Hello, Shell Script! Current date/time: Sat Jan 11 14:30:00 JST 2025 Username: user
The format date prints depends on the locale. Under a Japanese locale (ja_JP.UTF-8), for instance, it comes out as 2025年 1月11日 土曜日 14時30分00秒 JST.
The ./ in ./hello.sh means "in the directory I am in". Drop it and the shell searches only PATH, which ends in command not found.
A script runs whatever is written in it, unconditionally
A script shows no confirmation prompt. If it contains rm, the delete just happens. What happens when it goes wrong comes down to two things.
- Running a script without reading it first means any delete or overwrite inside it cannot be undone.
- Running it as
sudo ./script.shruns every operation inside with root privileges, which puts system files within reach.
How to try it safely takes three habits.
- Practise only inside a dedicated directory:
mkdir ~/script-practice && cd ~/script-practice. - Read anyone else's script with
cat script.shbefore running it. - Run without
sudofirst. If a permission error appears, then work out why it is needed.
About Shebang
#!/bin/bash is called a shebang and specifies the interpreter — the program responsible for running the file.
#!/bin/bash— Use Bash#!/bin/sh— Use POSIX shell#!/usr/bin/env bash— Auto-detect from environment
Forget the shebang and behaviour depends on which shell happens to run the file. With #!/bin/sh in particular, Bash-only syntax such as [[ ]] and arrays is unavailable. When in doubt, write #!/bin/bash.
Variables and Input
Conclusion: No spaces around =; quote all uses; use $() for substitution; read -p for input.
Variable Basics
#!/bin/bash
# Define variables (no spaces around =)
name="Linux User"
age=25
today=$(date +%Y-%m-%d)
# Use variables
echo "Name: $name"
echo "Age: ${age} years old"
echo "Today's date: $today"Calculation Example
#!/bin/bash num1=10 num2=3 sum=$((num1 + num2)) diff=$((num1 - num2)) product=$((num1 * num2)) quotient=$((num1 / num2)) echo "Addition: $num1 + $num2 = $sum" echo "Subtraction: $num1 - $num2 = $diff" echo "Multiplication: $num1 × $num2 = $product" echo "Division: $num1 ÷ $num2 = $quotient"
Addition: 10 + 3 = 13 Subtraction: 10 - 3 = 7 Multiplication: 10 × 3 = 30 Division: 10 ÷ 3 = 3
The division result of 3 is not a mistake. Arithmetic expansion in the shell is integer arithmetic, and the fractional part is discarded ($((10 / 3)) is 3). When you need decimals, use bc or awk.
echo "scale=2; 10/3" | bc # 3.33
awk 'BEGIN {print 10/3}' # 3.33333Special Variables
| Variable | Description | Example |
|---|---|---|
$0 |
Script name | ./script.sh |
$1, $2, ... |
Command line arguments | 1st arg, 2nd arg |
$# |
Number of arguments | 3 for 3 arguments |
$@ |
All arguments | "arg1" "arg2" "arg3" |
$? |
Exit status of last command | 0 for success, non-0 for failure |
$$ |
Current process ID | 12345 |
$USER |
Current username | user |
$HOME |
Home directory | /home/user |
$PWD |
Current directory | /home/user/scripts |
User Input
Basic Input
#!/bin/bash
echo "Please enter your name:"
read name
echo "Hello, ${name}!"Input with Prompt
#!/bin/bash read -p "Enter your age: " age read -s -p "Enter password: " password echo echo "Age: $age" echo "Password entered silently"
Multiple Values at Once
#!/bin/bash echo "Enter name and age (space-separated):" read name age echo "Name: $name, Age: $age"
Conditionals
Conclusion: Spaces inside [ ] required; quote string variables; use -eq/-gt/-lt for numbers.
if Statement
Basic if Statement
#!/bin/bash
read -p "Enter a number: " num
if [ "$num" -gt 0 ]; then
echo "$num is positive"
elif [ "$num" -lt 0 ]; then
echo "$num is negative"
else
echo "$num is zero"
fiFile Existence Check
#!/bin/bash
filename="test.txt"
if [ -f "$filename" ]; then
echo "$filename exists"
echo "File size: $(wc -c < "$filename") bytes"
else
echo "$filename does not exist"
echo "Creating file..."
touch "$filename"
fiConditional Operators
Numeric Comparison
| Operator | Meaning | Example |
|---|---|---|
-eq |
Equal | [ $a -eq $b ] |
-ne |
Not equal | [ $a -ne $b ] |
-gt |
Greater than | [ $a -gt $b ] |
-ge |
Greater or equal | [ $a -ge $b ] |
-lt |
Less than | [ $a -lt $b ] |
-le |
Less or equal | [ $a -le $b ] |
String Comparison
| Operator | Meaning | Example |
|---|---|---|
= |
Equal | [ "$a" = "$b" ] |
!= |
Not equal | [ "$a" != "$b" ] |
-z |
Empty string | [ -z "$str" ] |
-n |
Not empty | [ -n "$str" ] |
File Tests
| Operator | Meaning | Example |
|---|---|---|
-f |
Regular file | [ -f file.txt ] |
-d |
Directory | [ -d /home ] |
-e |
Exists | [ -e path ] |
-r |
Readable | [ -r file ] |
-w |
Writable | [ -w file ] |
-x |
Executable | [ -x script ] |
case Statement
#!/bin/bash
echo "Select an option:"
echo "1) List files"
echo "2) Show current time"
echo "3) Show system info"
echo "4) Exit"
read -p "Choice (1-4): " choice
case $choice in
1)
echo "=== File List ==="
ls -la
;;
2)
echo "=== Current Time ==="
date
;;
3)
echo "=== System Info ==="
uname -a
;;
4)
echo "Exiting."
exit 0
;;
*)
echo "Invalid choice."
;;
esacLoops
Conclusion: {1..N} for ranges, while IFS= read for file lines, until for inverse conditions.
for Loop
Basic for Loop
#!/bin/bash
# Numeric range
for i in {1..5}
do
echo "Count: $i"
done
echo "---"
# File processing
for file in *.txt
do
echo "Processing: $file"
wc -l "$file"
doneLoop with Arrays
#!/bin/bash
fruits=("apple" "banana" "orange" "grape")
echo "Fruit list:"
for fruit in "${fruits[@]}"
do
echo "- $fruit"
doneC-style for Loop
#!/bin/bash
echo "Multiplication table (partial):"
for ((i=1; i<=5; i++))
do
for ((j=1; j<=5; j++))
do
result=$((i * j))
printf "%2d " $result
done
echo
donewhile Loop
Basic while Loop
#!/bin/bash
count=1
while [ $count -le 5 ]
do
echo "Loop iteration $count"
count=$((count + 1))
doneReading Files
#!/bin/bash
filename="data.txt"
if [ -f "$filename" ]; then
while IFS= read -r line
do
echo "Read: $line"
done < "$filename"
else
echo "File $filename not found"
fiuntil Loop
#!/bin/bash
count=1
until [ $count -gt 5 ]
do
echo "Count: $count"
count=$((count + 1))
doneLoop Control
- break — Exit loop
- continue — Skip to next iteration
#!/bin/bash
for i in {1..10}
do
if [ $i -eq 3 ]; then
echo "Skipping 3"
continue
fi
if [ $i -eq 8 ]; then
echo "Stopping at 8"
break
fi
echo "Number: $i"
doneCommon Beginner Mistakes and Pitfalls
Conclusion: Six traps in Bash scripts: spacing, braces, brackets, quotes, backticks, math.
Mistake 1: Spaces in Variable Assignment
NG (causes error)
name = "Taro" # Space before/after = age= 25 # Space after = city ="Tokyo" # Space before =
Results in "command not found" error.
OK (correct usage)
name="Taro" # No spaces around = age=25 city="Tokyo"
Variable assignment must have no spaces around =.
Mistake 2: Missing Braces in Variable Reference
NG (unexpected results)
filename="test" echo "$filenameback.txt" # prints just ".txt" ($filenameback is undefined) echo "$filename_backup" # prints nothing ($filename_backup is undefined)
The shell reads $filenameback as one variable name. It never splits it into $filename plus back.
OK (correct usage)
filename="test"
echo "${filename}back.txt" # testback.txt
echo "${filename}_backup" # test_backupUse {} to clearly define variable name scope.
Mistake 3: Missing Spaces in if Statement Conditions
NG (syntax error)
if [$num -gt 5]; then # No space after [ if [ $num -gt 5]; then # No space before ]
OK (correct usage)
if [ $num -gt 5 ]; then # Spaces inside [ ] required if [[ $num -gt 5 ]]; then # Same for [[ ]] if (( num > 5 )); then # Arithmetic expression
Mistake 4: Missing Quotes in String Comparison
NG (dangerous examples)
if [ $name = John Doe ]; then # Issues with spaces if [ $empty_var = "" ]; then # Error when empty
OK (safe usage)
if [ "$name" = "John Doe" ]; then # Quote both sides if [ "$empty_var" = "" ]; then # No error when empty if [ -z "$var" ]; then # Dedicated empty check option
Mistake 5: Old Command Substitution Syntax
NG (not recommended)
date=`date` # Backticks result=`cat `which ls`` # Nesting causes error
OK (modern syntax)
date=$(date) # Use $() result=$(cat $(which ls)) # Easy to nest
Mistake 6: Arithmetic Operation Errors
NG (not calculated)
result = $num1 + $num2 # Interpreted as a "result" command (command not found) sum="$a + $b" # Not calculated
OK (correct calculation methods)
result=$((num1 + num2)) # Arithmetic expansion let "result = num1 + num2" # Use let command
Basic Rules to Prevent Mistakes
Writing Basics
- Variable assignment: no spaces around
= - Variable usage: always quote
"$var" - Complex variables: use braces
"${var}"
Conditional Basics
- Using
[ ]: always include internal spaces - String comparison: quote both sides
- Numeric comparison: use
-eq,-gt,-lt, etc.
Debugging and Troubleshooting
When a script misbehaves, do not guess — let set -x print the execution trace. Seeing how each variable expanded, line by line, makes the cause easy to spot.
set -x: print the execution trace
#!/bin/bash
set -x # display every command from here on
filename="data.txt"
echo "DEBUG: filename = [$filename]"
if [ ! -f "$filename" ]; then
echo "ERROR: File $filename not found" >&2
exit 1
fi+ filename=data.txt + echo 'DEBUG: filename = [data.txt]' DEBUG: filename = [data.txt] + '[' '!' -f data.txt ']' + echo 'ERROR: File data.txt not found' ERROR: File data.txt not found + exit 1
Lines starting with + are the commands the shell actually ran. Because you see them after variable expansion, causes such as "the variable was empty" or "the quotes did not apply" become identifiable on the spot. The value is printed inside [ ] to tell an empty string apart from a single space.
set -e / set -u: stop trouble early
#!/bin/bash set -e # stop as soon as a command fails set -u # stop as soon as an undefined variable is referenced
These two exist to stop processing from continuing past a problem unnoticed. set -u is especially effective at catching typos in variable names.
In a script with set -u enabled, referencing an undefined variable stops execution on that line with an unbound variable error. That is the intended behaviour, not a fault in set -x.
script.sh: line 5: var: unbound variable
Where you do not want it to stop, supply a default such as ${var:-}, which treats an undefined variable as an empty string.
Common Error Messages and Solutions
Symptom: command not found
Cause: spaces around = in a variable assignment, or running the script without ./.
Check:
bash -n script.sh # syntax check only (does not run)
Fix: remove the spaces around =. Invoke the script as ./script.sh.
Symptom: Permission denied
Cause: the script has no execute permission.
Check:
ls -l script.sh
No x in something like -rw-r--r-- means no execute permission.
Fix:
chmod +x script.sh
Symptom: syntax error near unexpected token
Cause: missing spaces inside [ ], an unclosed quote, or a missing then / fi / done.
Check:
bash -n script.sh
Fix: review the lines around the reported line number. if needs a matching then and fi; for and while need do and done.
Symptom: a variable comes out empty
Cause: characters follow the variable name directly, so the shell reads it as a different variable.
Check:
echo "DEBUG: var = [$var]"
The brackets let you tell an empty string apart from one containing spaces.
Fix: write ${var} to mark where the variable name ends.
Completion Checklist
- [ ] Wrote a shebang (
#!/bin/bash) on the first line - [ ] Added execute permission with
chmod +x - [ ] Left no spaces around
=in assignments - [ ] Quoted variable references as
"$var" - [ ] Ran inside a practice directory, without
sudo, to confirm behaviour - [ ] Used
bash -nandset -xto isolate the cause when it did not work