Bash Scripting for Beginners: A Step-by-Step Guide With Real Examples

A friendly, beginner-friendly intro to writing Bash scripts in 2026. Learn how the shell runs your code, how to use variables, conditionals, loops, and functions, and walk through real scripts you can actually use today.

12 min read

If you have spent any time in the terminal, you already know commands like ls, grep, and curl. Bash scripting is the next step — putting those commands into a file so you can run a sequence of them with one command. It is the duct tape of the developer world: not glamorous, but it holds the modern stack together.

This guide teaches Bash scripting from zero. You will learn the syntax, the gotchas (Bash has a lot of gotchas), and four small but real scripts you can copy into your own projects today. By the end you will be able to automate any repetitive task you do more than once a week.

Your First Script

A Bash script is just a text file containing commands. Save this as hello.sh:

bashbash
#!/usr/bin/env bash
set -euo pipefail
 
echo "Hello, $USER. Today is $(date +%Y-%m-%d)."

Make it executable and run it:

bashbash
$ chmod +x hello.sh
$ ./hello.sh
Hello, parth. Today is 2026-04-28.

Three lines worth memorising. The shebang (#!/usr/bin/env bash) tells the system which interpreter to use. set -euo pipefail turns on strict mode: exit on errors (-e), error on undefined variables (-u), and fail if any command in a pipeline fails (-o pipefail). Without strict mode, Bash silently swallows half its mistakes — turn it on in every script.

$(...) runs a command and substitutes its output. $USER is an environment variable. Already we are doing real work.

Variables and Quoting

Bash is unforgiving about quoting. Get this right and 80% of "weird Bash bugs" disappear.

bashbash
name="world"
greeting="Hello, $name!"
echo "$greeting"

Three rules: no spaces around = when assigning. Always wrap variables you read in double quotes ("$name", not $name) — otherwise filenames with spaces destroy your script. Use single quotes when you do not want variable expansion.

bashbash
echo "Today: $(date)"     # expands → Today: Mon Apr 28 12:00 ...
echo 'Today: $(date)'     # literal → Today: $(date)

Conditionals

bashbash
if [[ -f "config.json" ]]; then
  echo "Found config"
elif [[ -d "config" ]]; then
  echo "Found config directory"
else
  echo "No config"
  exit 1
fi

Use [[ ... ]] (Bash) instead of the older [ ... ] — it is safer and supports more operators. Useful tests: -f file (regular file), -d dir (directory), -z str (empty string), -n str (non-empty), == and != for strings, -eq/-ne/-lt/-gt for integers.

The semicolons are required where shown — they separate the test from then. Equally valid to put then on its own line.

Loops

Two forms you will use constantly. For loop:

bashbash
for file in *.log; do
  echo "Processing $file"
  gzip "$file"
done

While loop, perfect for reading lines from a file or command:

bashbash
while read -r line; do
  echo "Line: $line"
done < input.txt

The -r flag prevents backslash escaping — almost always what you want. The < redirects the file as input.

Functions and Arguments

Functions in Bash look unusual but are simple. Inside a function, $1, $2, ... are the arguments; $@ is all of them; $# is the count.

bashbash
greet() {
  local name="$1"
  local greeting="${2:-Hello}"   # default
  echo "$greeting, $name!"
}
 
greet "Ada"            # Hello, Ada!
greet "Ada" "Welcome"  # Welcome, Ada!

local keeps the variable scoped to the function — without it, every assignment is global. Always use local inside functions. ${2:-Hello} is parameter expansion: "use $2, but if unset, default to Hello."

Scripts themselves get arguments the same way: $1, $2, $@, $#. So ./deploy.sh staging v1.2 makes $1=staging and $2=v1.2 inside the script.

Real Script #1: Backup a Folder

bashbash
#!/usr/bin/env bash
set -euo pipefail
 
src="${1:?Usage: backup.sh <source-dir>}"
stamp="$(date +%Y%m%d-%H%M%S)"
out="backups/$(basename "$src")-$stamp.tar.gz"
 
mkdir -p backups
tar -czf "$out" "$src"
echo "Backed up $src$out"

${1:?...} exits with a message if $1 is missing — clean argument validation. Run it as ./backup.sh ./project and you get backups/project-20260428-120000.tar.gz. Schedule it with cron and you have automated backups.

Real Script #2: Wait for a Service to Be Healthy

A pattern every devops engineer uses. Useful in CI/CD pipelines when waiting for a service to start.

bashbash
#!/usr/bin/env bash
set -euo pipefail
 
url="${1:?Usage: wait-for.sh <url>}"
for i in {1..30}; do
  if curl -fsS "$url" > /dev/null; then
    echo "$url is up after ${i}s"
    exit 0
  fi
  sleep 1
done
 
echo "Timeout waiting for $url" >&2
exit 1

>&2 writes to stderr instead of stdout — the convention for error messages. curl -fsS exits non-zero on HTTP errors and stays quiet on success. The script polls for 30 seconds, then gives up.

Real Script #3: Bulk-Rename Files

Rename every .jpeg in the current directory to .jpg:

bashbash
#!/usr/bin/env bash
set -euo pipefail
 
shopt -s nullglob
 
for file in *.jpeg; do
  new="${file%.jpeg}.jpg"
  mv "$file" "$new"
  echo "$file$new"
done

shopt -s nullglob makes the loop skip cleanly if no files match (instead of running once with the literal string *.jpeg). ${file%.jpeg} is parameter expansion that strips the .jpeg suffix.

Real Script #4: Parse a Simple Flag-Based CLI

For anything beyond two arguments, use a case statement:

bashbash
#!/usr/bin/env bash
set -euo pipefail
 
env="dev"; verbose=0
 
while [[ $# -gt 0 ]]; do
  case "$1" in
    -e|--env) env="$2"; shift 2 ;;
    -v|--verbose) verbose=1; shift ;;
    *) echo "Unknown: $1" >&2; exit 1 ;;
  esac
