Lesson 1: Your First Script

Goal: Learn to create, configure, and run shell scripts with proper structure.


Table of Contents

  1. What is a Shell Script?
  2. The Shebang Line
  3. Creating Your First Script
  4. File Permissions
  5. Running Scripts
  6. Strict Mode — set Flags
  7. Comments
  8. Script Template
  9. Exercises

What is a Shell Script?

A shell script is a text file containing a sequence of commands that the shell executes line by line. Instead of typing commands one at a time, you write them in a file and run them all at once.

Why use scripts?

  • Automate repetitive tasks
  • Ensure consistency (same steps every time)
  • Document procedures (the script IS the documentation)
  • Build deployment pipelines, Docker entrypoints, CI/CD jobs

The Shebang Line

The first line of every script should be a shebang (#!). It tells the system which interpreter to use.

Common shebangs

#!/bin/bash          # Use bash (most common on Linux)
#!/bin/sh            # Use POSIX shell (more portable, fewer features)
#!/usr/bin/env bash  # Find bash in PATH (most portable)
#!/usr/bin/env python3  # Python script
#!/usr/bin/env node     # Node.js script

Why does the shebang matter?

Without a shebang, the system doesn't know which interpreter to use:

# Without shebang — may fail or use wrong shell
echo "Hello"

# With shebang — always uses bash
#!/bin/bash
echo "Hello"

bash vs sh

Feature /bin/bash /bin/sh
Arrays Yes No
[[ ]] tests Yes No
$((...)) arithmetic Yes Limited
String manipulation Yes Limited
Docker Alpine default No (install needed) Yes
POSIX compliant Mostly Yes

Rule of thumb:

  • Use #!/bin/bash for Linux scripts where bash is available
  • Use #!/bin/sh for Docker containers (especially Alpine) and maximum portability
  • Use #!/usr/bin/env bash when you're not sure where bash is installed

Creating Your First Script

Step 1: Create the file

nano hello.sh

Step 2: Write the script

#!/bin/bash

echo "Hello, World!"
echo "Today is $(date)"
echo "You are logged in as: $USER"
echo "Your home directory is: $HOME"

Step 3: Save and exit

In nano: Ctrl+O, Enter, Ctrl+X

Step 4: Make it executable

chmod +x hello.sh

Step 5: Run it

./hello.sh

Output:

Hello, World!
Today is Sun Mar 22 14:30:00 CET 2026
You are logged in as: benjamin
Your home directory is: /home/benjamin

File Permissions

Why do we need chmod?

By default, new files are not executable:

touch script.sh
ls -l script.sh
# -rw-r--r-- 1 benjamin benjamin 0 Mar 22 14:00 script.sh
#  ^ no 'x' anywhere — can't execute

Make executable

# Only for the owner
chmod u+x script.sh

# For everyone
chmod +x script.sh

# Using octal notation
chmod 755 script.sh
# rwxr-xr-x → owner: full, group/others: read+execute

Common permission patterns for scripts

chmod 755 script.sh   # Everyone can run, only owner can edit
chmod 700 script.sh   # Only owner can run and edit
chmod +x script.sh    # Quick: add execute for all

Check permissions

ls -l script.sh
# -rwxr-xr-x 1 benjamin benjamin 89 Mar 22 14:00 script.sh
#  ^^^                                              ← owner: rwx ✓

Running Scripts

There are several ways to run a script:

1. Direct execution (requires chmod +x)

./hello.sh

The ./ is important — it tells the shell "look in the current directory". Without it, the shell searches $PATH.

2. With the interpreter explicitly

bash hello.sh
sh hello.sh

This does NOT require execute permission.

3. Source the script (runs in current shell)

source hello.sh
. hello.sh

Important difference: source runs the script in your current shell, so variables set in the script persist:

# script sets MY_VAR="hello"
source script.sh
echo $MY_VAR  # "hello" — variable persists!

# vs.
./script.sh
echo $MY_VAR  # empty — ran in subshell, variable is gone

4. From anywhere (if in PATH)

# Move script to a PATH directory
cp hello.sh ~/bin/hello
chmod +x ~/bin/hello

# Now run from anywhere
hello

Strict Mode — set Flags

Professional scripts always start with strict mode to catch errors early.

#!/bin/bash
set -euo pipefail

What each flag does

set -e — Exit on error

#!/bin/bash
set -e

echo "Step 1"
false           # ← this command "fails" (exit code 1)
echo "Step 2"   # ← this line NEVER runs (script exits at 'false')

Without set -e, the script would continue past errors silently.

set -u — Exit on undefined variables

#!/bin/bash
set -u

echo "Hello, $UNDEFINED_VAR"
# Error: UNDEFINED_VAR: unbound variable

Without set -u, undefined variables silently expand to empty strings — a common source of bugs.

set -o pipefail — Catch pipe errors

#!/bin/bash
set -o pipefail

# Without pipefail: exit code is 0 (from wc)
# With pipefail: exit code is 1 (from the failing curl)
curl http://nonexistent.invalid 2>/dev/null | wc -l

The complete strict mode

#!/bin/bash
set -euo pipefail
IFS=$'\n\t'

IFS=$'\n\t' changes the word-splitting behavior to only split on newlines and tabs (not spaces), which prevents many common bugs with filenames containing spaces.


Comments

Single-line comments

# This is a comment
echo "Hello"  # This is an inline comment

Multi-line comments (using heredoc trick)

: << 'COMMENT'
This is a multi-line comment.
It can span many lines.
Variables like $HOME are NOT expanded.
COMMENT

Best practices for comments

#!/bin/bash
# ============================================================
# Script: deploy.sh
# Description: Deploy application to production
# Author: Benjamin Tietz
# Date: 2026-03-22
# Usage: ./deploy.sh [environment]
# ============================================================

# --- Configuration ---
APP_NAME="myapp"
DEPLOY_DIR="/var/www/$APP_NAME"

# Check if the target directory exists
if [[ ! -d "$DEPLOY_DIR" ]]; then
    echo "Error: Deploy directory not found"
    exit 1
fi

# Deploy the application
# NOTE: This requires SSH access to the server
rsync -avz ./dist/ "$DEPLOY_DIR/"

Script Template

Use this as a starting point for every new script:

#!/bin/bash
# ============================================================
# Script: script-name.sh
# Description: What this script does
# Usage: ./script-name.sh [options] [arguments]
# ============================================================
set -euo pipefail

# --- Configuration ---
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "$0")"

