health_check.sh already reads its configuration from the environment, but it is still a one-position button: it cannot explain itself, it does not accept "this time use threshold 60" without exporting a variable, and there is no way to tell it to be quiet. A script with no interface is a script only the person who wrote it can use, and this lesson gives it that interface. You are going to learn where information enters a script and where it must leave, and both questions have very concrete answers: arguments for what changes on each run, stdin for the data, stdout for the result and stderr for the human. That discipline is what makes a program composable, what makes it fit into a pipeline the way grep or sort do. By the end you will have health_check.sh with real options, and bookings_report.sh will be born.
Contents
- The four input channels
- Positional parameters
$@versus$*: the difference that causes the most bugs- Validating arguments and the help function
- Options: manual parsing and
getopts - Command-line conventions
read: reading from the user and from a file- Output: stdout for the result, stderr for the human
- Exit codes with meaning
- Configuration files and
--dry-runmode - Application: options in
health_check.shand the birth ofbookings_report.sh
- The four input channels
A script can receive information by four routes, and choosing badly is the first design decision that goes wrong.
| Channel | How it arrives | Use it for | Do not use it for |
|---|---|---|---|
| Arguments / stdin | script.sh --threshold 60 august / cat data | script.sh |
What changes on each run / data streams and pipelines | Large or secret data (visible in ps) / configuration |
| Environment | THRESHOLD=60 script.sh |
Inherited context, secrets, deployment settings | What gets typed every day |
| Config file | /etc/tramontana/app.conf |
Stable, shared settings | Values that change per run |
The course's precedence rule, from strongest to weakest: arguments > environment > configuration file > default values. It is what any administrator expects: it lets you fix the stable part in a file and override it occasionally from the command line.
- Positional parameters
When you invoke script.sh alpha beta, Bash fills in a set of special variables:
| Variable | Content |
|---|---|
$0 / $1 … $9 |
The name the script was invoked with / the arguments, in order |
${10} onwards |
From 10 on the braces are required: $10 is read as $1 followed by 0 |
$# / $@ / $* |
Number of arguments / all of them (see the next section) |
operator@srv-tramontana:~$ cat /tmp/args.sh
printf 'invoked as : %s (short: %s)\n' "$0" "${0##*/}"
printf 'arguments : %d -> [%s]\n' "$#" "$*"
shift 2
printf 'after shift 2 : %d -> [%s]\n' "$#" "$*"
operator@srv-tramontana:~$ bash /tmp/args.sh --verbose 08 report.txt
invoked as : /tmp/args.sh (short: args.sh)
arguments : 3 -> [--verbose 08 report.txt]
after shift 2 : 1 -> [report.txt]shift discards the first argument and moves the rest along: $2 becomes $1 and $# decreases; shift N discards N in one go. It is the basic mechanism of any parsing loop. ${0##*/} — parameter expansion from 04-02 — gives the name without the path, which is what you want in the help text. And you can reassign the positionals with set -- "${@:-08}" to apply a default value; the -- is essential, because without it set would take any argument starting with - as one of its own options.
$@ versus $*: the difference that causes the most bugs
$@ versus $*: the difference that causes the most bugsJust as with arrays in 04-02, the difference only appears inside double quotes, and that is where scripts break as soon as somebody passes a name with spaces.
operator@srv-tramontana:~$ cat /tmp/quotes.sh # three identical loops
for a in "$@"; do echo -n "[$a] "; done; echo ' <- "$@"'
for a in "$*"; do echo -n "[$a] "; done; echo ' <- "$*"'
for a in $@; do echo -n "[$a] "; done; echo ' <- $@ unquoted'
operator@srv-tramontana:~$ bash /tmp/quotes.sh "august report.txt" bookings.csv
[august report.txt] [bookings.csv] <- "$@"
[august report.txt bookings.csv] <- "$*"
[august] [report.txt] [bookings.csv] <- $@ unquoted"$@" is the only correct way to forward the arguments to another command: it produces exactly the ones you received, with their spaces intact. "$*" joins them into a string separated by the first character of IFS and is only good for printing. Unquoted, both are split and "august report.txt" becomes two files that do not exist. The rule, with no exceptions: "$@" to pass on, "$*" to display.
- Validating arguments and the help function
A serious script checks what it receives before doing anything, and it knows how to explain itself:
usage() {
cat <<EOF
Usage: ${0##*/} [options] <month>
Generates the bookings report for the given month (07, 08 or 09).
Options:
-o FILE Write the report to FILE instead of the standard path
-v Verbose mode: explains every step on stderr
-h Show this help and exit
EOF
}
[[ $# -ge 1 ]] || { usage >&2; exit 2; } # error: the help goes to stderrNote a detail almost nobody gets right, and one that separates a polite script from a rude one: if the user asks for help with -h, the help is the result and goes to stdout with code 0; if it is shown because they made a mistake, it is an error message and goes to stderr with a non-zero code. The difference matters: script.sh -h | less works in the first case, and script.sh 2>/dev/null does not silence the legitimate result in the second. The cat <<EOF is the here-document from 03-04: with no quotes around EOF, ${0##*/} is expanded.
- Options: manual parsing and
getopts
getoptsManual parsing with while and case
The only way to accept long options (--threshold) in pure Bash, and the pattern is always the same:
DRY_RUN=0; THRESHOLD=80 # defaults, weaker than everything else
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
-n|--dry-run) DRY_RUN=1; shift ;;
-u|--threshold) THRESHOLD="$2"; shift 2 ;;
--threshold=*) THRESHOLD="${1#*=}"; shift ;;
--) shift; break ;;
-*) printf 'Unknown option: %s\n' "$1" >&2; exit 2 ;;
*) break ;;
esac
doneEach branch consumes what belongs to it with shift (one if it is a switch, two if it carries a value) and the loop starts again. --threshold=60 is resolved with ${1#*=}, which trims up to the first =; -- marks the end of the options; anything starting with - that you do not recognise is a usage error, and anything else is the first positional and ends the parsing. (case is formalised in 04-04.)
getopts for short options
When short options are enough for you, getopts is a builtin that does all the work, including grouping -vn as -v -n:
VERBOSE=0; THRESHOLD=80
while getopts ":hvu:" option; do
case "$option" in
h) usage; exit 0 ;;
v) VERBOSE=1 ;;
u) THRESHOLD="$OPTARG" ;;
\?) printf 'Unknown option: -%s\n' "$OPTARG" >&2; usage >&2; exit 2 ;;
:) printf 'Option -%s needs a value\n' "$OPTARG" >&2; exit 2 ;;
esac
done
shift $(( OPTIND - 1 ))There are four pieces to understand:
- The string
":hvu:". Each letter is an option; a colon after a letter (u:) means "this one takes a value". - The leading colon. It turns on silent mode:
getoptsstops printing its own messages and reports to you with\?(unknown option) and:(missing value), putting the offending letter inOPTARG. Always include it, or your script will mix its own messages with Bash's. OPTARGholds the value of the current option, andOPTINDthe index of the next argument to process. The finalshift $(( OPTIND - 1 ))discards the consumed options and leaves the first real positional in$1. If you forget that line,$1will still be-v.
There is also getopt (without the s, GNU's external program), which does accept long options by reordering the arguments, but its syntax with eval set -- "$OPTIONS" is fragile and the macOS version is not compatible. In short: getopts if short options are enough, manual parsing if you need long ones, and getopt only if you know exactly what you are doing.
- Command-line conventions
Respecting them is what lets somebody else use your script without reading it.
| Option | Expected meaning |
|---|---|
-h, --help / --version |
Help or version to stdout and exit 0 |
-v, --verbose / -q, --quiet |
More detail on stderr / errors only |
-n, --dry-run / -f, --force |
Show what it would do / skip the confirmations |
-- |
End of options: what follows is data even if it starts with -. It solves a real problem: rm -- "-report.txt" is the only way to delete that file |
read: reading from the user and from a file
read: reading from the user and from a fileread takes a line from stdin and distributes it into variables. Its useful options:
| Option | Effect |
|---|---|
-r |
Does not interpret \ as an escape. Always include it; without it a Windows path is destroyed |
-p TEXT / -s |
Shows a prompt before reading / no echo, for passwords |
-t N / -n N / -a A |
Waits N seconds / reads N characters without Enter / distributes into array A |
operator@srv-tramontana:~$ read -r -s -p "DB password: " password; echo
DB password:
operator@srv-tramontana:~$ echo "${#password} characters read, not displayed"
14 characters read, not displayedThat -s is the correct way to ask for a secret interactively: it is left neither on the screen nor in the history. In 04-07 we will see the alternative for unattended tasks.
The canonical pattern for reading a file
while IFS= read -r line; do ... done < file is one of those lines worth memorising exactly as it is, because every piece is there for a specific reason:
IFS=(empty, and only for thisread) stops Bash trimming the leading and trailing spaces; without it, the indentation of a configuration file disappears.-rstops backslashes being interpreted as escapes, and< fileafter thedone— not a pipeline — is the important bit, as we are about to see.
The loop that loses its variables
operator@srv-tramontana:~$ counter=0; grep -c '' /var/log/tramontana/errors.log
87
operator@srv-tramontana:~$ cat /var/log/tramontana/errors.log |
> while read -r l; do counter=$(( counter + 1 )); done
operator@srv-tramontana:~$ echo "$counter"
0It went through 87 lines and the counter is 0. You have known the reason since 04-02: each stage of a pipeline runs in a subshell, so the while incremented its copy and that copy died when the pipeline closed. The same wall as with cd, in disguise. Three solutions, in order of preference:
# 1. Direct redirection: the while runs in the main shell
while read -r l; do (( ++counter )); done < /var/log/tramontana/errors.log
# 2. Process substitution: when the input comes from a command
while read -r l; do (( ++counter )); done < <(grep 500 /var/log/tramontana/errors.log)
# 3. lastpipe: makes the LAST stage run in the main shell
shopt -s lastpipe
grep 500 /var/log/tramontana/errors.log | while read -r l; do (( ++counter )); doneNumber 2 is the one you will use most. Number 3 has small print: lastpipe only works with job control disabled, which is the case in a non-interactive script but not in your terminal, so trying it by hand can mislead you.
- Output: stdout for the result, stderr for the human
The rule that turns a script into a piece of Unix: stdout is the result — what another program might want to process, data and nothing but data — and stderr is everything else: warnings, progress, errors, -v messages. If you mix them, my_script.sh | awk '{...}' chokes on your "Processing..." and my_script.sh > report.txt leaves the errors inside the report. The two functions that guarantee the discipline:
# Informative log and error, both with an ISO timestamp and going to stderr.
log() { printf '[%s] %s\n' "$(date '+%F %T')" "$*" >&2; }
error() { printf '[%s] ERROR: %s\n' "$(date '+%F %T')" "$*" >&2; }
operator@srv-tramontana:~$ bash /tmp/demo_log.sh > /tmp/output.txt
[2026-08-18 10:02:11] Checking the active release
operator@srv-tramontana:~$ cat /tmp/output.txt
3.2.1The messages appeared in the terminal and the file contains only the data: that is a composable script. We use printf instead of echo for the reason given in 04-02: defined behaviour and controlled formatting.
- Exit codes with meaning
With the interface in place, the exit code stops being "0 or 1" and becomes executable documentation. The Tramontana convention borrows the values from sysexits.h:
| Code | Name | When |
|---|---|---|
0 / 1 |
OK / generic error | Everything correct / unclassified failure |
2 |
Incorrect usage | Missing arguments, unknown option |
64 / 65 |
EX_USAGE / EX_DATAERR |
Incorrect usage, in sysexits scripts / malformed data |
66 / 69 |
EX_NOINPUT / EX_UNAVAILABLE |
The input cannot be read / a service is not responding |
73 / 78 |
EX_CANTCREAT / EX_CONFIG |
The output could not be created / incorrect configuration |
The essential thing is not to adopt exactly these numbers, but to choose some, document them in the header and be consistent: a script whose 69 always means "the application is not responding" lets the caller act without reading its output.
- Configuration files and
--dry-run mode
--dry-run modeThere are two ways to read a configuration file, and one of them is dangerous. source /etc/tramontana/app.conf is convenient, but the file is code executed with your permissions: if somebody with write access to it adds rm -rf /, your script runs it. It is the same risk that left a credential exposed in a backup with 644 permissions. You can only live with source if the file is yours, with strict, verified permissions. The safe alternative is to parse it:
read_config() {
local file="$1" key value
[[ -r $file ]] || return 0 # no file, default values
while IFS='=' read -r key value; do
key="${key// /}" # strip spaces from the key
[[ -z $key || $key == \#* ]] && continue # skip blanks and comments
value="${value#"${value%%[![:space:]]*}"}" # and those leading the value
case "$key" in
max_connections) MAX_CONNECTIONS="$value" ;;
query_timeout) TIMEOUT="$value" ;;
*) : ;; # unknown: ignored
esac
done < "$file"
}Only the keys the script knows about are accepted. It is more code, but it is the difference between reading data and running somebody else's code.
--dry-run mode is the other indispensable pattern, and it is implemented with a wrapper:
DRY_RUN=0
run() { if (( DRY_RUN )); then printf '[dry-run] %s\n' "$*" >&2; else "$@"; fi; }
run rm -f /srv/tramontana/backups/temp/partial.tar"$@" inside run rebuilds the command with its exact arguments — hence why section 3 mattered so much — and with DRY_RUN=1 it merely prints it. The rule from 03-03 thus becomes automatic: every script that deletes, moves or overwrites anything has a --dry-run.
- Application: options in
health_check.sh and the birth of bookings_report.sh
health_check.sh and the birth of bookings_report.shThe header of health_check.sh now documents the syntax and the codes (# Usage: health_check.sh [-h] [-v] [-u THRESHOLD], # Exit: 0 correct | 2 incorrect usage), and this is slotted in between the configuration from 04-02 and the body:
VERBOSE=0
log() { (( VERBOSE )) && printf '[%s] %s\n' "$(date '+%F %T')" "$*" >&2; return 0; }
error() { printf '[%s] ERROR: %s\n' "$(date '+%F %T')" "$*" >&2; }
usage() {
cat <<EOF
Usage: ${0##*/} [-h] [-v] [-u THRESHOLD]
Checks the application (port ${PORT}), the disk and today's errors.
-u THRESHOLD Disk percentage above which to warn (default ${DISK_THRESHOLD})
-v Verbose mode on stderr
-h This help
EOF
}
while getopts ":hvu:" option; do
case "$option" in
h) usage; exit 0 ;; v) VERBOSE=1 ;; u) DISK_THRESHOLD="$OPTARG" ;;
\?) error "unknown option: -$OPTARG"; usage >&2; exit 2 ;;
:) error "option -$OPTARG needs a value"; exit 2 ;;
esac
done
shift $(( OPTIND - 1 ))
log "starting health check (disk threshold: ${DISK_THRESHOLD}%)"
# ... the body from 04-02, unchanged ... -> log "health check finished"DISK_THRESHOLD can no longer be readonly: the -u option must override the environment value, which in turn overrides the default. It is the precedence from section 1, implemented.
operator@srv-tramontana:~$ ~/scripts/health_check.sh -v -u 60 > /tmp/health.txt
[2026-08-18 10:14:03] starting health check (disk threshold: 60%)
[2026-08-18 10:14:04] health check finished
operator@srv-tramontana:~$ ~/scripts/health_check.sh -x; echo "code: $?"
[2026-08-18 10:14:20] ERROR: unknown option: -x
code: 2The result went to the file and the trace to the terminal, exactly as section 8 promises. And notice the return 0 in log(): without it, when VERBOSE is 0 the function returns 1 and with set -e the script would die on the first call; it is one of the traps 04-06 pulls apart. Now for the new script Marta asked for, for her monthly report:
#!/usr/bin/env bash
#
# bookings_report.sh - Bookings report for a month of 2026.
# Author : Systems operator <operator@srv-tramontana> Date: 2026-08-18
# Usage : bookings_report.sh [-o FILE] [-v] <month: 07|08|09>
# Exit : 0 correct | 2 incorrect usage | 66 the CSV cannot be found
set -euo pipefail
readonly CSV="${TRAMONTANA_CSV:-/home/operator/data/bookings.csv}"
readonly WORK_BASE="/home/operator/work/2026"
OUTPUT=""; VERBOSE=0
log() { (( VERBOSE )) && printf '[%s] %s\n' "$(date '+%F %T')" "$*" >&2; return 0; }
error() { printf '[%s] ERROR: %s\n' "$(date '+%F %T')" "$*" >&2; }
usage() { printf 'Usage: %s [-o FILE] [-v] <month: 07|08|09>\n' "${0##*/}"; }
# Same getopts block as the previous script, with ":hvo:" and o) OUTPUT="$OPTARG".
shift $(( OPTIND - 1 )) # after this, $1 is the first real positional
[[ $# -eq 1 ]] || { error "one argument is required: the month"; usage >&2; exit 2; }
month="$1"
[[ -r $CSV ]] || { error "cannot read $CSV"; exit 66; }
# If -o was not given, the standard path for the month in the work tree.
: "${OUTPUT:=${WORK_BASE}/${month}/reports/bookings-${month}.txt}"
mkdir -p "${OUTPUT%/*}" && log "writing the report to $OUTPUT"
{
printf 'BOOKINGS REPORT - MONTH %s\n\n' "$month"
LC_ALL=C awk -F';' -v month="2026-$month" '
NR>1 && $2 ~ "^" month { r[$3]++; n[$3]+=$5; a[$3]+=$6; tr++; tn+=$5; ta+=$6 }
END { printf "%-14s %9s %8s %12s\n", "HOUSE", "BOOKINGS", "NIGHTS", "AMOUNT"
for (h in r) printf "%-14s %9d %8d %12.2f\n", h, r[h], n[h], a[h]
printf "%-14s %9d %8d %12.2f\n", "TOTAL", tr, tn, ta }' "$CSV"
} > "$OUTPUT"
log "report finished"
printf '%s\n' "$OUTPUT" # stdout: the path, for chaining
exit 0operator@srv-tramontana:~$ filepath=$(~/scripts/bookings_report.sh -v 08)
[2026-08-18 10:21:40] writing the report to /home/operator/work/2026/08/reports/bookings-08.txt
[2026-08-18 10:21:40] report finished
operator@srv-tramontana:~$ head -3 "$filepath"; tail -1 "$filepath"
BOOKINGS REPORT - MONTH 08
HOUSE BOOKINGS NIGHTS AMOUNT
TOTAL 25 70 7842.50Two design details: : "${OUTPUT:=...}" uses the : builtin (which does nothing) purely to force the ${VAR:=value} default assignment from 04-02, and the script prints the path on stdout, so that filepath=$(...) captures it and the report is ready to be chained with mail or scp.
Common Mistakes and Tips
- Forgetting
shift $(( OPTIND - 1 )). Failure number one withgetopts: the options still occupy$1and the script thinks it is missing arguments. Number two is using$@or$*without quotes, whereupon any argument with spaces gets split. readwithout-r. A path with\is silently corrupted; there is no case in which you wantreadwithout-r. And avoidwhile readloops at the end of a pipeline: the variables die with the subshell, so use< fileor< <(command).- Sending progress messages to stdout. You contaminate the result and break any pipeline: anything that is not the data goes to stderr. And
getoptswithout the leading colon makes your script print messages you do not control. - Putting a secret on the command line. Anybody with access to
pssees it, and it stays in~/.bash_history. A file with 600 permissions or an environment variable; we close this in 04-07 and in 06-05. - Tip: write the
usage()function before the body; if you struggle to describe the interface, you have not decided it yet. And make-halways work, even with no arguments and with no valid configuration: it is the first thing anybody who finds your script tries. - Tip: validate the arguments as soon as you have them and exit immediately if something does not add up. The sooner it fails, the less it has wrecked.
Exercises
Exercise 1. Write ~/scripts/count_errors.sh, which reads from stdin a log in the format of access.log and accepts -c CODE (500 by default), -v and -h. It must print on stdout only the number of lines with that code and, with -v, how many lines it read in total, on stderr. access.log has 412 lines and 14 responses with code 500.
Exercise 2. Extend bookings_report.sh with -n/--dry-run, which shows which file would be written without creating it, using the run() wrapper. Explain why the parsing has to move from getopts to manual.
Exercise 3. Luis has written this to count how many distinct IPs there are in access.log and says "it always returns 0, must be something about the log": n=0; cut -d' ' -f6 access.log | sort -u | while read ip; do n=$((n+1)); done; echo "$n". Diagnose it and give two solutions.
Solutions
Solution 1. Header and getopts as in sections 4 and 5, with ":hvc:" and CODE="$OPTARG". The interesting part is the body:
# The loop reads from stdin, inherited from whoever invokes the script; as
# there is no pipeline inside, the variables survive. read distributes the
# line into fields by IFS: the code is the fifth in access.log's format.
total=0; matches=0
while read -r _f _h _m _r code _rest; do
(( ++total ))
[[ $code == "$CODE" ]] && (( ++matches ))
done
(( VERBOSE )) && printf 'Read %d lines looking for code %s\n' "$total" "$CODE" >&2
printf '%d\n' "$matches" # stdout: the data only
exit 0operator@srv-tramontana:~$ ~/scripts/count_errors.sh -v < /var/log/tramontana/access.log
Read 412 lines looking for code 500
14read with several names chops the line up by IFS without calling cut or awk: 412 processes saved. Here we do omit IFS=, because splitting into fields is precisely what we want.
Solution 2. You have to move to manual parsing because getopts does not accept long options: --dry-run would arrive whole and getopts would read it as -, -d, -r… You replace the while getopts with the while [[ $# -gt 0 ]] loop from section 5, adding the branch -n|--dry-run) DRY_RUN=1; shift ;;, and the wrapper does the rest:
run() { if (( DRY_RUN )); then printf '[dry-run] %s\n' "$*" >&2; else "$@"; fi; }
run mkdir -p "${OUTPUT%/*}"
if (( DRY_RUN )); then log "the report would be written to $OUTPUT"
else generate_report > "$OUTPUT" # the { ... } block extracted into a function
fi
printf '%s\n' "$OUTPUT"operator@srv-tramontana:~$ ~/scripts/bookings_report.sh --dry-run -v 09
[2026-08-18 10:33:02] the report would be written to /home/operator/work/2026/09/reports/bookings-09.txt
/home/operator/work/2026/09/reports/bookings-09.txt
operator@srv-tramontana:~$ ls /home/operator/work/2026/09/reports/The directory is still empty: nothing was created. And note that the redirection > "$OUTPUT" cannot go through run, because the shell resolves it before invoking the function; hence that case having its own if. It is the classic limitation of the pattern.
Solution 3. The diagnosis: the while read is the last stage of a pipeline, so it runs in a subshell; n is incremented in the copy and the echo in the main shell sees the original 0. It has nothing to do with the log.
# A: process substitution, the while runs in the main shell
n=0; while IFS= read -r ip; do (( ++n )); done < <(cut -d' ' -f6 access.log | sort -u)
# B: do not use a loop at all, which is the right answer here
n=$(cut -d' ' -f6 access.log | sort -u | wc -l)
printf 'Distinct IPs: %d\n' "$n" # -> Distinct IPs: 5The five known IPs (10.0.2.77, .31, .44, .52 and .18). Prefer B: if the loop only counts, you do not need the loop. A third route would be shopt -s lastpipe, but it does not work in your interactive terminal and you might conclude your fix is no good.
Conclusion
Your scripts now have an interface, and with it they stop being yours and become usable.
- You distinguish the four input channels and apply the precedence arguments > environment > configuration > defaults.
- You handle the positional parameters with
$#,shift,${10}andset --, and you know that"$@"is the only correct way to forward arguments while"$*"is only good for displaying. - You write a
usage()function that goes to stdout when it is asked for and to stderr when it is an error, and you parse options withgetopts— option string, leading colon,OPTARGand the indispensableshift $(( OPTIND - 1 ))— or by hand withwhileandcasefor the long ones, respecting the-h,-v,-q,-nand--conventions. - You use
readwith-ralways, and you know the canonical patternwhile IFS= read -r line; do ... done < file, with the three solutions to the loop that loses its variables to the subshell. - You separate stdout (the result) from stderr (the human) with
log()anderror(), you return documented exit codes, you read configuration by parsing it instead of withsource, and you build in--dry-runwith therun()wrapper.
But look at the code you have written today: it is full of if, of case and of [[ ]] used on intuition, without ever having studied them. It is time to put that right. In Control Structures you will see the idea that holds the whole of Bash up — that it branches on a command's exit code, not on a boolean — the three kinds of test and when to use each, the complete table of file, string and number operators, =~ with BASH_REMATCH for validating dates, case with its patterns and its three terminators, all the loops with the warning about why never for f in $(ls), and a real measurement over access.log that will tell you when a Bash loop is a thousand times slower than an awk. By the end of it, health_check.sh will be able to say OK, WARN or CRITICAL, and purge_releases.sh will walk /opt/tramontana/releases/ without touching the active version.
Linux Course: From Beginner to System Administrator
Module 1: Introduction to Linux
- What Is Linux?
- History of Linux
- Linux Distributions
- Installing Linux
- First Contact with the System
- The Linux File System Structure
Module 2: Basic Linux Commands
- Introduction to the Command Line
- Getting Help and System Documentation
- Navigating the File System
- File and Directory Operations
- Viewing and Editing Files
- Hard and Symbolic Links
- File Permissions and Ownership
Module 3: Advanced Command-Line Skills
- The Shell Environment: Variables, Aliases and History
- Using Wildcards and Regular Expressions
- Searching Files and Content: find, locate and grep
- Pipes and Redirection
- Text Processing: cut, sort, uniq, sed and awk
- Process Management
- Scheduling Tasks with Cron
- Networking Commands
Module 4: Shell Scripting
- Introduction to Shell Scripting
- Variables and Data Types
- Script Input, Output and Arguments
- Control Structures
- Functions and Libraries
- Debugging and Error Handling
- Production Scripts: Best Practices
Module 5: System Administration
- User and Group Management
- sudo and Special Permissions
- Package Management
- Disk Management
- systemd and Service Management
- System Logs: journald and syslog
- System Monitoring and Performance Tuning
- Backup and Restore
Module 6: Networking and Security
- Network Configuration
- SSH and Remote Access
- Firewalls and Perimeter Security
- Intrusion Detection Systems
- Secrets Management and TLS Certificates
- Securing Linux Systems
Module 7: Advanced Topics
- The Boot Process and System Recovery
- Advanced Diagnostics: strace, perf and eBPF
- Linux Kernel Tuning
- Virtualization with Linux
- Linux Containers and Docker
- Automation with Ansible
- High Availability and Load Balancing