done
 
echo "env=$env verbose=$verbose"

Run as ./run.sh --env prod -v and you get env=prod verbose=1. This is the foundation of any non-trivial Bash CLI.

Common Mistakes Beginners Make

  • Forgetting set -euo pipefail. Without it, errors silently slip through and your script "succeeds" with the wrong result.
  • Not quoting variables. rm $file breaks on filenames with spaces. Always rm "$file".
  • Spaces around =. name = "x" is wrong; name="x" is right.
  • Using cd without checking. A failed cd in a script keeps running in the wrong directory. With set -e, it aborts cleanly.
  • Reaching for Bash for complex logic. Bash is for short glue. Anything with data structures, arithmetic, or > 100 lines belongs in Python or Go.

Quick Reference

  • Shebang: #!/usr/bin/env bash. Strict mode: set -euo pipefail.
  • Make executable: chmod +x script.sh. Run: ./script.sh.
  • Variables: name="value" (no spaces). Read: "$name" (quoted).
  • Conditionals: if [[ -f file ]]; then ... fi. File tests: -f, -d, -z, -n.
  • Loops: for x in list; do ... done, while read -r line; do ...; done < file.
  • Function args: $1, $2, $@ (all), $# (count). Use local inside functions.
  • Default value: ${VAR:-default}. Required: ${VAR:?error message}.
  • Run command: $(...). Stderr: >&2. Discard output: > /dev/null.
  • Lint scripts with shellcheck — catches 90% of beginner bugs.
Rune AI

Rune AI

Key Insights

  • Always start scripts with #!/usr/bin/env bash and set -euo pipefail.
  • Quote every variable read ("$name") to survive spaces and special characters.
  • Use [[ ... ]] for conditionals and local inside functions.
  • Validate required arguments with ${1:?Usage: ...}.
  • Lint with shellcheck and graduate to Python the moment a script outgrows ~100 lines.
RunePowered by Rune AI

Frequently Asked Questions

Bash vs Zsh vs Fish for scripting?

lways Bash for *scripts* (universally available). Use whatever you like for your interactive shell.

When should I switch from Bash to Python?

The moment you need real data structures (arrays of objects, nested config), heavy string processing, HTTP work beyond `curl`, or your script crosses ~100 lines.

What is shellcheck?

free static analyser for shell scripts ([shellcheck.net](https://www.shellcheck.net/)). Run it on every script you write — it catches quoting bugs, unused variables, and a hundred other beginner traps. There is a great VS Code extension for it.

How do I make a script accept input from a pipe?

Read stdin: `while read -r line; do ...; done`. Or `xargs` to feed it as arguments.

What about POSIX `sh` vs Bash?

Some environments only have `sh` (Alpine containers, BusyBox). Stick to POSIX features (`#!/bin/sh`, `[ ... ]`, no arrays) when targeting them. Otherwise prefer Bash for the better syntax.

Conclusion

Bash scripting is the highest-ROI skill in a developer's toolbox. An hour spent learning the patterns above turns into hundreds of hours saved over a career. Pick one repetitive task you do this week and turn it into a 20-line script — that is the only way to build the muscle memory.