# --- Functions ---
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}

error() {
    echo "[ERROR] $*" >&2
    exit 1
}

usage() {
    echo "Usage: $SCRIPT_NAME [options]"
    echo ""
    echo "Options:"
    echo "  -h, --help    Show this help message"
    exit 0
}

# --- Main ---
main() {
    log "Starting $SCRIPT_NAME"

    # Your logic here

    log "Done"
}

# --- Entry Point ---
main "$@"

Why this template?

  • set -euo pipefail — catches errors immediately
  • SCRIPT_DIR — always know where the script lives (for relative paths)
  • log() function — consistent, timestamped output
  • error() function — errors go to stderr and exit
  • main() — organizes code; "$@" passes all arguments
  • readonly — prevents accidental modification of constants

Exercises

Exercise 1: Hello World

# 1. Create the script
cat > hello.sh << 'EOF'
#!/bin/bash
echo "Hello, World!"
echo "Script: $0"
echo "Date: $(date)"
echo "User: $USER"
echo "Shell: $SHELL"
echo "PWD: $PWD"
EOF

# 2. Make executable
chmod +x hello.sh

# 3. Run it
./hello.sh

# 4. Run with bash explicitly
bash hello.sh

# 5. Clean up
rm hello.sh

Exercise 2: Strict mode testing

# 1. Create a script WITHOUT strict mode
cat > test-nostrict.sh << 'EOF'
#!/bin/bash
echo "Step 1"
ls /nonexistent/path
echo "Step 2 — this runs even after error!"
echo "Undefined var: $OOPS"
echo "Step 3 — empty var was silently ignored"
EOF

# 2. Create the SAME script WITH strict mode
cat > test-strict.sh << 'EOF'
#!/bin/bash
set -euo pipefail
echo "Step 1"
ls /nonexistent/path
echo "Step 2 — you will never see this"
EOF

# 3. Compare behavior
echo "=== Without strict mode ==="
bash test-nostrict.sh 2>/dev/null

echo ""
echo "=== With strict mode ==="
bash test-strict.sh 2>/dev/null || echo "(Script exited with error — that's correct!)"

# 4. Clean up
rm test-nostrict.sh test-strict.sh

Exercise 3: Script template

# 1. Create a real script using the template
cat > sysinfo.sh << 'SCRIPT'
#!/bin/bash
set -euo pipefail

log() {
    echo "[$(date '+%H:%M:%S')] $*"
}

main() {
    log "System Information Report"
    echo "========================"
    echo "Hostname: $(hostname)"
    echo "User:     $USER"
    echo "Kernel:   $(uname -r)"
    echo "Uptime:   $(uptime -p)"
    echo "Memory:   $(free -h | awk '/Mem/ {print $3 " / " $2}')"
    echo "Disk:     $(df -h / | awk 'NR==2 {print $3 " / " $2 " (" $5 ")"}')"
    echo "========================"
    log "Report complete"
}

main "$@"
SCRIPT

# 2. Make executable and run
chmod +x sysinfo.sh
./sysinfo.sh

# 3. Clean up
rm sysinfo.sh

Key Takeaways

  • Every script starts with a shebang: #!/bin/bash
  • Always chmod +x before running with ./
  • Use set -euo pipefail (strict mode) in every script
  • ./script.sh runs in a subshell, source script.sh runs in current shell
  • Comment your scripts — your future self will thank you
  • Use a template for consistent, professional scripts

Next Lesson: Lesson 2: Variables & Data Types →