← 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).


Continue

← Linux from Scratch hub · Cheat sheet · All tutorials