
Bash Scripting Tutorial for Beginners With Real Examples
Learn Bash scripting step by step with real tested scripts, strict mode, traps and ShellCheck warnings from Bash 5.2.

A Bash script is a text file of shell commands that you can run again and again. It turns a sequence you type by hand into one command, with variables, decisions, loops and functions when you need them.
This tutorial builds up from a first script to strict mode, cleanup traps and ShellCheck. Every script and output below was run in Bash 5.2.21, and the language rules were checked against the GNU Bash manual on October 23, 2026.
The short answer
Create a file that starts with a shebang line, make it executable with chmod, and run it. Learn variables, quoting, if statements, loops and functions, then add strict mode and ShellCheck so mistakes show up early.
If you remember one rule, make it this: put double quotes around variable expansions. Most beginner bugs come from unquoted variables.
Bash scripts run on Linux, macOS and Windows through WSL. Our guide to terminal and shell setup covers getting a working shell on each system.
What a Bash script is
According to the GNU Bash manual, when a script's first line starts with the two characters #!, the remainder of that line names the interpreter. That first line is the shebang.
A common choice is the line below. It asks the system to find Bash through your PATH, instead of pointing at one fixed location. That is our explanation of why people prefer it.
#!/usr/bin/env bash
Your first script
This script prints a greeting. The expression with a colon and a dash gives name a default value when no argument is passed. The details come later in this guide.
#!/usr/bin/env bash
name="${1:-world}"
echo "Hello, $name"
Save it as hello.sh, make it executable and run it three ways. The manual says a script can be made executable with chmod, and that running the file is the same as running bash with the file name.
$ chmod +x hello.sh
$ ./hello.sh
Hello, world
$ ./hello.sh Sam
Hello, Sam
$ bash hello.sh "Ada Lovelace"
Hello, Ada Lovelace
Arguments
Inside a script, $1 is the first argument, $2 the second, $# the number of arguments and $@ all of them. Quote $@ so arguments that contain spaces stay whole.
#!/usr/bin/env bash
echo "script: $0"
echo "first: $1, count: $#"
for a in "$@"; do echo "arg: $a"; done
$ bash args.sh one "two words" three
script: args.sh
first: one, count: 3
arg: one
arg: two words
arg: three
Notice that two words stayed together as one argument. Without quotes around $@, Bash would split it into two.
Variables and how to quote them
Write a variable assignment with no spaces around the equals sign. Read it with a dollar sign. Double quotes allow expansion, and single quotes do not.
#!/usr/bin/env bash
count=3
greeting="Total: $count files"
echo "$greeting"
echo 'No expansion in single quotes: $count'
today=$(date +%Y)
echo "Year has ${#today} digits"
echo "Sum: $((count + 4))"
echo "Default: ${missing:-fallback}"
$ bash vars.sh
Total: 3 files
No expansion in single quotes: $count
Year has 4 digits
Sum: 7
Default: fallback
The Bash manual describes four quoting methods: backslash escapes, single quotes, double quotes and dollar-single quotes. For everyday scripts, double quotes cover most cases.
Command substitution and arithmetic
The manual says command substitution replaces a command with its output. The standard form is a dollar sign and parentheses. The older backtick form is described as deprecated.
The today line above used it. Arithmetic works the same way, with double parentheses, and the sum line printed 7.
Why quoting matters
Here is the bug that quoting prevents. I created a file called my notes.txt with a space in its name, then listed it unquoted and quoted.
$ f="my notes.txt"
$ ls $f
ls: cannot access 'my': No such file or directory
ls: cannot access 'notes.txt': No such file or directory
$ ls "$f"
my notes.txt
Unquoted, Bash split the name into two words and ls looked for two files that do not exist. With rm instead of ls, the same mistake can delete the wrong thing.
Decisions with if and case
The manual describes the if form as running the test commands and then the matching branch when the exit status is zero. For files and strings, use the double square bracket test.
The manual says the words inside double square brackets do not undergo word splitting or filename expansion. That makes them safer than the single bracket form for beginners.
#!/usr/bin/env bash
file="${1:?usage: cond.sh FILE}"
if [[ -f "$file" ]]; then
echo "$file is a regular file"
elif [[ -d "$file" ]]; then
echo "$file is a directory"
else
echo "$file does not exist"
fi
case "$file" in
*.txt) echo "text file" ;;
*.sh) echo "shell script" ;;
*) echo "something else" ;;
esac
$ bash cond.sh cond.sh
cond.sh is a regular file
shell script
$ bash cond.sh /tmp
/tmp is a directory
something else
$ bash cond.sh nothing.txt
nothing.txt does not exist
text file
$ bash cond.sh
cond.sh: line 2: 1: usage: cond.sh FILE
Two things to notice. The case statement matched the name nothing.txt even though the file does not exist, because case only looks at the text. And the usage message came from the colon-question-mark expansion when no argument was given.
Exit status
The manual says an exit status of zero means success and any non-zero status means failure. Statuses fall between 0 and 255, and a command ended by a fatal signal gets 128 plus the signal number.
Every if, while and && in your scripts is really testing an exit status. You can read the last one in the special variable $?, as the function example below shows.
Loops
The manual defines for, while and until loops. A for loop runs once per word in a list, while runs as long as a test succeeds, and until runs as long as a test fails.
#!/usr/bin/env bash
for f in *.sh; do
echo "found $f"
done
n=3
while (( n > 0 )); do
echo "countdown $n"
n=$((n - 1))
done
$ bash loops.sh
found cond.sh
found hello.sh
found loops.sh
found vars.sh
countdown 3
countdown 2
countdown 1
Looping over a glob such as *.sh is safer than looping over the output of ls. ShellCheck warns about the second pattern, as you will see below.
Arrays
The manual says Bash provides one-dimensional indexed and associative arrays. Create an indexed array with parentheses, and declare an associative array with declare -A.
fruits=(pears apples bananas)
echo "count: ${#fruits[@]}"
for x in "${fruits[@]}"; do echo "- $x"; done
declare -A age=([sam]=31 [ada]=36)
echo "ada is ${age[ada]}"
count: 3
- pears
- apples
- bananas
ada is 36
Functions
The manual describes functions as a way to group commands under one name and run them like a simple command. They run in the current shell, with no new process.
Use local for variables that should stay inside the function. A function returns an exit status, which is why one can be used directly in an if.
add() {
local a=$1 b=$2
echo $((a + b))
}
is_even() { (( $1 % 2 == 0 )); }
echo "add: $(add 2 3)"
if is_even 4; then echo "4 is even"; fi
is_even 5 || echo "5 is odd (function returned $?)"
add: 5
4 is even
5 is odd (function returned 1)
Reading input
The read builtin reads a line into a variable. Use the r option so backslashes are not treated specially. A while loop with read processes input line by line.
read -r -p "Name? " name
echo "Hi $name"
while IFS= read -r line; do echo "line: $line"; done < <(printf 'a\nb\n')
$ echo Sam | bash readin.sh
Hi Sam
line: a
line: b
Strict mode
Bash keeps going after errors by default. Strict mode makes problems stop the script. It combines three settings that the manual documents under the set builtin.
| Setting | What it does |
|---|---|
| set -e | Exits immediately if a command returns a non-zero status, with exceptions |
| set -u | Treats an unset variable as an error |
| set -o pipefail | Makes a pipeline fail if any command in it fails |
The manual's exceptions for set -e matter.
The shell does not exit for a failing command in an if test, in a while or until condition, in most parts of an && or || list, or when its status is inverted with an exclamation mark.
#!/usr/bin/env bash
set -euo pipefail
echo "start"
false
echo "never printed"
$ bash strict.sh
start
The script stopped at false with exit status 1. The second echo never ran.
Unset variables and pipelines
With set -u, using a variable you never assigned stops the script and names it. That catches typos.
$ bash unset.sh
unset.sh: line 3: undefined_var: unbound variable
The manual says pipefail is disabled by default. Without it, a pipeline reports the status of its last command only. Here is the difference.
$ bash pipe.sh
without pipefail: 0
with pipefail: 1
Cleaning up with trap
A trap runs a command when the script exits. It is the standard way to remove temporary files even when something fails.
#!/usr/bin/env bash
set -euo pipefail
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"; echo "cleaned up"' EXIT
echo "working in a temp folder"
test -d "$tmp" && echo "temp folder exists"
$ bash trap.sh
working in a temp folder
temp folder exists
cleaned up
Let ShellCheck find mistakes
ShellCheck is a tool that analyzes shell scripts without running them. It reads your script and points out quoting problems and other common bugs before you run it. I used ShellCheck 0.11.0.
This script has three mistakes. ShellCheck found all of them and suggested a fix for the quoting.
#!/usr/bin/env bash
file="my notes.txt"
rm $file
for i in $(ls); do echo $i; done
[ $x = 1 ] && echo yes
$ shellcheck bad.sh
In bad.sh line 3:
rm $file
^---^ SC2086 (info): Double quote to prevent globbing and word splitting.
Did you mean:
rm "$file"
In bad.sh line 4:
for i in $(ls); do echo $i; done
^---^ SC2045 (error): Iterating over ls output is fragile. Use globs.
In bad.sh line 5:
[ $x = 1 ] && echo yes
^-- SC2154 (warning): x is referenced but not assigned.
The ShellCheck wiki entry for SC2086 explains why: an unquoted expansion is split on spaces and expanded as a glob. Run ShellCheck on every script you keep.
A cheat sheet
This table summarizes the syntax above. Each item appeared in a script I ran.
| Syntax | Meaning |
|---|---|
| $1, $2 | First and second argument |
| $# | Number of arguments |
| "$@" | All arguments, kept separate |
| $? | Exit status of the last command |
| $(command) | Output of a command |
| $((a + b)) | Arithmetic |
| ${var:-default} | Value of var, or a default if unset or empty |
| ${#var} | Length of var |
| [[ -f file ]] | True if file is a regular file |
| [[ -d dir ]] | True if dir is a directory |
| for x in list; do … done | Repeat for each item |
| while test; do … done | Repeat while the test passes |
| case word in pattern) … ;; esac | Match a word against patterns |
| name() { …; } | Define a function |
| declare -A map | Declare an associative array |
| set -euo pipefail | Strict mode |
| trap 'cmd' EXIT | Run cmd when the script exits |
Bash scripting on Windows
On Windows, run Bash through WSL. Microsoft says wsl –install installs the features WSL needs and Ubuntu by default. Our guides to every wslconfig setting explained and how to reclaim WSL disk space cover what comes next.
PowerShell has its own scripting language, so a Bash script will not run in it. Our Linux commands cheat sheet explains the Windows and Linux differences.
Bash scripting books and courses
I did not review any book or course for this guide, so I do not recommend one. The GNU Bash Reference Manual is free online and is the primary reference for everything on this page.
The man page for Bash on your own system is the other reference. Type man bash, and press q to leave. Our advice is to read one section of the manual each time you write a script.
How to practice
These ideas are our advice, and they cost nothing.
Automate something you do by hand
A backup, a rename, a report or a setup routine makes a good first script.
Run ShellCheck every time
Fix each warning and read its wiki page to learn why.
Use a safe folder
Test scripts that delete or rename files in a practice folder first.
Add strict mode early
Start new scripts with set -euo pipefail so mistakes stop the script.
Common mistakes
Forgetting quotes
Without quotes, variables split on spaces and expand globs.
Spaces around the equals sign
Name = value runs a command and does not assign anything.
Looping over ls output
Use a glob instead.
Relying on set -e alone
Its exceptions mean some failures still pass silently.
Forgetting the execute bit
Run chmod +x or start the script with bash.
Frequently asked questions
What counts as a Bash script?
A text file of shell commands that Bash runs in order. Its first line, the shebang, names the interpreter.
How do I run a Bash script?
Make it executable with chmod +x and run ./script.sh, or run bash script.sh.
What is the difference between Bash and shell scripting?
Shell scripting is the general idea. Bash is one common shell, and its scripts can use features other shells may lack.
Why quote variables in Bash?
Unquoted expansions are split on spaces and expanded as filename patterns, which breaks names that contain spaces.
What is the effect of set -euo pipefail?
It halts the script on errors, on unset variables and on failures inside pipelines. The manual documents exceptions to set -e.
How do I learn Bash scripting?
Write small scripts for real tasks, run ShellCheck, and read the GNU Bash manual.
A quick checklist
Sources
- GNU Bash Reference Manual: Shell scripts
- GNU Bash Reference Manual: Quoting
- GNU Bash Reference Manual: Command substitution
- GNU Bash Reference Manual: Conditional constructs
- GNU Bash Reference Manual: Exit status
- GNU Bash Reference Manual: Looping constructs
- GNU Bash Reference Manual: Arrays
- GNU Bash Reference Manual: Shell functions
- GNU Bash Reference Manual: The set builtin
- ShellCheck
- ShellCheck wiki: SC2086
- Microsoft Learn: Install WSL
From circuittimes.com/cloud-dev/bash-scripting/
Was this guide helpful?
What went wrong?
Read nextComparison 13 min readGit Merge vs Rebase: When to Use Each, With Real ExamplesGit merge, rebase, fast-forward and squash compared with real command output, conflicts, force pushing and pull settings.
Comparison 11 min readJest vs Vitest: Which Test Runner to Use in 2026
Comparison 10 min readPlaywright vs Cypress: Which Testing Tool in 2026
Comparison 12 min readGitHub vs GitLab vs Bitbucket: Which to Choose in 2026
The briefing, without the noise.
The stories that matter in technology, every weekday morning.
Free. Unsubscribe in one click.







