health_check.sh already exists, but it carries the disk threshold written by hand in the body and the server IP just above it. The day Marta asks you to watch the test server too you will have to duplicate the file, and from that moment on you will have two scripts that drift apart little by little until one of them is lying. The cure is to take the values out of the body and put them into well-thought-out variables.
This lesson is about that, but above all it is about something deeper: in Bash everything is a string of text, and that single sentence explains 90% of the surprises coming your way. In exchange, Bash gives you an arsenal of string manipulation — parameter expansion — so powerful that it replaces basename, dirname, cut, tr and a good part of sed without launching a single process.
Contents
- Bash has no types: everything is a string
- Assignment: the three rules that catch everybody out
- Scope: script, subshell and environment
declareand its attributes- Parameter expansion in depth
- Arithmetic, and why Bash knows nothing about decimals
- Indexed arrays
- Associative arrays: bookings per house
- Strings and
printf - Application: a configurable
health_check.sh
- Bash has no types: everything is a string
In Python or Java you declare the type and the language protects you. In Bash there are no types: every variable holds a string of bytes, and the "type" only appears when you use it, according to the context.
operator@srv-tramontana:~$ n=42; echo "${n} + 1 = $((n + 1))"
42 + 1 = 43
operator@srv-tramontana:~$ n=42abc; echo "$((n + 1))"
-bash: 42abc: value too great for base (error token is "42abc")The same variable is a number inside $(( )) and text outside it. Nobody warned you when you assigned 42abc; the error arrived when you tried to add. Three consequences worth internalising today:
- Nothing validates what you store. Validating is your job, and we will formalise it in 04-04 with
[[ $v =~ ^[0-9]+$ ]]. - Comparing numbers and comparing strings are different operations.
"10" < "9"is true as text and false as a number; hence-ltand<existing separately. - There are no decimals.
$(( 7842 / 25 ))gives 313, not 313.70 (section 6). The only partial exceptions to the rule aredeclare's attributes and arrays, which really are a genuine data structure.
- Assignment: the three rules that catch everybody out
operator@srv-tramontana:~$ version=3.2.1 # correct
operator@srv-tramontana:~$ version = 3.2.1
-bash: version: command not found
operator@srv-tramontana:~$ message=backup complete
-bash: complete: command not found
operator@srv-tramontana:~$ message="backup complete" # correct
operator@srv-tramontana:~$ LC_ALL=C sort ~/data/houses.txt | head -1
cal-ferrer
operator@srv-tramontana:~$ echo "LC_ALL is still: '${LC_ALL:-empty}'"
LC_ALL is still: 'empty'- No spaces around the
=.version = 3.2.1is not an assignment: Bash reads it as the commandversionwith the arguments=and3.2.1. It is the rule most often broken by people coming from other languages, and the error message is no help at all. - Quotes if there are spaces. Without them the assignment ends at the first space. Working rule: always quote the value, whether it has spaces or not.
- To use it,
$; to assign it, no$.name=valueto write,"$name"to read.
The last two lines of the example show something else: putting a variable in front of a command makes it exist only for that one execution, which is exactly what the course's LC_ALL=C convention does. On names: letters, digits and _, not starting with a digit; UPPERCASE for constants and the environment, lowercase for working variables. And avoid trampling on PATH, HOME, IFS, PWD, RANDOM or SECONDS, which already have an owner.
- Scope: script, subshell and environment
Picking up 03-01 again, now from inside a script:
| Scope | How it is created | Who sees it |
|---|---|---|
| Shell | VAR=value |
Only the current script, including its functions |
| Environment | export VAR=value |
The script and every child process it launches |
| Local | local var=value (in functions) |
Only the function and the ones it calls |
operator@srv-tramontana:~$ cat /tmp/scope.sh
HOUSE="mas-figueres"; export RELEASE="3.2.1"
bash -c 'echo "child sees HOUSE=[${HOUSE:-}] RELEASE=[${RELEASE:-}]"'
( HOUSE="can-ventos"; echo "subshell changes HOUSE to $HOUSE" )
echo "the parent still has HOUSE=$HOUSE"
operator@srv-tramontana:~$ bash /tmp/scope.sh
child sees HOUSE=[] RELEASE=[3.2.1]
subshell changes HOUSE to can-ventos
the parent still has HOUSE=mas-figueresThree lessons in six lines. The child does not see HOUSE because it is not exported, but it does see RELEASE. The subshell — the parentheses — does see everything from the parent because it is a copy of it, but its changes do not come back: the same wall you met with cd, and the one that will make a while read inside a pipeline lose its variables (04-03). As for local, it can only be used inside a function and it is a preview of 04-05, but the rule is so important that we state it now: every variable created inside a function must be local; without it, it is global, and one function can silently trample another one's counter.
declare and its attributes
declare and its attributesdeclare (or its synonym typeset) creates variables with attributes, the closest thing to a type that Bash has.
| Attribute | Effect |
|---|---|
-r / -x |
Read-only (= readonly) / exported (= export) |
-i |
Integer: assignments are evaluated as arithmetic |
-a / -A |
Indexed / associative array — the latter must be declared |
-l / -u |
Converts to lowercase / uppercase on assignment |
-p |
Does not declare: prints how a variable is declared |
operator@srv-tramontana:~$ declare -r ACTIVE_RELEASE="3.2.1"; ACTIVE_RELEASE="3.3.0"
-bash: ACTIVE_RELEASE: readonly variable
operator@srv-tramontana:~$ declare -i errors=0; errors+=14; errors+=5; echo "$errors"
19
operator@srv-tramontana:~$ declare -u code="ok"; echo "$code"; declare -p errors
OK
declare -i errors="19"Look at errors+=14: with the -i attribute it adds, whereas without it, it would concatenate and leave 014. That different behaviour depending on an invisible attribute is why many people prefer explicit arithmetic with $(( )); we will use -i only for obvious counters. readonly we will use a lot: it turns a logic error — reassigning a constant by accident — into an immediate, noisy error, always better than a silent one.
- Parameter expansion in depth
This is where Bash stops looking like a toy language. Everything that follows happens inside the shell, without launching processes. We start from a real name from our backups:
operator@srv-tramontana:~$ f="/srv/tramontana/backups/outgoing/bookings-2026-08-18.csv.gz"
operator@srv-tramontana:~$ base="${f##*/}"; echo "$base" # after the last /
bookings-2026-08-18.csv.gz
operator@srv-tramontana:~$ echo "${f%/*}" # before the last /
/srv/tramontana/backups/outgoing
operator@srv-tramontana:~$ echo "${base%%.*}" # up to the first dot
bookings-2026-08-18
operator@srv-tramontana:~$ echo "${base#*-}" # after the first hyphen
2026-08-18.csv.gzThat is basename and dirname without basename or dirname. The mnemonic is visual: on a keyboard, # is to the left of $ and strips from the left; % is to the right and strips from the right. Doubling the symbol makes the pattern greedy, like the greediness from 03-02. Careful: these are globbing patterns (*, ?, [...]), not regular expressions.
| Expansion | What it returns |
|---|---|
${VAR:-value} / ${VAR:=value} |
value if VAR is empty or unset; the second form also assigns |
${VAR:?message} |
The value, or aborts the script with that message if it is empty |
${VAR:+value} |
value only if VAR has content; otherwise, the empty string |
${#VAR} / ${VAR:off:len} |
Length in characters / substring from off (0-based) |
${VAR#pattern} / ${VAR##pattern} |
Removes the shortest / longest matching prefix |
${VAR%pattern} / ${VAR%%pattern} |
Removes the shortest / longest matching suffix |
${VAR/pat/new} / ${VAR//pat/new} |
Replaces the first occurrence / all of them |
${VAR^^} / ${VAR,,} |
Everything to uppercase / to lowercase |
The first four are the basis of default configuration, and :+ is the least known: it adds something only if there is something to add.
operator@srv-tramontana:~$ datestamp="2026-08-18"; level="warn"; filepath="/houses/el-molí"; verbose=""
operator@srv-tramontana:~$ echo "Year ${datestamp:0:4}, month ${datestamp:5:2}, length ${#datestamp}"
Year 2026, month 08, length 10
operator@srv-tramontana:~$ echo "${filepath//í/i} → [${level^^}]"
/houses/el-moli → [WARN]
operator@srv-tramontana:~$ echo "curl -s ${verbose:+-v} ${URL:?the URL to check is missing}"
-bash: URL: the URL to check is missingThe ${filepath//í/i} resolves, without calling sed, the accent the application puts into the links to /houses/el-molí and which causes the four 404s in access.log. The ${verbose:+-v} adds the option only if the variable has content, which is how conditional command lines are built. And ${VAR:?message}, already seen in 03-01, is the cheapest way to demand a mandatory value.
- Arithmetic, and why Bash knows nothing about decimals
Bash has two ways of doing sums and both work only with signed 64-bit integers.
operator@srv-tramontana:~$ echo "$(( (14 + 5 + 4) * 100 / 412 ))" # % of errors
5
operator@srv-tramontana:~$ i=0; (( i++ )); (( i += 10 )); echo "$i"
11$(( )) substitutes the result, so you use it wherever you want the value. (( )) evaluates and prints nothing: it is for modifying variables or for use as a condition, returning 0 (success) if the result is non-zero. Inside both, variables go without $ and you have + - * / % **, comparisons, && || !, ++ --, += and the ternary a ? b : c.
Two warnings: a number with a leading zero is interpreted as octal: $(( 08 )) is a syntax error, which ruins any calculation involving months (08, 09); force it with $(( 10#08 )). And (( i++ )) with i at 0 returns 1, because the value of the expression is the one before the increment, and with set -e that aborts the script; it is a trap from 04-06, and for now use (( ++i )) or i=$(( i + 1 )).
For decimals you have to leave Bash:
operator@srv-tramontana:~$ echo "scale=2; 7842.50 / 25" | bc
313.70
operator@srv-tramontana:~$ LC_ALL=C awk -F';' 'NR>1 { s += $6; n++ } END { printf "%.2f\n", s/n }' \
/home/operator/data/bookings.csv
313.70Tramontana's average amount per booking is 313.70 €. You have to tell bc the scale (scale=2) because by default it truncates to zero decimals when dividing; bc -l loads the maths library and raises it to 20. But look at the second example: if the data is already in a file, awk does the reading and the sums at once. The course's rule: bc for a one-off calculation, awk as soon as there is a file. And the LC_ALL=C is not decorative: under a locale that uses the comma, printf '%.2f' would print 313,70, and that comma breaks any later comparison.
- Indexed arrays
An array is an ordered list of strings with a numeric index starting at 0. It solves "storing several things in one variable" without resorting to space-separated strings, which break as soon as an element contains one.
operator@srv-tramontana:~$ releases=(3.1.0 3.2.0 3.2.1); releases+=(3.3.0)
operator@srv-tramontana:~$ echo "${releases[0]} ... ${releases[-1]} (total ${#releases[@]})"
3.1.0 ... 3.3.0 (total 4)
operator@srv-tramontana:~$ echo "${releases[@]:1:2}"
3.2.0 3.2.1
operator@srv-tramontana:~$ unset 'releases[0]'; echo "${!releases[@]}"
1 2 3It is created with parentheses, extended with +=(...), [-1] is the last one, ${#array[@]} counts elements and ${array[@]:1:2} is a slice. The last command reveals something important: unset leaves a hole, the indices are not renumbered; that is why ${!array[@]} exists, giving you the indices and not the values.
${array[@]} versus ${array[*]}
The difference that causes the most bugs, and it only shows up inside double quotes:
operator@srv-tramontana:~$ files=("august report.pdf" "bookings.csv")
operator@srv-tramontana:~$ for f in "${files[@]}"; do echo -n "[$f] "; done; echo
[august report.pdf] [bookings.csv]
operator@srv-tramontana:~$ for f in "${files[*]}"; do echo -n "[$f] "; done; echo
[august report.pdf bookings.csv]
operator@srv-tramontana:~$ for f in ${files[@]}; do echo -n "[$f] "; done; echo
[august] [report.pdf] [bookings.csv]"${array[@]}" produces one argument per element, respecting spaces: it is what you want 99% of the time. "${array[*]}" produces a single argument with the elements joined by the first character of IFS, useful only for printing. And without quotes Bash splits on spaces, and "august report.pdf" becomes two files that do not exist. Burn it in: arrays always with "${array[@]}".
- Associative arrays: bookings per house
An associative array indexes by string instead of by number: it is what awk gave you in 03-05, now inside Bash. It has to be declared with declare -A before use; otherwise Bash treats it as indexed and all the keys collapse into index 0.
#!/usr/bin/env bash
set -euo pipefail
CSV="/home/operator/data/bookings.csv"
declare -A house_nights
# tail -n +2 skips the header. IFS=';' splits each line on the CSV
# separator; read -r assigns each field to a variable (04-03 details it).
# ${var:-0} gives 0 the first time a house appears, with set -u active.
while IFS=';' read -r _id _date house _guest nights _amount; do
house_nights["$house"]=$(( ${house_nights["$house"]:-0} + nights ))
done < <(tail -n +2 "$CSV")
# ${!array[@]} are the KEYS; sort leaves them in a reproducible order.
for house in $(printf '%s\n' "${!house_nights[@]}" | LC_ALL=C sort); do
printf '%-14s %3d nights\n' "$house" "${house_nights[$house]}"
doneoperator@srv-tramontana:~$ bash /tmp/per_house.sh
cal-ferrer 11 nights can-ventos 17 nights
el-moli 8 nights la-solana 13 nights
mas-figueres 21 nightsThe 70 known nights, distributed (shown here in two columns to save space). Two new pieces: < <(tail -n +2 "$CSV") is a process substitution that stops the while running in a subshell and losing the arrays when it finishes — 04-03 details it — and _id, _date, _guest are the convention for "I read this field but I do not use it", which also keeps shellcheck quiet. An honest note: this report fits in one line of awk. Associative arrays shine when what you accumulate is not tabular text — exit codes per service, paths already processed, a cache; with a CSV, awk still wins. The rule is formalised in 04-04.
- Strings and
printf
printfConcatenating means writing one thing after another, and += appends at the end:
operator@srv-tramontana:~$ host="10.0.2.15"; port=8080
operator@srv-tramontana:~$ url="http://${host}:${port}"; url+="/health"; echo "$url"
http://10.0.2.15:8080/healthThe braces in ${host} are not optional: without them $host: would work by luck, but $portX would look for a variable called portX. Use braces whenever the variable touches adjoining text. For output, printf replaces echo in everything serious: echo varies between shells and does odd things with -n and backslashes, whereas printf is precisely defined and reuses the format while arguments remain, which turns it into a table generator.
| Directive | Meaning |
|---|---|
%s / %d |
String / decimal integer |
%.2f |
Floating point with two decimals |
%-20s |
String left-aligned in 20 columns |
%10.2f |
Float in 10 columns, right-aligned |
%% / \n / \t |
A literal % / newline / tab |
operator@srv-tramontana:~$ LC_ALL=C printf '%-14s %6d %10.2f\n' \
mas-figueres 21 2450 can-ventos 17 1980 la-solana 13 1425.50
mas-figueres 21 2450.00
can-ventos 17 1980.00
la-solana 13 1425.50A single call, three rows: the format was reused three times. That is the table for Marta's report, and the reason echo is now left only for one-line messages with no formatting.
- Application: a configurable
health_check.sh
health_check.shThe underlying change: no value is nailed into the body any more. They are all constants at the top, and the ones it makes sense to adjust from outside are read from the environment with ${VAR:-default}.
#!/usr/bin/env bash
#
# health_check.sh - Quick health check for Tramontana Bookings.
#
# Author : Systems operator <operator@srv-tramontana>
# Date : 2026-08-18
# Usage : health_check.sh
# Env : TRAMONTANA_HOST, TRAMONTANA_PORT, TRAMONTANA_DISK_THRESHOLD,
# TRAMONTANA_ERROR_LOG
set -euo pipefail
# --- Configuration (environment > default value) --------------------------
readonly HOST="${TRAMONTANA_HOST:-10.0.2.15}"
readonly PORT="${TRAMONTANA_PORT:-8080}"
readonly DISK_THRESHOLD="${TRAMONTANA_DISK_THRESHOLD:-80}"
readonly ERROR_LOG="${TRAMONTANA_ERROR_LOG:-/var/log/tramontana/errors.log}"
readonly MAX_WAIT=5
readonly HEALTH_URL="http://${HOST}:${PORT}/health"
# --- Body -----------------------------------------------------------------
LC_ALL=C printf '=== Tramontana Bookings health (%s) ===\n' "$(date '+%F %T')"
printf 'Server : %s Target: %s\n\n' "$(hostname)" "$HEALTH_URL"
http_code=$(curl -s -o /dev/null -w '%{http_code}' \
--max-time "$MAX_WAIT" "$HEALTH_URL" || true)
disk_usage=$(df --output=pcent / | tail -1 | tr -dc '0-9')
free_kb=$(df --output=avail / | tail -1 | tr -dc '0-9')
# An integer of KiB says nothing to a human: we turn it into GiB with one decimal.
free_gb=$(LC_ALL=C awk -v k="$free_kb" 'BEGIN { printf "%.1f", k/1048576 }')
today=$(date '+%Y-%m-%d')
errors_today=$(grep -c "^$today" "$ERROR_LOG" || true)
LC_ALL=C printf '%-14s %s\n' \
"HTTP:" "$http_code" \
"Disk:" "${disk_usage}% used, ${free_gb} GiB free (threshold ${DISK_THRESHOLD}%)" \
"Errors:" "$errors_today today"
exit 0operator@srv-tramontana:~$ ~/scripts/health_check.sh
=== Tramontana Bookings health (2026-08-18 09:41:02) ===
Server : srv-tramontana Target: http://10.0.2.15:8080/health
HTTP: 200
Disk: 30% used, 15.4 GiB free (threshold 80%)
Errors: 6 today
operator@srv-tramontana:~$ TRAMONTANA_PORT=9090 ~/scripts/health_check.sh | sed -n 2p
Server : srv-tramontana Target: http://10.0.2.15:9090/healthWithout touching the file, the script points at another port. That is the first step of the precedence chain arguments > environment > configuration file > defaults that we will complete in 04-03 and close in 04-07. It still compares nothing against DISK_THRESHOLD: that arrives in 04-04.
Common Mistakes and Tips
- Spaces around the
=.VAR = valueassigns nothing: it is error number one. Number two is using$VARwithout quotes: if the value contains spaces or a*, Bash splits it or expands it against the disk. SC2086 is exactly this, and it is not a pedantic warning: it is a failure waiting for its file with a space in the name. - Forgetting
declare -A. Without it,data[mas-figueres]=1evaluates the key as arithmetic, turns it into 0 and all the houses trample each other, with no error whatsoever. And do not confuse${VAR:-x}with${VAR:=x}: the first does not modify the variable, the second does. ${array[*]}in a loop. It iterates over a single gigantic element; always use"${array[@]}". And beware of leading zeros:$(( 08 + 1 ))fails because of octal, so write10#$month.- Expecting decimals from
$(( )).$(( 7842 / 25 ))gives 313 and the remainder is lost without warning. For money,awkorbcwithLC_ALL=C. - Tip:
declare -p VARtells you what a variable really is — array, integer, exported — when something does not add up. It is thestatof variables. - Tip: prefer parameter expansion to
basename,dirname,cutortrfor simple things: in a loop of a thousand iterations that is a thousand processes you save. And declare every constantreadonly.
Exercises
Exercise 1. Given f="/srv/tramontana/backups/pre-deploy/app-3.2.1-2026-08-11.tar.gz", obtain using parameter expansion only (no basename, no cut, no sed): the directory, the file name, the name without the extension, the version 3.2.1 and the date 2026-08-11. Explain each expansion.
Exercise 2. Write ~/scripts/bookings_summary.sh which, reading bookings.csv, prints an aligned table with house, bookings, nights and amount, plus a totals line. Use an associative array and printf. It must match the known data: 25 bookings, 70 nights, 7,842.50 €.
Exercise 3. Luis proposes this to calculate the percentage of POST requests out of the 412 in access.log. Explain the three flaws and rewrite it.
Solutions
Solution 1.
operator@srv-tramontana:~$ f="/srv/tramontana/backups/pre-deploy/app-3.2.1-2026-08-11.tar.gz"
operator@srv-tramontana:~$ base="${f##*/}"; name="${base%%.tar.gz}"; noapp="${name#app-}"
operator@srv-tramontana:~$ printf '%s\n%s\n%s\n%s\n%s\n' "${f%/*}" "$base" "$name" \
"${noapp%%-*}" "${name##*-[0-9].[0-9].[0-9]-}"
/srv/tramontana/backups/pre-deploy
app-3.2.1-2026-08-11.tar.gz
app-3.2.1-2026-08-11
3.2.1
2026-08-11${f%/*}removes the shortest suffix starting with/: that isdirname.${f##*/}removes the longest prefix ending in/: that isbasename.${base%%.tar.gz}trims the compound extension. Here%%and%give the same result because the pattern is literal, but%%.*would have been dangerous: it would also cut at the dot in3.2.1, leavingapp-3.- The version comes out in two steps:
#app-removes the prefix and%%-*trims from the first remaining hyphen, the one before the date. For the date,##*-[0-9].[0-9].[0-9]-uses a globbing pattern — not a regex — that consumes everything up to just past the version number.
Solution 2.
#!/usr/bin/env bash
#
# bookings_summary.sh - Table of bookings, nights and amount per house.
# Author : Systems operator <operator@srv-tramontana> Date: 2026-08-18
set -euo pipefail
readonly CSV="${TRAMONTANA_CSV:-/home/operator/data/bookings.csv}"
declare -A n_bookings n_nights amounts
declare -i total_bookings=0 total_nights=0
all_amounts=""
# The amounts are accumulated as space-separated text, because Bash cannot
# add decimals; the sum is done by awk with LC_ALL=C for the decimal point.
sum() { LC_ALL=C awk '{ s=0; for (i=1;i<=NF;i++) s+=$i; printf "%.2f", s }' <<<"$1"; }
while IFS=';' read -r _id _date house _guest nights amount; do
n_bookings["$house"]=$(( ${n_bookings["$house"]:-0} + 1 ))
n_nights["$house"]=$(( ${n_nights["$house"]:-0} + nights ))
amounts["$house"]="${amounts["$house"]:-}${amount} "
all_amounts+="${amount} "
total_bookings+=1; total_nights+=nights
done < <(tail -n +2 "$CSV")
LC_ALL=C printf '%-14s %9s %8s %12s\n' HOUSE BOOKINGS NIGHTS AMOUNT
for house in $(printf '%s\n' "${!n_bookings[@]}" | LC_ALL=C sort); do
LC_ALL=C printf '%-14s %9d %8d %12s\n' \
"$house" "${n_bookings[$house]}" "${n_nights[$house]}" "$(sum "${amounts[$house]}")"
done
LC_ALL=C printf '%-14s %9d %8d %12s\n' TOTAL "$total_bookings" "$total_nights" "$(sum "$all_amounts")"
exit 0operator@srv-tramontana:~$ ~/scripts/bookings_summary.sh
HOUSE BOOKINGS NIGHTS AMOUNT
cal-ferrer 4 11 1120.00
can-ventos 6 17 1980.00
el-moli 3 8 867.00
la-solana 5 13 1425.50
mas-figueres 7 21 2450.00
TOTAL 25 70 7842.50It matches the known data. The sum function is a preview of 04-05, but it illustrates why functions are inevitable: the same awk line is used in two places.
Solution 3. The three flaws:
- Spaces around the
=.posts = 114tries to run the commandpostsand the script dies on the first line. - The order of operations truncates the result.
$posts/$total*100first calculates114/412, which in integer arithmetic is 0, and then0*100= 0. Always multiply before dividing. - The
$signs are unnecessary inside$(( ))— noise, not an error — and above all a percentage calls for a decimal that Bash cannot give.
posts=114
total=412
# Multiplying before dividing gives the correct integer: 27
pct_int=$(( posts * 100 / total ))
# And with one decimal, going out to awk with a fixed locale:
pct=$(LC_ALL=C awk -v p="$posts" -v t="$total" 'BEGIN { printf "%.1f", p*100/t }')
LC_ALL=C printf 'POST: %s of %s (%s %% integer, %s %% exact)\n' \
"$posts" "$total" "$pct_int" "$pct"
# -> POST: 114 of 412 (27 % integer, 27.7 % exact)Notice the %% in the format: inside printf, a literal % is written doubled.
Conclusion
You no longer depend on values nailed into the code, and along the way you have picked up the part of Bash that gives the most productivity per line written.
- You know that in Bash everything is a string and that the "type" is supplied by the context: nothing is validated, comparing numbers is not comparing text, and there are no decimals. You assign with no spaces around the
=, you always quote the value, you distinguish the shell, environment and subshell scopes knowing that the child's changes never come back to the parent, and you usedeclarewith its-r,-i,-a,-A,-xand-pattributes. - You have mastered the whole of parameter expansion:
:-,:=,:?and:+, lengths, substrings, trims with#,##,%,%%, replacements with/and//, and case changes. You no longer needbasenameorcutfor the simple things. - You do integer arithmetic with
$(( ))and(( )), you know the octal trap and the(( i++ ))one, and you go out tobcorawkwithLC_ALL=Cas soon as a decimal appears: Tramontana's average amount is 313.70 €. - You handle indexed arrays — always
"${array[@]}", never${array[*]}— you count by key with associative arrays after the indispensabledeclare -A, you format withprintf(which already produces the table for Marta's report), andhealth_check.shreads its configuration from the environment with default values.
But the script is still mute: you cannot ask it for help, you cannot tell it "use threshold 60" without exporting a variable, and there is no way for it to read the month for a report. In the next lesson, Script Input, Output and Arguments, we give it a real interface: positional parameters and the critical difference between $@ and $*, a help function, getopts for short options, the -h, -v, --dry-run and -- conventions that make a script look professional, read in all its variants and the canonical pattern for reading a file line by line, the golden rule of stdout for results and stderr for humans, and exit codes with meaning. By the end of it, health_check.sh will accept options and bookings_report.sh will be born.
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
