Home← Linux from ScratchCheat sheet
M14Linux from Scratch

M14 — Intro to shell scripting

Time: about 60–90 minutes
Lab root: /tmp/linux-lab-pushpjeet/m14

Up to now you typed commands one at a time. Real work repeats: greet a user, check a file, pack a folder every night. A shell script is a text file of those commands plus a little logic — variables, if, loops, and exit codes — so the computer can run the sequence for you.

This module is generic bash. It works the same on Ubuntu, Debian, Fedora, RHEL-family distros, and cloud Linux images. Nothing here depends on a vendor training deck.

Prerequisites#

  • Comfortable with M8–M11 (expansions, redirection, variables, ~/.bashrc).
  • Can edit a file in vim or any editor (M7).
  • Lab directory: work under /tmp/linux-lab-pushpjeet/m14 (or recreate that tree on your own VM).

Learning objectives#

By the end you will be able to:

  1. Write a script that starts with a portable shebang (#!/usr/bin/env bash).
  2. Make a script executable with chmod +x and run it by path or via bash script.sh.
  3. Use variables, positional parameters ($1, $#, $@), and "${var:-default}".
  4. Branch with if / then / elif / else / fi and test with [[ ]].
  5. Loop with for / do / done.
  6. Return meaningful exit codes (0 success, non-zero failure) and check them with $?.
  7. Build a small backup-a-folder script that creates a dated .tar.gz.

Lab environment#

mkdir -p /tmp/linux-lab-pushpjeet/m14
cd /tmp/linux-lab-pushpjeet/m14

All demo output below was captured on 2026-09-26 IST on a Debian GNU/Linux 13 practice box.


1. What a script is (and is not)#

A shell script is not a compiled program. The shell reads each line and runs it, the same way you would at the prompt. That is why scripts feel familiar: echo, ls, tar all work inside them.

Idea Meaning
Shebang First line telling the kernel which interpreter to use
Executable bit Permission that lets you run ./script.sh
Exit code Integer the script leaves behind (0 = OK)
Positional params Arguments the caller passed ($1, $2, …)

Rule of thumb: if you typed the same three commands more than twice this week, put them in a script.


2. Shebang and making it runnable#

Create hello.sh:

#!/usr/bin/env bash
# Simple greeting — Linux from Scratch M14
set -euo pipefail
name="${1:-friend}"
echo "Hello, ${name}! Today is $(date '+%Y-%m-%d')."
exit 0
Line Why it is there
#!/usr/bin/env bash Portable shebang: finds bash on $PATH (better than hard-coding /bin/bash alone on some systems)
set -euo pipefail Exit on error (-e), treat unset vars as errors (-u), fail pipelines properly (pipefail) — good habit for new scripts
"${1:-friend}" Use first argument, or the word friend if missing
exit 0 Explicit success (optional when the last command already succeeded)

Make it executable and run it:

chmod +x hello.sh
./hello.sh
./hello.sh Pushpjeet

Expected output (lab run):

Hello, friend! Today is 2026-09-26.
Hello, Pushpjeet! Today is 2026-09-26.

You can also run without the execute bit: bash hello.sh Pushpjeet. That always works and is useful while debugging.


3. Variables and positional parameters#

Inside a script:

Token Meaning
$0 Script name (as invoked)
$1, $2, … First, second, … argument
$# Number of arguments
"$@" All arguments, safely quoted as separate words
$? Exit code of the previous command

Demo script count-args.sh:

#!/usr/bin/env bash
set -euo pipefail
echo "Script name: $0"
echo "Argument count: $#"
i=1
for arg in "$@"; do
  echo "  \$$i = $arg"
  i=$((i + 1))
done
if [[ $# -eq 0 ]]; then
  echo "No arguments given."
  exit 2
fi
exit 0
chmod +x count-args.sh
./count-args.sh alpha beta gamma
echo "exit: $?"
./count-args.sh
echo "exit: $?"

Expected:

Script name: ./count-args.sh   # path may be absolute in your run
Argument count: 3
  $1 = alpha
  $2 = beta
  $3 = gamma
exit: 0
...
Argument count: 0
No arguments given.
exit: 2

4. Conditionals with [[ ]]#

Prefer [[ ... ]] in bash (not the older [ ... ] / test) for safer string handling.

Test True when
[[ -z "$x" ]] $x is empty
[[ -n "$x" ]] $x is non-empty
[[ -f "$p" ]] $p is a regular file
[[ -d "$p" ]] $p is a directory
[[ "$a" -eq "$b" ]] Integers equal
[[ "$a" == "$b" ]] Strings equal

check-file.sh:

#!/usr/bin/env bash
set -euo pipefail
target="${1:-}"
if [[ -z "$target" ]]; then
  echo "Pass a path to check." >&2
  exit 1
fi
if [[ -f "$target" ]]; then
  echo "Regular file: $target ($(wc -c < "$target") bytes)"
elif [[ -d "$target" ]]; then
  echo "Directory: $target"
else
  echo "Not found: $target"
  exit 3
fi

5. Loops#

for-each over a list:

for host in web db cache; do
  echo "ping plan: $host"
done

for-each over files (globs):

for f in /tmp/linux-lab-pushpjeet/m14/notes/*.txt; do
  echo "file: $f"
done

while:

n=1
while [[ $n -le 3 ]]; do
  echo "n=$n"
  n=$((n + 1))
done

Always quote expansions that might contain spaces: "$f", "$1".


6. Exit codes — the quiet API of scripts#

Code Convention
0 Success
1 Generic failure
2 Misuse (bad args) — common in CLI tools
126 / 127 Not executable / command not found (shell-assigned)

Callers check with:

./backup-folder.sh /some/path
if [[ $? -ne 0 ]]; then
  echo "backup failed" >&2
fi

Or, with set -e, the script stops at the first failing command — which is why we used set -euo pipefail in the demos.


7. Lab script: backup a folder#

backup-folder.sh — the module lab goal:

#!/usr/bin/env bash
# Backup a folder into a dated tar.gz under /tmp
set -euo pipefail
src="${1:-}"
if [[ -z "$src" || ! -d "$src" ]]; then
  echo "Usage: $0 <source-directory>" >&2
  exit 1
fi
stamp=$(date '+%Y%m%d-%H%M%S')
base=$(basename "$src")
dest="/tmp/backup-${base}-${stamp}.tar.gz"
tar -czf "$dest" -C "$(dirname "$src")" "$base"
echo "Created: $dest"
ls -lh "$dest"
exit 0

Setup and run:

mkdir -p /tmp/linux-lab-pushpjeet/m14/notes
echo "meeting notes" > /tmp/linux-lab-pushpjeet/m14/notes/day1.txt
echo "todo list" > /tmp/linux-lab-pushpjeet/m14/notes/todo.txt
chmod +x backup-folder.sh
./backup-folder.sh /tmp/linux-lab-pushpjeet/m14/notes

Expected (lab run):

Created: /tmp/backup-notes-20260926-110522.tar.gz
-rw-r--r-- 1 box box 191 Sep 26 11:05 /tmp/backup-notes-20260926-110522.tar.gz

Your timestamp will differ; the pattern backup-notes-YYYYMMDD-HHMMSS.tar.gz should match.


Common mistakes#

Mistake Fix
Forgetting the shebang Add #!/usr/bin/env bash as line 1 (no blank line above)
./script.sh: Permission denied chmod +x script.sh
Using $1 unquoted with spaces Always "$1"
Writing == for integers Use -eq / -ne / -lt inside [[ ]] for integers
Not checking arguments Validate early; exit non-zero with a clear stderr message
Hard-coding /tmp/mybackup.tar.gz Include a timestamp so runs do not overwrite each other

Hands-on lab (45–60 minutes)#

Part A — Hello with args (10 min)#

  1. Create hello.sh with the shebang above.
  2. chmod +x and run with / without an argument.
  3. Change the date format to %A %d %B and re-run.

Part B — Guard clauses (15 min)#

  1. Write check-file.sh.
  2. Test against a file, a directory, and a missing path.
  3. Confirm exit codes with echo $? after each run.

Part C — Backup script (20 min)#

  1. Implement backup-folder.sh.
  2. Back up your notes folder twice; confirm two different archive names.
  3. List archive contents: tar -tzf /tmp/backup-notes-*.tar.gz | head.

Part D — Stretch#

Add a second argument to backup-folder.sh for the destination directory (default /tmp). Reject the run if that destination is not writable.


Practice: check your understanding#

Multiple choice#

Q1. What does the first line #!/usr/bin/env bash do?

  • A. Comments out the rest of the file
  • B. Tells the kernel to run the file with bash found on $PATH
  • C. Compiles the script into a binary
  • D. Sets the execute bit automatically

Answer: B — it is the shebang. It does not set permissions; you still need chmod +x (or call bash script.sh).

Q2. After ./tool.sh finishes, how do you see its exit code?

  • A. echo $1
  • B. echo $#
  • C. echo $?
  • D. echo $0

Answer: C.

Q3. Which test is true when $path names an existing directory?

  • A. [[ -f "$path" ]]
  • B. [[ -d "$path" ]]
  • C. [[ -z "$path" ]]
  • D. [[ -x "$path" ]] only

Answer: B. (-x means executable/searchable; a directory can be -d without being your focus here.)

Flashcards#

Front Back
Shebang First line #!… choosing the interpreter
chmod +x script.sh Add execute permission for the user/group/other as specified
"${1:-friend}" Use $1, or friend if unset/empty
set -e Exit the script when a command fails
$# Count of positional arguments
"$@" All args as separate quoted words
exit 2 Quit with status 2 (often “bad usage”)
[[ -f f ]] True if f is a regular file

Match the columns#

Match left → right:

Item Pair with
$0 Script path/name as invoked
$1 First argument
$? Previous command’s exit code
$# Number of arguments
[[ -d x ]] x is a directory
for x in …; do …; done Loop over a list

Fill in the blank#

  1. Portable shebang: #!______ bash
    Answer: /usr/bin/env

  2. Make runnable: chmod ___ myscript.sh
    Answer: +x

  3. Default value: name="${1:______}" (default word world)
    Answer: -world → full form "${1:-world}"

  4. Integer compare equal in [[ ]]: [[ "$a" ___ "$b" ]]
    Answer: -eq

Order the steps — “script flow”#

Put these in the order you should do them for a new tool:

  1. Write the shebang and set -euo pipefail
  2. Decide arguments and validate them early (if [[ -z … ]])
  3. Perform the main work (commands / loops)
  4. chmod +x the file
  5. Run once with good args; check $?
  6. Run again with bad args; confirm a non-zero exit

Correct order: 1 → 2 → 3 → 4 → 5 → 6
(You may draft and edit before step 4; the run sequence still needs the execute bit or bash script.sh.)

Mini terminal challenges#

Challenge: Greet-by-user#

Goal: Script that prints Welcome, <name> using $1, exiting 2 if no name was given.

Setup: cd /tmp/linux-lab-pushpjeet/m14

Tasks:

  1. Create greet.sh with shebang and set -euo pipefail.
  2. If $# is 0, print a usage line to stderr and exit 2.
  3. Otherwise print Welcome, $1.

Verify: ./greet.sh Ada prints a welcome; ./greet.sh exits 2.

Stretch: Accept a second optional argument for a role (Welcome, Ada (admin)).

Challenge: Backup a folder#

Goal: Use (or re-create) backup-folder.sh from section 7.

Setup: Ensure notes/ has at least two files.

Tasks:

  1. Run the backup script.
  2. Confirm the archive exists under /tmp.
  3. tar -tzf the archive and confirm both files are listed.

Verify: Archive lists notes/day1.txt and notes/todo.txt (paths relative to how tar was invoked).

Stretch: Skip backup if the source directory is empty (exit 0 with a message, or exit 1 — pick one and document it in a comment).


Module wrap#

You can now turn repeated commands into scripts with a shebang, arguments, tests, loops, and exit codes — including a dated folder backup that feeds naturally into archives (M15).

Next step: Bash deep dive. Finished M14? Bash Scripting: Zero to Pro takes scripting from your first echo to production-ready tools: 45 modules, 162 tested scripts with real output, exercises with solutions, and a server-health capstone with monitoring exit codes. It adds strict mode and its gotchas, trap, getopts, logging, ShellCheck and cron.


Continue

← Linux from Scratch hub · Cheat sheet · All tutorials

Newsletter

Liked this? Get the next tutorial by email

New tutorials, labs and cheat sheets, plus AWS batch dates and Oracle tips if you want them. No spam, unsubscribe anytime.

I’m interested in

Double opt-in: you’ll get a confirmation email first. Privacy policy

WhatsApp