Everything you have built so far happens on a single server. But in production there are three — srv-veloz-01, srv-veloz-02 and srv-veloz-03 — and checking the fleet's status by logging into each one by hand is not automation: it is the same as before with more terminal windows. ssh is the tool that turns a local script into a command that runs on any machine, and with it the toolkit takes its last leap of the module. You will see SSH from a script's point of view — quite different from that of a person typing in a terminal — how to authenticate without anybody typing a password and without opening a hole, and how to walk a fleet in parallel without one downed server bringing the whole walk down.
Contents
sshfrom a script: what shell you get and what code it returns- Key authentication
- Limiting what an automated key can do
~/.ssh/configand connection reuse- The options a script needs
known_hostsmanaged properly- Remote blocks with a here-document
- Sending scripts and transferring files
- Walking the fleet, serially and in parallel
- Remote
sudoand when Bash stops being the answer - Application:
fleet.shis born
ssh from a script: what shell you get and what code it returns
ssh from a script: what shell you get and what code it returnsWith no command, ssh opens an interactive session. With a command (ssh veloz@srv-veloz-02 'uptime') something different and very important happens: a non-interactive, non-login shell is run, exactly the same situation as cron (07-01).
Interactive session ssh host |
ssh host command |
|
|---|---|---|
Does it read ~/.bash_profile or ~/.bashrc? |
Yes | No (except BASH_ENV, rare) |
Aliases and functions from your profile? / PATH |
Yes / your profile's | No / the system minimum |
| Is there a TTY? | Yes | No, unless -t |
Check it with ssh veloz@srv-veloz-02 'echo "$PATH"': it answers /usr/local/bin:/usr/bin:/bin, and not what you see when you log in by hand. The consequence is the same as with cron, and so is the solution: absolute paths always, and if you need configuration, load it explicitly. An ssh srv-veloz-02 'service-status.sh' will fail with "command not found" even though the script works perfectly when you log in by hand. And since there is no TTY, a remote command that asks something hangs or fails: property 1 from 07-02 applies, nothing interactive, now also at the other end of the wire. ssh returns the exit code of the remote command, which is magnificent: all your error logic from 05-03 travels over the network. But there is one critical exception: 255 is the code ssh uses for its own errors — unreachable host, rejected authentication, unknown key. Telling it apart is what separates "the server is down" from "the server's service is down", two very different incidents:
output=$(ssh -n -o BatchMode=yes veloz@srv-veloz-02 'service-status.sh' 2>&1) && code=0 || code=$?
case "$code" in
0) veloz_log_info "srv-veloz-02: OK" ;;
255) veloz_log_error "srv-veloz-02: UNREACHABLE (${output%%$'\n'*})" ;;
*) veloz_log_warn "srv-veloz-02: failed with code $code" ;;
esacThe ambiguity is theoretical but real: if your remote command returned 255 on its own, you could not tell it apart; that is why the conventional codes from 05-03 stay below 128.
- Key authentication
A script cannot type a password; public-key authentication solves that and is also more secure.
ssh-keygen -t ed25519 -C "veloz-ops on srv-veloz-01" -f ~/.ssh/id_veloz_ops
ssh-copy-id -i ~/.ssh/id_veloz_ops.pub veloz@srv-veloz-02-t ed25519 picks today's recommended algorithm: short, fast and solid keys (4096-bit RSA is fine if the destination is old). -f gives it its own name, so as not to mix the automation key with your personal one; it generates id_veloz_ops (private, it never leaves the machine) and id_veloz_ops.pub (public, the one you copy). ssh-copy-id adds it to the destination's ~/.ssh/authorized_keys and fixes the permissions, which matters because SSH is strict about them and fails silently if it does not like them: ~/.ssh at 700, authorized_keys and the private key at 600, and ~ not writable by group or others — otherwise anybody could replace .ssh. The passphrase is the uncomfortable decision: a protected key is more secure, but somebody has to type it. ssh-agent keeps the decrypted key in memory for your session (eval "$(ssh-agent)" and ssh-add ~/.ssh/id_veloz_ops), which is perfect for interactive work and useless for a cron job at 03:00, when there is no session and no agent. Automated jobs use keys with no passphrase, and you have to be honest about the risk: whoever gets that file has access to every destination where it is authorized. Since the risk cannot be avoided, it is bounded: mode 600 and the right owner, a separate key per purpose so it can be revoked without affecting anything else, and restrictions on the destination.
- Limiting what an automated key can do
An authorized_keys entry accepts options in front of the key that limit what can be done with it. It is the difference between a key that grants total access to the server and one that only serves the purpose you stated:
from="10.0.0.11",command="/home/veloz/veloz-ops/bin/remote-dispatcher.sh",no-port-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAAC3Nz... veloz-ops
from= prevents using the key from another address; no-port-forwarding prevents using the server as a tunnel into the internal network; no-agent-forwarding prevents taking advantage of your agent to hop to other machines; no-pty denies the interactive terminal; and command= forces it to always run that command, whatever the client asks for. That last one is the most powerful and the most often forgotten: even if somebody steals the key, all they can do is launch that script. The command the client asked for is left in $SSH_ORIGINAL_COMMAND, which allows a small dispatcher that only accepts what was planned:
# remote-dispatcher.sh — the only command allowed by the operations key
case "${SSH_ORIGINAL_COMMAND:-}" in
status) exec /home/veloz/veloz-ops/bin/service-status.sh ;;
backup) exec /home/veloz/veloz-ops/bin/backup.sh ;;
*) printf 'command not allowed\n' >&2; exit 64 ;;
esacThe case with a closed list (04-05) is the right approach: enumerate what is allowed instead of filtering what is dangerous. And never pass $SSH_ORIGINAL_COMMAND to eval or to a shell; that would be handing over the arbitrary execution you just avoided (08-03).
~/.ssh/config and connection reuse
~/.ssh/config and connection reuseRepeating user, port and key on every call is noise; ~/.ssh/config (mode 600) centralizes it.
Host srv-veloz-*
User veloz
IdentityFile ~/.ssh/id_veloz_ops
IdentitiesOnly yes
ConnectTimeout 5
ServerAliveInterval 15
ControlMaster auto
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 60
Host srv-veloz-03
HostName 10.0.2.33
ProxyJump bastion.veloz.exampleHost srv-veloz-* applies to everything matching the pattern, and more specific blocks add to it. IdentitiesOnly yes stops SSH from offering all your keys before the right one, which can trigger "too many authentication failures"; ServerAliveInterval detects a dead connection instead of leaving it hanging; and ProxyJump hops through an intermediate machine without hand-built tunnels. ControlMaster is the gem for loops. With auto, the first connection to a host opens a control socket at ControlPath and the following ones reuse that connection instead of negotiating again; ControlPersist 60 keeps the socket for a minute after the last one. Establishing an SSH session costs between 200 and 500 ms, so a loop with ten commands over three servers goes from several seconds to almost nothing:
running time ssh srv-veloz-02 true twice in a row gives 0m0.412s the first time and 0m0.038s the second, ten times faster because it did not authenticate again. One warning: if the socket is left orphaned, ssh -O exit srv-veloz-02 closes it.
- The options a script needs
| Option | What it does | Why it is indispensable |
|---|---|---|
-n |
Standard input from /dev/null |
See below: it is the classic mistake |
-o BatchMode=yes |
Never asks; fails instead | Without it, it hangs asking for a password |
-o ConnectTimeout=5 |
Maximum time to connect | A downed host must not hang the job |
-o StrictHostKeyChecking=accept-new |
Accepts new hosts, rejects changes | Section 6 |
The -n problem deserves its own paragraph, because it baffles everybody the first time. In while read -r host; do ssh "$host" 'uptime'; done < servers.txt only the first server is processed, and the reason is that ssh reads standard input in order to send it to the remote command. Inside the loop, standard input is servers.txt, so the first ssh swallows the rest of the file and the second iteration's read finds nothing: the loop ends after a single server, with no error and no explanation. The solution is ssh -n, an idiom to be memorized just like while IFS= read -r from 04-01. The alternative is to use descriptor 3 (done < servers.txt 3<&0 and read -u 3), but -n is simpler. And careful: you only add -n when you do not want to feed the remote command; if you are piping data into it, it must not be there.
known_hosts managed properly
known_hosts managed properlyThe first time you connect to a machine, SSH asks whether you accept its fingerprint. That question hangs a script, and the temptation is -o StrictHostKeyChecking=no, which is a bad idea: it disables the check that stops an attacker from impersonating your server. Being common does not make it correct. The two correct ways:
# a) Accept new hosts, but keep rejecting key changes
ssh -o StrictHostKeyChecking=accept-new -n veloz@srv-veloz-02 'uptime'
# b) Register the keys in advance, during provisioning
ssh-keyscan -t ed25519 srv-veloz-01 srv-veloz-02 srv-veloz-03 >> ~/.ssh/known_hostsaccept-new is the reasonable middle ground: it does not ask the first time, but if a host's key changes — a sign of an attack or a reinstall — it fails loudly, which is exactly what you want. Option (b) is stricter and the right one for a serious environment: the keys are collected once, verified and distributed with the rest of the configuration. When you reinstall a server and the key legitimately changes, ssh-keygen -R srv-veloz-02 removes the old entry.
- Remote blocks with a here-document
For several remote commands, packing them into a string with ; becomes unreadable; the clean way uses a here-document (05-05) feeding a remote shell.
ssh -o BatchMode=yes veloz@srv-veloz-02 bash -s <<'EOF'
set -euo pipefail
cd /srv/veloz/data
printf 'srv-veloz-02: %s shipments, %s free\n' "$(wc -l < shipments.csv)" "$(df -h --output=avail /srv|tail -1)"
EOFbash -s tells the remote Bash to read the script from its standard input, and the here-document provides it. Notice that -n is not used here: standard input is precisely the channel the script travels on. And the single quotes on the delimiter are the critical part of this idiom, because they decide where the variables are expanded. With <<'EOF' the text travels literally and $var is expanded at the destination; with an unquoted <<EOF, the local Bash expands before sending. Compare them:
report_date=2026-08-03
ssh srv-veloz-02 bash -s <<'EOF'
echo "remote: $(hostname), date: $report_date" # -> remote: srv-veloz-02, date:
EOF
ssh srv-veloz-02 bash -s <<EOF
echo "remote: \$(hostname), date: $report_date" # -> remote: srv-veloz-02, date: 2026-08-03
EOFIn the first case, $(hostname) runs at the destination (correct) and $report_date comes out empty because that variable does not exist there. In the second, $report_date is expanded by the local shell before sending and \$(hostname) is escaped so it runs at the destination. Mixing the two worlds produces baffling errors, so the recommendation is to always use <<'EOF' and pass the local values as arguments, which is safer and more readable:
ssh srv-veloz-02 bash -s -- "$report_date" "$threshold" <<'EOF'
set -euo pipefail
report_date="$1"; threshold="$2"
awk -F, -v f="$report_date" -v u="$threshold" '$2 ~ f && $5 == "issue" { c++ }
END { print (c + 0 >= u ? "ALERT" : "OK"), c + 0 }' /srv/veloz/data/shipments.csv
EOFThe arguments after -- reach the remote script as $1, $2… That way no local expansion sneaks in, and a value with spaces or quotes cannot alter the meaning of the remote code: the same defense against injection you applied with awk -v (06-01) and jq --arg (06-05).
- Sending scripts and transferring files
When the remote work goes past twenty lines it stops making sense to embed it.
# a) Send over standard input, leaving no trace at the destination
ssh -o BatchMode=yes veloz@srv-veloz-02 'bash -s' -- --dry-run < ~/veloz-ops/bin/check-node.sh
# b) Copy, run and clean up, preserving the exit code
scp -q ~/veloz-ops/bin/check-node.sh veloz@srv-veloz-02:/tmp/ && ssh -n veloz@srv-veloz-02 \
'bash /tmp/check-node.sh; rc=$?; rm -f /tmp/check-node.sh; exit $rc'Option (a) is elegant and the one to prefer by default: it does not touch the remote disk, so there is nothing to clean up and no forgotten old version left behind. Option (b) is needed when the script must persist; notice that it saves the exit code before deleting, so as not to lose it in the rm. For files, scp is fine for a single one, but for directories rsync is superior in every way — it transfers only what changed, preserves permissions, allows excluding, resumes and accepts --link-dest:
rsync -az --delete -e 'ssh -o BatchMode=yes -o ConnectTimeout=5' /backups/daily/ srv-veloz-02:/backups/n01/-e sets the transport with all the connection's options, and -z does make sense here because it goes over the network (remember from 07-03 that locally it added nothing). This completes the 3-2-1 rule: the copy that leaves the server. And to do it properly, the remote backup key is restricted at the destination with command="rrsync -wo /backups", a wrapper that only lets rsync write under that path: section 3 applied to the most frequent case.
- Walking the fleet, serially and in parallel
The central requirement: a downed server must not abort the walk. With set -e active, a failed ssh would kill the script at the first unreachable host and you would lose the information from the other two. The command && code=0 || code=$? idiom is what neutralizes set -e for that command and captures the code at the same time (05-03), and from there a case like the one in section 1 tells 255 apart from the rest. Three servers serially, at two seconds each, is six seconds; with thirty, a minute. In parallel, the time is that of the slowest. Picking up 05-02, with one important caveat: if several processes write to the same output at the same time, the lines interleave, so each branch writes to its own file and everything is consolidated at the end. Four decisions make the parallel walk in section 11 work. Each background branch writes its code and its output to separate files inside a mktemp -d, because a subshell cannot return values to the parent through variables. The wait || true waits for all of them and prevents one branch's non-zero code from aborting the script. The consolidation walks the server list in its original order, so the report comes out deterministic even if the nodes answer in any order — a report whose order changes every day cannot be compared. And a trap … EXIT cleans up the temporary directory. For large fleets it is worth limiting concurrency: xargs -P 10 (05-01) is the simplest way not to exceed ten simultaneous connections.
- Remote
sudo and when Bash stops being the answer
sudo and when Bash stops being the answerIf the remote command needs privileges, the problem is that sudo will want a password and there is no terminal to type it in. The right way is to authorize one specific command without a password at the destination, with a line veloz ALL=(root) NOPASSWD: /usr/bin/systemctl restart veloz-api in /etc/sudoers.d/veloz-ops. With that, ssh -n srv-veloz-02 'sudo /usr/bin/systemctl restart veloz-api' works without asking. Absolute path, no wildcards and for a specific user: a NOPASSWD: ALL is equivalent to handing the root password to whoever steals the key. The alternative sudo -S, which reads the password from standard input, forces you to send it through a pipe, which is exactly what we were trying to avoid. And here requiretty shows up: some sudoers configurations demand a terminal, which makes every non-interactive remote sudo fail with "sorry, you must have a tty to run sudo"; it is fixed with Defaults:veloz !requiretty at the destination. Forcing the TTY with ssh -t is the dirty solution, because it also mixes stdout and stderr and scrambles the output. You have to know how to recognize the limit. With three servers and simple checks, fleet.sh is perfect: there is nothing to install, it is transparent and anybody understands it. But if you start needing configuration templates, package management, enforcement that the desired state holds, dynamic inventories or execution across fifty machines, you are badly rewriting what Ansible already does well — and over SSH, agentless, with the same mental model; pssh/parallel-ssh is the intermediate step for launching the same command on many hosts. The alarm signal is clear: when your Bash script starts carrying a list of packages to install and configuration file templates inside it, it has stopped being an operations script.
- Application:
fleet.sh is born
fleet.sh is bornThe toolkit's last script runs service-status.sh on the three servers in parallel and consolidates the result.
#!/usr/bin/env bash
# fleet.sh — Consolidated fleet status. WHEN: every 15 min (timer).
# Codes: 0 whole fleet OK | 1 some node with a problem | 2 some node unreachable
set -euo pipefail
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; export LC_ALL=C
readonly BASE="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
. "$BASE/lib/common.sh"; [[ -r $BASE/etc/veloz-ops.conf ]] && . "$BASE/etc/veloz-ops.conf"
VELOZ_COMPONENT=fleet
readonly -a FLEET=(${VELOZ_FLEET:-srv-veloz-01 srv-veloz-02 srv-veloz-03})
readonly REMOTE=/home/veloz/veloz-ops/bin/service-status.sh
readonly -a SSH_OPTS=(-n -o BatchMode=yes -o ConnectTimeout=5 -o ControlMaster=auto
-o StrictHostKeyChecking=accept-new -o ControlPersist=30
-o ControlPath="$HOME/.ssh/cm-%r@%h:%p")
status_text() { case "$1" in 0) printf OK ;; 255) printf UNREACHABLE ;; *) printf "FAIL($1)" ;; esac; }
main() {
veloz_require ssh; local tmp host code worst=0
tmp=$(mktemp -d "${TMPDIR:-/tmp}/fleet.XXXXXX"); trap 'rm -rf "$tmp"' EXIT
for host in "${FLEET[@]}"; do
{ local out c; out=$(timeout 60s ssh "${SSH_OPTS[@]}" "$host" "$REMOTE" 2>&1) && c=0 || c=$?
printf '%s\n' "$c" > "$tmp/$host.code"; printf '%s\n' "$out" > "$tmp/$host.out"; } &
done
wait || true
printf '%-14s %-12s %s\n' NODE STATUS DETAIL
for host in "${FLEET[@]}"; do
code=$(< "$tmp/$host.code"); printf '%-14s %-12s %s\n' "$host" \
"$(status_text "$code")" "$(head -1 "$tmp/$host.out")"
case "$code" in
0) veloz_log_info "$host: OK" ;;
255) veloz_log_error "$host: unreachable"; worst=2 ;;
*) veloz_log_warn "$host: code $code"; (( worst < 1 )) && worst=1 ;;
esac
done
veloz_log_info "END, worst state: $worst"; return "$worst"
}
main "$@"NODE STATUS DETAIL srv-veloz-01 OK veloz-api healthy, 14 shipments queued srv-veloz-03 UNREACHABLE ssh: connect to host srv-veloz-03 port 22: timed out
Go over the pieces: the SSH options grouped in an array so they are not repeated and the quoting is not lost; ControlMaster to reuse the connection; timeout 60s in addition to ConnectTimeout, because the first covers a hung remote command and the second only the connection; parallel branches writing to separate files; consolidation in a fixed order; and codes that distinguish the three outcomes. Since it returns 0/1/2, fleet.sh fits as one more check in watchdog.sh (07-04) or as a .service with its timer (07-05).
Common Mistakes and Tips
- Forgetting
-nin a loop. Thessheats the input file and only the first host is processed, with no visible error at all. - Assuming the destination reads
.bashrc, or usingStrictHostKeyChecking=no. The first gives "command not found" despite working when you log in by hand (absolute paths); the second disables the only defense against impersonation (useaccept-neworssh-keyscan). - No
BatchMode=yesor noConnectTimeout. The first hangs the script waiting for a password nobody will type; the second lets a downed host block the walk for minutes. - Confusing 255 with an application failure. "The server does not respond" and "the server's service fails" are different incidents.
- An unquoted here-document delimiter, or several parallel branches writing to the same output. The first expands the variables in the wrong place; the second interleaves the lines (one file per branch and consolidate). And
NOPASSWD: ALLturns a stolen key into total access: authorize specific commands with an absolute path. - Tip: always test
ssh -n host 'command'by hand before putting it in a loop, and add-vif something does not add up:ssh -v's trace says exactly which key was offered, what the server rejected and which configuration file was applied.
Exercises
Exercise 1. This loop only processes the first server and, when one is down, it aborts. Find the four problems and rewrite it.
Exercise 2. Write veloz_remote for lib/common.sh: it runs a command on a remote host with all the automation options, distinguishes unreachable from an application failure with different codes, and limits the total duration.
Solutions
Solution 1. The four problems: (a) -n is missing, so the first ssh consumes the file and the loop ends; (b) set -e aborts as soon as one host fails, losing the rest; (c) with no BatchMode and no ConnectTimeout, a downed host hangs the walk or asks for a password; (d) without IFS= read -r and without filtering blank or commented lines, a normal inventory file breaks the loop.
while IFS= read -r host; do
[[ -z $host || $host == \#* ]] && continue
if output=$(ssh -n -o BatchMode=yes -o ConnectTimeout=5 "$host" 'uptime; df -h /' 2>&1); then
printf '=== %s ===\n%s\n' "$host" "$output"
else
printf '=== %s === ERROR (%s): %s\n' "$host" "$?" "${output%%$'\n'*}"
fi
done < ~/veloz-ops/etc/fleet.txtThe if around the ssh is what allows it to coexist with set -e (05-03): a failure inside an if condition does not abort the script. And the output is captured into a variable so it can be labeled with the host name, instead of letting three servers write interleaved.
Solution 2.
# veloz_remote <host> <command...>
# Codes: 0 ok | 69 host unreachable | 124 timed out | anything else = from the remote command
veloz_remote() {
local host="${1:?missing the host}" limit="${VELOZ_SSH_LIMIT:-60}" output code; shift
output=$(timeout "${limit}s" ssh -n -o BatchMode=yes -o ConnectTimeout=5 \
-o StrictHostKeyChecking=accept-new -o ControlMaster=auto \
-o ControlPath="$HOME/.ssh/cm-%r@%h:%p" "$host" "$@" 2>&1) && code=0 || code=$?
case "$code" in
0) printf '%s\n' "$output"; return 0 ;;
124) veloz_log_error "$host: exhausted the ${limit}s"; return 124 ;;
255) veloz_log_error "$host: unreachable (${output%%$'\n'*})"; return 69 ;;
*) veloz_log_warn "$host: the command failed with code $code"
printf '%s\n' "$output" >&2; return "$code" ;;
esac
}The 255 is translated into 69 (EX_UNAVAILABLE), the same code veloz_api_get used in 06-05 for "the service is not available": that way the caller treats a downed API and a downed server the same, which operationally they are. The other codes are propagated as they are, so the remote script's error logic reaches the local one intact. And the correct output goes to stdout while diagnostics go to stderr (02-04), so data=$(veloz_remote srv-veloz-02 cat /etc/os-release) captures only the useful part.
Conclusion
ssh host command gives you a non-interactive, profile-less shell: the same environment problem as cron, with the same solution — absolute paths and configuration loaded explicitly — and nothing interactive at the other end. It returns the remote command's code, except for 255, which are ssh's own errors and must always be told apart. Authentication is by key (ssh-keygen -t ed25519, ssh-copy-id, strict 700/600 permissions); in automation it goes without a passphrase, and since the risk cannot be eliminated it is bounded: one key per purpose and restrictions in authorized_keys with command=, from=, no-port-forwarding and no-pty. Centralize the repetitive parts in ~/.ssh/config with User, IdentityFile, IdentitiesOnly, ProxyJump and above all ControlMaster/ControlPersist, which reuses the connection and makes a loop ten times faster. The four indispensable options: -n — because ssh reads standard input and inside a while read it swallows the file, processing a single host without giving any error — -o BatchMode=yes so it fails instead of asking, -o ConnectTimeout so a downed host does not block the walk, and StrictHostKeyChecking=accept-new (or ssh-keyscan in advance) instead of the dangerous no. For remote blocks, ssh host bash -s <<'EOF' with quotes on the delimiter and the local values passed as arguments after --: that is what decides where the variables are expanded and what stops a piece of data from altering the code. Send scripts over standard input rather than copying them, use rsync -az -e ssh instead of scp for directories — which also completes the 3-2-1 rule from 07-03 — walk the fleet capturing each host's code so that a downed one does not abort the others, parallelize with & and wait writing to separate files and consolidating in a fixed order, authorize remote sudo per specific command — never NOPASSWD: ALL — and recognize the limit: when templates and inventories show up, Ansible does that job better.
With fleet.sh, Module 7 and the Veloz Envíos toolkit are complete. It no longer waits for your orders: daily-report.sh publishes its report every morning even if the server spent the night powered off, backup.sh copies, verifies, retains and replicates off the server, watchdog.sh checks eight things every five minutes and alerts only once per incident, and fleet.sh queries the three servers in parallel. And here the next problem appears, of a different nature: this is already several hundred lines spread across five scripts and a library, written over seven modules, that run privileged jobs, handle SSH keys and touch customer data. It works, but is it maintainable? Is it secure? How do you know a change does not break anything? Module 8 answers: readable code (08-01), optimizing what really matters (08-02), security (08-03), version control with Git (08-04), static analysis with ShellCheck and shfmt (08-05), automated testing with Bats (08-06) and portability versus POSIX (08-07). The toolkit already runs on its own; now it is time to turn it into software you can trust.
Bash Programming Course
Module 1: Introduction to Bash
- What Is Bash?
- Setting Up Your Environment
- Basic Command Line Navigation
- Understanding the Shell
- Finding Help: man, help and --help
Module 2: Basic Bash Commands
- File and Directory Operations
- Text Processing Commands
- File Permissions and Ownership
- Redirection and Piping
- Wildcards and Path Expansion
- History and Keyboard Shortcuts
Module 3: Scripting Fundamentals
- Creating and Running a Script
- Variables and Constants
- Basic Operators
- Conditional Statements
- Arguments and User Input
- Quoting, Expansion and Substitution
Module 4: Intermediate Scripting
- Loops in Bash
- Functions in Bash
- Arrays and Associative Arrays
- String Manipulation
- The case Statement and Interactive Menus
- Arithmetic and Numeric Calculations
Module 5: Advanced Scripting Techniques
- Advanced File Operations
- Process Management
- Error Handling and Debugging
- Regular Expressions
- Advanced I/O: Descriptors and Here-Documents
- Modular Scripts and Reusable Libraries
Module 6: Working with External Tools
Module 7: Automation and Scheduling
- Cron Jobs
- Automating Tasks
- Backup and Restore Scripts
- Monitoring and Logging
- Services and Timers with systemd
- Remote Automation with SSH
Module 8: Best Practices and Optimization
- Writing Readable Code
- Optimizing Bash Scripts
- Security Considerations
- Version Control with Git
- Static Analysis with ShellCheck and shfmt
- Automated Testing with Bats
- Portability: POSIX sh versus Bashisms
