Lesson 1: Your First Script
Goal: Learn to create, configure, and run shell scripts with proper structure.
Table of Contents
- What is a Shell Script?
- The Shebang Line
- Creating Your First Script
- File Permissions
- Running Scripts
- Strict Mode — set Flags
- Comments
- Script Template
- 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/bashfor Linux scripts where bash is available - Use
#!/bin/shfor Docker containers (especially Alpine) and maximum portability - Use
#!/usr/bin/env bashwhen 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.
The recommended header
#!/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 immediatelySCRIPT_DIR— always know where the script lives (for relative paths)log()function — consistent, timestamped outputerror()function — errors go to stderr and exitmain()— organizes code;"$@"passes all argumentsreadonly— 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 +xbefore running with./ - Use
set -euo pipefail(strict mode) in every script ./script.shruns in a subshell,source script.shruns 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 →