Prerequisites#
- Comfortable with M8–M11 (expansions, redirection, variables,
~/.bashrc). - Can edit a file in
vimor 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:
- Write a script that starts with a portable shebang (
#!/usr/bin/env bash). - Make a script executable with
chmod +xand run it by path or viabash script.sh. - Use variables, positional parameters (
$1,$#,$@), and"${var:-default}". - Branch with
if/then/elif/else/fiand test with[[ ]]. - Loop with
for/do/done. - Return meaningful exit codes (
0success, non-zero failure) and check them with$?. - 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)#
- Create
hello.shwith the shebang above. chmod +xand run with / without an argument.- Change the date format to
%A %d %Band re-run.
Part B — Guard clauses (15 min)#
- Write
check-file.sh. - Test against a file, a directory, and a missing path.
- Confirm exit codes with
echo $?after each run.
Part C — Backup script (20 min)#
- Implement
backup-folder.sh. - Back up your notes folder twice; confirm two different archive names.
- 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
bashfound 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#
-
Portable shebang:
#!______ bash
Answer:/usr/bin/env -
Make runnable:
chmod ___ myscript.sh
Answer:+x -
Default value:
name="${1:______}"(default wordworld)
Answer:-world→ full form"${1:-world}" -
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:
- Write the shebang and
set -euo pipefail - Decide arguments and validate them early (
if [[ -z … ]]) - Perform the main work (commands / loops)
chmod +xthe file- Run once with good args; check
$? - 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:
- Create
greet.shwith shebang andset -euo pipefail. - If
$#is 0, print a usage line to stderr andexit 2. - 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:
- Run the backup script.
- Confirm the archive exists under
/tmp. tar -tzfthe 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).