<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged bash-scripting</title>
<link>https://coderlegion.com/tag/bash-scripting</link>
<description>Powered by Question2Answer</description>
<item>
<title>Your Cron Job Works. That's Not the Same as Surviving Cron.</title>
<link>https://coderlegion.com/22128/your-cron-job-works-thats-not-the-same-as-surviving-cron</link>
<description>&lt;p&gt;&lt;em&gt;Level: Intermediate · Time: 7 min · Outcome: A mental model and three guards for unattended cron jobs&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;There's a gap between a script that works when you run it and a script that survives running unattended for a year. The first only has to succeed once, with you watching, on a good day. The second has to keep working through slow disks, hung sockets, recovering services, and reboots — with nobody watching, because the entire point of cron is that nobody is watching.&lt;/p&gt;
&lt;p&gt;Almost every script I've seen take down a server was fine in a manual run and fell apart the first time conditions weren't ideal and there was no human there to notice. I've hit three versions of this personally: a sync that stacked copies until the box hit load 41, a backup that hung and didn't run for nine days without a peep, and a deploy that raced a database's boot and paged me at 11pm. Three commands, three failures, one underlying truth — &quot;works on my machine&quot; and &quot;survives cron&quot; are different engineering problems.&lt;/p&gt;
&lt;p&gt;Cron jobs die quietly in exactly three ways, and there's a fourth problem underneath all of them that keeps the first three invisible.&lt;/p&gt;
&lt;h3 id=&quot;the-invisible-problem-first-cron-eats-your-output&quot;&gt;The invisible problem first: cron eats your output&lt;/h3&gt;
&lt;p&gt;When cron runs a job, anything it prints to stdout or stderr goes nowhere unless you've configured otherwise. No log, no record, no trace. So a job can fail every night for a month and the only signal is the absence of whatever it was supposed to produce — and you notice that when something downstream breaks, not when the job fails.&lt;/p&gt;
&lt;p&gt;Two things turn cron from silent to legible. &lt;code&gt;MAILTO&lt;/code&gt; at the top of the crontab mails you any output a job produces. More reliably, redirect every job's output to a log file you control:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;*/5 * * * * /usr/local/bin/safe-sync.sh &amp;gt;&amp;gt; /var/log/sync.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;2&amp;gt;&amp;amp;1&lt;/code&gt; is the part people forget — it folds stderr into the same stream as stdout, so your errors land in the log instead of evaporating. Everything below assumes you've done this. A locked, time-bounded, retried job that still logs nothing is a job whose failures you'll discover by accident.&lt;/p&gt;
&lt;h3 id=&quot;overlap-lock-it-to-one-instance&quot;&gt;Overlap: lock it to one instance&lt;/h3&gt;
&lt;p&gt;This is the failure that turns a transient slow patch into a self-inflicted outage. A sync scheduled every minute normally finishes in twenty seconds; one slow-disk afternoon it starts taking ninety, and cron launches a new copy every minute on top of the last, until a half-dozen copies are fighting over the same disk and the load climbs into the dozens.&lt;/p&gt;
&lt;p&gt;The guard is &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-flock-single-instance&quot;&gt;&lt;code&gt;flock&lt;/code&gt;&lt;/a&gt;: a kernel-held lock on an open file descriptor that lets exactly one copy run and makes the rest skip. Unlike a PID file, there's nothing to clean up — the kernel releases the lock automatically when the process exits, crashes, or is killed, so you never inherit a stale lock from a run that died badly. But a lock has a sharp edge: if the locked job hangs, it holds the lock forever and every future run skips. The job stops running entirely, silently. Which is the next problem.&lt;/p&gt;
&lt;h3 id=&quot;hang-bound-the-runtime&quot;&gt;Hang: bound the runtime&lt;/h3&gt;
&lt;p&gt;A hung command is worse than a failed one, because a failed command at least exits and frees its lock. A backup wedged on a held database lock, a &lt;code&gt;curl&lt;/code&gt; against a dead socket, an &lt;code&gt;ssh&lt;/code&gt; into a black hole — these never return. Under cron, &quot;never returns&quot; means the slot is jammed, the lock is held, and the job produces zero signal because it never gets far enough to log anything.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-timeout-command&quot;&gt;&lt;code&gt;timeout&lt;/code&gt;&lt;/a&gt; is the outside bound the command can't set on itself:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# SIGTERM at 5 minutes; if it's ignored (process stuck in I/O), SIGKILL 20s later
timeout -k 20s 5m mysqldump --single-transaction mydb &amp;gt; /backup/mydb.sql.partial
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It exits &lt;code&gt;124&lt;/code&gt; when it had to step in and &lt;code&gt;137&lt;/code&gt; when the command ignored the polite SIGTERM and had to be force-killed — codes worth branching on, because &quot;slow&quot; and &quot;wedged&quot; are different problems. The &lt;code&gt;-k&lt;/code&gt; grace matters specifically for processes stuck in uninterruptible I/O, which can't act on SIGTERM at all. Timeout is what makes a lock safe: with both in place, a hang gets killed on a deadline, the lock always releases on time, and the failure becomes a loud &lt;code&gt;124&lt;/code&gt; instead of a silent gap.&lt;/p&gt;
&lt;h3 id=&quot;transient-failure-retry-with-backoff&quot;&gt;Transient failure: retry with backoff&lt;/h3&gt;
&lt;p&gt;Not every failure deserves to kill the run. A fresh database container that isn't accepting connections for six seconds, a 503 during a rolling restart, a 429 explicitly asking you to back off — these are transient. Treating the first one as fatal is how a normal six-second boot delay becomes a failed deploy at eleven at night.&lt;/p&gt;
&lt;p&gt;The guard is a bounded &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-retry-with-backoff&quot;&gt;retry with exponential backoff&lt;/a&gt;: try the command, and on failure wait a delay that doubles each round, with jitter so parallel callers don't retry in lockstep, capped so a genuinely dead dependency fails fast instead of looping forever.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Wait for the port to be ready before doing the work that depends on it
retry 6 nc -z -w 2 db.internal 5432
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The discipline that makes retries safe rather than dangerous: retry transient failures only. A 404 fails identically on every attempt, so retrying it just delays the real error and buries it under retry noise.&lt;/p&gt;
&lt;h3 id=&quot;composing-all-three-and-knowing-which-job-needs-which&quot;&gt;Composing all three — and knowing which job needs which&lt;/h3&gt;
&lt;p&gt;Locking, bounding, and retrying are independent guards, and the real value is stacking them: a single run that can't overlap, can't hang, and rides out a blip — and tells you, by mail or log, when it finally does give up. That composition is mechanical enough to generate, which is exactly what the &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/tools/cron-wrapper-generator&quot;&gt;Hardened Cron Wrapper Generator&lt;/a&gt; does: paste the command, toggle the guards, get a ShellCheck-clean wrapper and the crontab line.&lt;/p&gt;
&lt;p&gt;But not every job needs all of it — wrapping a one-line &lt;code&gt;date&lt;/code&gt; echo in flock and timeout is ceremony. Add a lock to anything that mutates shared state and can outrun its interval. Add a timeout to anything that touches the network or a database. Add retries to anything that depends on something which boots or recovers on its own clock. Log everything, always. The full decision table — guard by guard, when to add it and when to skip it — is in the guide.&lt;/p&gt;
&lt;p&gt;Read the whole thing, including the &quot;know when it broke&quot; alerting layer and the decision matrix: &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/guides/bash-scripts-that-survive-cron&quot;&gt;https://bashsnippets.xyz/guides/bash-scripts-that-survive-cron&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The three guards as standalone snippets: &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-flock-single-instance&quot;&gt;flock&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-timeout-command&quot;&gt;timeout&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-retry-with-backoff&quot;&gt;retry with backoff&lt;/a&gt;. To write a new hardened script from a blank slate rather than wrap an existing command, the &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/tools/bash-boilerplate-generator&quot;&gt;Bash Boilerplate Generator&lt;/a&gt; and the &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-error-handling&quot;&gt;error-handling snippet&lt;/a&gt; cover the skeleton. The rest of the library is at &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz&quot;&gt;https://bashsnippets.xyz&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/guides/bash-scripts-that-survive-cron&quot;&gt;bashsnippets.xyz&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/22128/your-cron-job-works-thats-not-the-same-as-surviving-cron</guid>
<pubDate>Wed, 08 Jul 2026 23:44:51 +0000</pubDate>
</item>
<item>
<title># My Backup Hadn't Run in 9 Days and Nothing Told Me</title>
<link>https://coderlegion.com/21384/my-backup-hadnt-run-in-9-days-and-nothing-told-me</link>
<description>&lt;h2 id=&quot;my-backup-hadn-t-run-in-9-days-and-nothing-told-me&quot;&gt;My Backup Hadn't Run in 9 Days and Nothing Told Me&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Level: Intermediate · Time: 5 min · Outcome: A cron command that can't hang forever&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The backup ran fine every night for fourteen months, and then it didn't run for nine days, and nothing told me. No error in the log. No failed-job alert. No bounced cron mail. The nightly &lt;code&gt;mysqldump&lt;/code&gt; had hung — the database had a long-held lock from a runaway analytics query, the dump opened its transaction and sat there waiting for it, forever.&lt;/p&gt;
&lt;p&gt;Cron launched it at 2am, it never exited, and here's the cruel part: because I'd been smart enough to wrap it in a lock so two dumps couldn't run at once, every subsequent night's run saw the lock still held by the zombie from the 9th and skipped quietly. The clever lock turned a one-night hang into a nine-day outage. I found it when I went to restore a table and discovered my newest &quot;backup&quot; was a &lt;code&gt;mysqldump&lt;/code&gt; process that had been running since the previous Tuesday.&lt;/p&gt;
&lt;p&gt;That's nine days I'd have lost if anything had gone wrong with the live database. The ten minutes of feeling foolish when I traced it back was nothing next to that.&lt;/p&gt;
&lt;h3 id=&quot;a-hung-command-is-worse-than-a-failed-one&quot;&gt;A hung command is worse than a failed one&lt;/h3&gt;
&lt;p&gt;This is the lesson worth internalizing, because it's counterintuitive. A failed command exits, frees its lock, and the next run tries again — the system self-heals. A hung command exits never. It holds resources, blocks its own future runs, and produces exactly zero signal because it never gets far enough to log anything. Failures are loud. Hangs are silent, and silence is what kills you in unattended automation.&lt;/p&gt;
&lt;p&gt;You can't rely on a command to bound its own runtime, either. The whole problem is that it's wedged somewhere it can't time itself out of — blocked in the kernel waiting on a lock, or on a dead socket that will never send the FIN it's waiting for. So you bound it from the outside.&lt;/p&gt;
&lt;h3 id=&quot;bounding-the-runtime-with-timeout&quot;&gt;Bounding the runtime with timeout&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash
# Script: bounded-dump.sh
# Purpose: Stop a hung command from running forever and jamming the cron slot
set -euo pipefail

CHECK=&quot;✓&quot;
CROSS=&quot;✗&quot;

MAX_RUNTIME=&quot;5m&quot;   # longer than the normal worst case, well under the interval
KILL_GRACE=&quot;20s&quot;   # after SIGTERM, wait this long, then SIGKILL
DEST=&quot;/backup/mydb.sql&quot;

# In an `if` so set -e doesn't abort before we read the exit code.
# Write to a .partial file so a timed-out run never leaves a corrupt &quot;backup&quot;.
if timeout -k &quot;$KILL_GRACE&quot; &quot;$MAX_RUNTIME&quot; \
        mysqldump --single-transaction mydb &amp;gt; &quot;$DEST.partial&quot;; then
    mv &quot;$DEST.partial&quot; &quot;$DEST&quot;
    echo &quot;$CHECK dump completed within $MAX_RUNTIME&quot;
else
    code=$?
    rm -f &quot;$DEST.partial&quot;
    case &quot;$code&quot; in
        124) echo &quot;$CROSS dump exceeded $MAX_RUNTIME — terminated (SIGTERM)&quot; &amp;gt;&amp;amp;2 ;;
        137) echo &quot;$CROSS dump ignored SIGTERM — force-killed (SIGKILL)&quot; &amp;gt;&amp;amp;2 ;;
        *)   echo &quot;$CROSS dump failed with exit code $code&quot; &amp;gt;&amp;amp;2 ;;
    esac
    exit &quot;$code&quot;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;timeout -k &quot;$KILL_GRACE&quot; &quot;$MAX_RUNTIME&quot; mysqldump ...&lt;/code&gt; is the entire mechanism. At five minutes, &lt;code&gt;timeout&lt;/code&gt; sends the dump a SIGTERM. A well-behaved program treats SIGTERM as &quot;wrap up and exit.&quot; But the dump from my outage wasn't misbehaving — it was blocked in the kernel waiting on a lock, and a process in that state physically cannot act on SIGTERM. That's what &lt;code&gt;-k 20s&lt;/code&gt; is for: twenty seconds after the polite signal, &lt;code&gt;timeout&lt;/code&gt; sends SIGKILL, which the kernel enforces unconditionally. Nothing survives SIGKILL.&lt;/p&gt;
&lt;h3 id=&quot;read-the-exit-code-it-s-the-difference-between-a-log-and-a-mystery&quot;&gt;Read the exit code — it's the difference between a log and a mystery&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;124  # still running at the deadline — SIGTERM was sent
137  # 128 + 9 — ignored SIGTERM, had to be force-killed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Collapsing every outcome into &quot;backup failed&quot; throws away the one piece of information that tells you whether you have a slow database, a wedged one, or a broken dump command. A &lt;code&gt;124&lt;/code&gt; says &quot;this is taking too long — investigate the query.&quot; A &lt;code&gt;137&lt;/code&gt; says &quot;this is wedged in I/O — investigate the lock or the mount.&quot; They point at different problems. (If you ever blank on which code is which, the &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/tools/bash-exit-code-lookup&quot;&gt;Bash Exit Code Lookup&lt;/a&gt; decodes 124 and 137 directly.)&lt;/p&gt;
&lt;h3 id=&quot;the-partial-trick-matters-more-than-it-looks&quot;&gt;The .partial trick matters more than it looks&lt;/h3&gt;
&lt;p&gt;If you redirect straight to the real backup file and the command times out mid-write, you've just replaced last night's good backup with a half-written, unrestorable file — and you won't know until the night you need it. Writing to a &lt;code&gt;.partial&lt;/code&gt; path and &lt;code&gt;mv&lt;/code&gt;-ing into place only on a clean exit means a failed or timed-out run leaves the previous good backup untouched. A &lt;code&gt;mv&lt;/code&gt; on the same filesystem is atomic; the redirect is not.&lt;/p&gt;
&lt;p&gt;For commands that talk to the network, layer the tool's own timeout underneath — &lt;code&gt;curl --max-time&lt;/code&gt;, &lt;code&gt;ssh -o ConnectTimeout&lt;/code&gt;, a &lt;code&gt;net_read_timeout&lt;/code&gt; on the dump. Those fire first and fail cleanly. &lt;code&gt;timeout&lt;/code&gt; is the outer hard stop for the night the inner one doesn't.&lt;/p&gt;
&lt;h3 id=&quot;back-to-the-nine-days&quot;&gt;Back to the nine days&lt;/h3&gt;
&lt;p&gt;A timeout is what makes a lock safe. Locking a job to a single instance stops overlap, but a hang inside the locked job holds that lock forever — which is precisely how my nine-day gap happened. Bound the runtime and the lock always gets released, on a deadline, every time, and the failure becomes a loud &lt;code&gt;124&lt;/code&gt; in the log instead of a silent gap you discover during a restore.&lt;/p&gt;
&lt;p&gt;Full script with the exit-code branching and the FAQ on timing out a whole pipeline: &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-timeout-command&quot;&gt;https://bashsnippets.xyz/snippets/bash-timeout-command&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The lock that made my hang invisible is &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-flock-single-instance&quot;&gt;flock&lt;/a&gt;, and the third guard is &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-retry-with-backoff&quot;&gt;retry with backoff&lt;/a&gt;. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/tools/cron-wrapper-generator&quot;&gt;Hardened Cron Wrapper Generator&lt;/a&gt; composes all three, &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/guides/bash-scripts-that-survive-cron&quot;&gt;Bash Scripts That Survive Cron&lt;/a&gt; walks the whole decision, and the rest of the library is at &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz&quot;&gt;https://bashsnippets.xyz&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-timeout-command&quot;&gt;bashsnippets.xyz&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21384/my-backup-hadnt-run-in-9-days-and-nothing-told-me</guid>
<pubDate>Fri, 26 Jun 2026 03:41:25 +0000</pubDate>
</item>
<item>
<title># A Cron Job Took Our Server to Load 41 by Attacking Itself</title>
<link>https://coderlegion.com/21166/a-cron-job-took-our-server-to-load-41-by-attacking-itself</link>
<description>&lt;p&gt;A &lt;code&gt;*/1&lt;/code&gt; rsync took our staging box to a load average of 41 one afternoon, and it took me longer than I want to admit to work out why. The sync normally finished in about twenty seconds. That day the backup target's NFS mount went sluggish, the sync started taking ninety seconds, and cron — which does not know or care whether the last run is still going — launched a fresh copy every single minute on top of it.&lt;/p&gt;
&lt;p&gt;Inside ten minutes there were a half-dozen rsyncs all reading the same tree off the same slow disk, each one making the disk slower, each new minute adding another. The box wasn't under attack. It was attacking itself, one polite copy at a time. The thing that stung was that nothing was broken — every individual rsync was correct, the disk eventually recovered on its own, and the only reason it became an outage is that cron has no concept of &quot;the last one is still running.&quot;&lt;/p&gt;
&lt;p&gt;That's the trap with scheduled jobs: a command that's perfectly fine when you run it by hand can take down a server the first time it runs longer than its interval with nobody watching.&lt;/p&gt;
&lt;h3 id=&quot;the-fix-everyone-reaches-for-first-is-the-wrong-one&quot;&gt;The fix everyone reaches for first is the wrong one&lt;/h3&gt;
&lt;p&gt;The instinct is a PID file: write &lt;code&gt;$$&lt;/code&gt; to &lt;code&gt;/var/run/job.pid&lt;/code&gt; on start, check whether that file exists on the next run, bail if it does. It almost works. Then one run gets &lt;code&gt;kill -9&lt;/code&gt;'d, or the box reboots mid-job, and the PID file is left behind pointing at a process that died on Tuesday. Now every future run sees a &quot;lock&quot; owned by a PID that no longer exists, and the job never runs again — the opposite failure, just as silent.&lt;/p&gt;
&lt;p&gt;There's also a race between the check and the write, and the times you most need the lock to be clean are exactly the times cleanup didn't happen, because the process died before it could clean up.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;flock&lt;/code&gt; has none of that. The lock isn't a file you create and delete — it's a lock the kernel holds on an open file descriptor, and the kernel releases it automatically the instant that descriptor closes. The process exiting closes it. So does crashing. So does &lt;code&gt;kill -9&lt;/code&gt;. There is no state to leave behind, which is the entire reason it survives the failure modes a PID file can't.&lt;/p&gt;
&lt;h3 id=&quot;the-single-instance-pattern&quot;&gt;The single-instance pattern&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash
# Script: backup-with-lock.sh
# Purpose: Stop a cron job from overlapping itself when one run runs long
set -euo pipefail

CHECK=&quot;✓&quot;
CROSS=&quot;✗&quot;

# /run/lock is tmpfs, cleared cleanly on reboot. Never /tmp — temp-cleaners
# delete files there, and a deleted lock mid-run lets a second copy run.
LOCK_FILE=&quot;/run/lock/$(basename &quot;$0&quot;).lock&quot;

# The &amp;gt; opens (and creates) the lock file on fd 200 and holds it open for the
# whole script. The lock lives on this descriptor, not on the file existing.
exec 200&amp;gt;&quot;$LOCK_FILE&quot;

# -n = non-blocking: if a previous run still holds the lock, give up now
# instead of queueing another copy behind it.
if ! flock -n 200; then
    echo &quot;$CROSS $(date '+%F %T') previous run still active — skipping&quot; &amp;gt;&amp;amp;2
    exit 0
fi

echo &quot;$CHECK $(date '+%F %T') lock acquired — starting&quot;
rsync -a --delete /data/ /mnt/backup/data/
echo &quot;$CHECK $(date '+%F %T') finished — kernel releases the lock on exit&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The two lines doing the work are &lt;code&gt;exec 200&amp;gt;&quot;$LOCK_FILE&quot;&lt;/code&gt; and &lt;code&gt;flock -n 200&lt;/code&gt;. The first opens the lock file on a descriptor that stays open for the life of the process. The second tries to grab the lock without waiting; if a sibling process already holds it, &lt;code&gt;flock&lt;/code&gt; returns non-zero, we log it and exit &lt;code&gt;0&lt;/code&gt; — a skipped run is normal, not an error, so we don't want it lighting up cron's mail.&lt;/p&gt;
&lt;p&gt;Notice there is no cleanup. No &lt;code&gt;trap&lt;/code&gt; to remove a PID file, no &lt;code&gt;rm&lt;/code&gt; at the end. When this script exits for any reason, fd 200 closes and the lock is gone. That &quot;for any reason&quot; is the whole point.&lt;/p&gt;
&lt;h3 id=&quot;you-can-lock-a-job-without-editing-it-at-all&quot;&gt;You can lock a job without editing it at all&lt;/h3&gt;
&lt;p&gt;If the misbehaving job is already deployed and you don't want to touch it, wrap it from the crontab line:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Skip the run if the last one is still going
*/1 * * * * /usr/bin/flock -n /run/lock/sync.lock /usr/local/bin/sync.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;flock&lt;/code&gt; runs &lt;code&gt;sync.sh&lt;/code&gt; only if it can grab the lock; if last minute's run is still holding it, this minute's run exits immediately and does nothing. It's the fastest retrofit for a job that's already on fire — no redeploy.&lt;/p&gt;
&lt;p&gt;One thing worth burning into memory: &lt;code&gt;-n&lt;/code&gt; skips, &lt;code&gt;-w 30&lt;/code&gt; waits up to thirty seconds then gives up, and a bare &lt;code&gt;flock&lt;/code&gt; with neither blocks forever. On a fast cron schedule that bare form turns your &quot;skipped&quot; runs into a pile of stuck processes — the exact thing you were trying to prevent.&lt;/p&gt;
&lt;h3 id=&quot;the-part-that-actually-mattered&quot;&gt;The part that actually mattered&lt;/h3&gt;
&lt;p&gt;The load-41 afternoon ended the moment I wrapped that rsync in &lt;code&gt;flock -n&lt;/code&gt;. The slow NFS mount was still slow, but now exactly one sync ran at a time and the extras skipped harmlessly until the disk recovered. Locking didn't fix the slow disk — it stopped a transient slow disk from becoming a self-inflicted outage. That's the difference between a script that works when you run it and one that survives unattended.&lt;/p&gt;
&lt;p&gt;A lock alone isn't the whole story, though. If the locked job itself hangs, it holds the lock forever and every future run skips — so the job silently stops running and you find out days later. That's why locking pairs with bounding runtime with &lt;code&gt;timeout&lt;/code&gt; and retrying transient failures.&lt;/p&gt;
&lt;p&gt;Full script, the &lt;code&gt;-n&lt;/code&gt; vs &lt;code&gt;-w&lt;/code&gt; decision, and the FAQ on where the lock file should live: &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-flock-single-instance&quot;&gt;https://bashsnippets.xyz/snippets/bash-flock-single-instance&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you're hardening a cron job, the next two guards are &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-timeout-command&quot;&gt;timeout&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-retry-with-backoff&quot;&gt;retry with backoff&lt;/a&gt;; the &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/tools/cron-wrapper-generator&quot;&gt;Hardened Cron Wrapper Generator&lt;/a&gt; stitches all three into one wrapper, and the full reasoning is in &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/guides/bash-scripts-that-survive-cron&quot;&gt;Bash Scripts That Survive Cron&lt;/a&gt;. The rest of the library is at &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz&quot;&gt;https://bashsnippets.xyz&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-flock-single-instance&quot;&gt;bashsnippets.xyz&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/21166/a-cron-job-took-our-server-to-load-41-by-attacking-itself</guid>
<pubDate>Tue, 23 Jun 2026 00:16:48 +0000</pubDate>
</item>
<item>
<title>A Function Without local Overwrote My Variable and rm -rf Deleted the Wrong Directory</title>
<link>https://coderlegion.com/21095/a-function-without-local-overwrote-my-variable-and-rm-rf-deleted-the-wrong-directory</link>
<description>&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;Bash variables are global by default, so a helper function clobbered the caller's path variable and rm -rf cleaned up the wrong directory. local, return-as-status, and getopts — all in one incident.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; #bash  #linux  #scripting  #devops&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;The deploy script had been running in production for four months without a problem. It built releases into a temp directory, ran some validation, and then cleaned up by removing whatever &lt;code&gt;$target&lt;/code&gt; pointed at. &lt;code&gt;$target&lt;/code&gt; was set near the top of the script to the current release directory — the one the running application was serving from. A helper function called &lt;code&gt;prepare()&lt;/code&gt; also used a variable named &lt;code&gt;target&lt;/code&gt;, because the person who wrote it (me, four months earlier) did not think about scope.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;prepare()&lt;/code&gt; built the new release into a temp directory. On a good run, it set &lt;code&gt;target&lt;/code&gt; to the temp path, did its work, and returned. The main script then did its cleanup at the end and removed &lt;code&gt;$target&lt;/code&gt; — which, after calling &lt;code&gt;prepare()&lt;/code&gt;, was the temp directory. That worked correctly for four months.&lt;/p&gt;
&lt;p&gt;Then a deploy failed partway through &lt;code&gt;prepare()&lt;/code&gt;. The temp directory was half-built. &lt;code&gt;target&lt;/code&gt; was now pointing at the half-built temp path. The main script caught the failure, started its cleanup, and ran &lt;code&gt;rm -rf &quot;$target&quot;&lt;/code&gt;. It removed the half-built temp directory, which was correct. But then it kept going — there was a second cleanup step that also used &lt;code&gt;$target&lt;/code&gt; and expected it to still be the release directory. By the time the script finished, it had removed the running application's release directory. The application restarted and found nothing to serve.&lt;/p&gt;
&lt;p&gt;The users noticed before I did. The restart loop was filling logs, the application was returning 502, and I was sitting in the deploy output trying to figure out what had gone wrong in a failure path I had tested against a stub.&lt;/p&gt;
&lt;h3 id=&quot;variables-in-bash-are-global-by-default&quot;&gt;Variables in bash are global by default&lt;/h3&gt;
&lt;p&gt;This is the single most surprising thing about bash functions if you have written code in almost any other language. In Python, a variable assigned inside a function is local to that function unless you explicitly declare it &lt;code&gt;global&lt;/code&gt;. In bash, it is the opposite. A variable assigned inside a function is visible — and writable — everywhere in the current shell unless you declare it &lt;code&gt;local&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;target=&quot;/srv/release/current&quot;

prepare() {
  target=$(mktemp -d)    # No local — this overwrites the global $target
  echo &quot;building in $target&quot;
}

prepare
echo &quot;target is now: $target&quot;   # Prints the temp dir, not /srv/release/current
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The function does exactly what it looks like — it sets &lt;code&gt;target&lt;/code&gt;. The problem is that it sets &lt;code&gt;target&lt;/code&gt; everywhere, not just inside itself. The caller's &lt;code&gt;target&lt;/code&gt; is gone.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;local&lt;/code&gt; confines the assignment to the function scope:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;prepare() {
  local target              # confined to this function
  target=$(mktemp -d)
  echo &quot;$target&quot;            # hand the value out via stdout
}

build_dir=$(prepare)        # capture what the function echoed
echo &quot;built in: $build_dir&quot;
echo &quot;release dir still: $target&quot;   # unchanged
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;local target&lt;/code&gt; means: this variable exists only inside this function. When the function returns, the variable and its value vanish. The caller's &lt;code&gt;target&lt;/code&gt; is never touched. The habit I now enforce on every bash function I write: &lt;code&gt;local&lt;/code&gt; for every variable the function introduces, not just the ones I think might conflict. The conflict I do not predict is the one that deletes the wrong directory.&lt;/p&gt;
&lt;h3 id=&quot;return-is-a-status-not-a-value&quot;&gt;return is a status, not a value&lt;/h3&gt;
&lt;p&gt;After the incident, I audited every function in the deploy script. I found a second bug:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;count_pending() {
  local n
  n=$(find &quot;$QUEUE_DIR&quot; -type f | wc -l)
  return &quot;$n&quot;      # WRONG if n &amp;gt; 255
}

count_pending
if [[ $? -gt 0 ]]; then
  echo &quot;queue has items&quot;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;return&lt;/code&gt; sets an exit status. Exit statuses are a single byte: &lt;code&gt;0&lt;/code&gt; to &lt;code&gt;255&lt;/code&gt;. &lt;code&gt;return 300&lt;/code&gt; wraps to &lt;code&gt;44&lt;/code&gt;. For months the queue count had been above 255 on busy days, and the &lt;code&gt;$?&lt;/code&gt; check was comparing against a wrapped value. The logic had been wrong for months and had accidentally worked because the wrapped values still triggered the &lt;code&gt;gt 0&lt;/code&gt; condition. But any script making real decisions based on the actual count — how many workers to spin up, whether to page someone — would have been working with garbage.&lt;/p&gt;
&lt;p&gt;The correct pattern is to echo the value and capture it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;count_pending() {
  local n
  n=$(find &quot;$QUEUE_DIR&quot; -type f | wc -l)
  echo &quot;$n&quot;         # data goes to stdout
  return 0          # status: success
}

pending=$(count_pending)
echo &quot;queue depth: $pending&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;return&lt;/code&gt; answers &quot;did this succeed.&quot; &lt;code&gt;echo&lt;/code&gt; plus command substitution answers &quot;what is the value.&quot; These are two different questions and bash gives you two different mechanisms for a reason. Mixing them is how a function that counts 300 items makes the caller think it counted 44.&lt;/p&gt;
&lt;h3 id=&quot;arguments-and-the-quoting-rule&quot;&gt;Arguments and the $@ quoting rule&lt;/h3&gt;
&lt;p&gt;Inside a function, arguments arrive as positional parameters: &lt;code&gt;$1&lt;/code&gt;, &lt;code&gt;$2&lt;/code&gt;, all of them as &lt;code&gt;&quot;$@&quot;&lt;/code&gt;, the count as &lt;code&gt;$#&lt;/code&gt;. Quoting &lt;code&gt;&quot;$@&quot;&lt;/code&gt; is what keeps multi-word arguments intact:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;process_hosts() {
  echo &quot;processing $# hosts&quot;
  for host in &quot;$@&quot;; do
    echo &quot;  checking: $host&quot;
  done
}

process_hosts &quot;web-01&quot; &quot;db primary&quot; &quot;cache-02&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without the quotes around &lt;code&gt;&quot;$@&quot;&lt;/code&gt;, &lt;code&gt;db primary&lt;/code&gt; splits into two loop iterations and you are back to the word-splitting problem from a different angle. The quoted &lt;code&gt;&quot;$@&quot;&lt;/code&gt; is the way bash passes an array of arguments through a function call with each element preserved.&lt;/p&gt;
&lt;p&gt;This matters most when you are writing wrapper functions — functions that receive arguments and pass them to another command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;run_with_retry() {
  local retries=&quot;${1:?}&quot;
  shift
  local attempt=0
  while ((attempt &amp;lt; retries)); do
    &quot;$@&quot; &amp;amp;&amp;amp; return 0
    ((attempt++))
    echo &quot;retry $attempt/$retries&quot;
    sleep 2
  done
  return 1
}

run_with_retry 3 rsync -av &quot;source dir/&quot; remote:/dest/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&quot;$@&quot;&lt;/code&gt; after the &lt;code&gt;shift&lt;/code&gt; is everything after the retry count — the command and all its arguments, each preserved as a separate item even if they contain spaces. Without the quotes, &lt;code&gt;&quot;source dir/&quot;&lt;/code&gt; splits and &lt;code&gt;rsync&lt;/code&gt; receives the wrong arguments.&lt;/p&gt;
&lt;h3 id=&quot;getopts-for-anything-with-flags&quot;&gt;getopts for anything with flags&lt;/h3&gt;
&lt;p&gt;For one or two fixed positional arguments, reading &lt;code&gt;$1&lt;/code&gt; and &lt;code&gt;$2&lt;/code&gt; directly is fine. The moment a function or script takes optional flags in any order, do not parse them by hand:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;verbose=0
output_dir=&quot;&quot;

while getopts &quot;vd:&quot; opt; do
  case &quot;$opt&quot; in
    v) verbose=1 ;;
    d) output_dir=&quot;$OPTARG&quot; ;;
    *) echo &quot;usage: $0 [-v] [-d dir]&quot;; exit 1 ;;
  esac
done
shift $((OPTIND - 1))   # move past the flags to positional args
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The colon after &lt;code&gt;d&lt;/code&gt; marks it as requiring an argument, which arrives in &lt;code&gt;$OPTARG&lt;/code&gt;. &lt;code&gt;getopts&lt;/code&gt; handles flag bundling (&lt;code&gt;-vd dir&lt;/code&gt;), missing arguments (&lt;code&gt;-d&lt;/code&gt; with no path generates an error automatically), and unknown flags. Hand-rolled &lt;code&gt;$1&lt;/code&gt; parsing gets all of these wrong in subtle ways — it accepts &lt;code&gt;-d&lt;/code&gt; at the end without a value, it does not handle &lt;code&gt;-vd dir&lt;/code&gt;, and it requires the flags in a specific order.&lt;/p&gt;
&lt;p&gt;The deploy script that caused the incident had hand-rolled argument parsing. Among other things, it silently accepted a &lt;code&gt;--target&lt;/code&gt; flag with no value and proceeded with an empty string, which caused a different class of problem I had also not fully traced before the bigger incident made the whole thing visible.&lt;/p&gt;
&lt;h3 id=&quot;what-the-deploy-script-looks-like-now&quot;&gt;What the deploy script looks like now&lt;/h3&gt;
&lt;p&gt;Every function declares &lt;code&gt;local&lt;/code&gt; for every variable. Values that need to cross function boundaries go through &lt;code&gt;echo&lt;/code&gt; and command substitution. Exit statuses communicate success or failure. &lt;code&gt;getopts&lt;/code&gt; handles the flags. There is a &lt;code&gt;trap&lt;/code&gt; on &lt;code&gt;EXIT&lt;/code&gt; that cleans up the temp directory using a local variable that only the cleanup function can see:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cleanup() {
  local tmp_dir=&quot;${TEMP_BUILD_DIR:-}&quot;   # local ref to the temp dir
  [[ -n &quot;$tmp_dir&quot; &amp;amp;&amp;amp; -d &quot;$tmp_dir&quot; ]] &amp;amp;&amp;amp; rm -rf &quot;$tmp_dir&quot;
}
trap cleanup EXIT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;$target&lt;/code&gt; variable in the main script is set once at the top and never touched by any function. Functions that need a temp directory create one, store it in a local variable, use it, and the cleanup trap handles removal. The variable naming conflict that caused the incident cannot happen because the pattern prevents it structurally.&lt;/p&gt;
&lt;p&gt;The application has been running cleanly since then. The deploy script has hit the failure path twice since the fix — different failures, unrelated causes — and both times the cleanup ran correctly and left the running release untouched.&lt;/p&gt;
&lt;p&gt;Full version with the local-scope fix, the echo-for-values pattern, the return-as-status trap, and a getopts template: &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-functions-arguments&quot;&gt;https://bashsnippets.xyz/snippets/bash-functions-arguments&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A function that fails should fail loudly — wrap the script in &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-error-handling&quot;&gt;set -euo pipefail&lt;/a&gt; — and the &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/tools/bash-boilerplate-generator&quot;&gt;bash boilerplate generator&lt;/a&gt; can scaffold all of this with the right traps and argument parsing wired in from the start. The rest is at &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz&quot;&gt;https://bashsnippets.xyz&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-functions-arguments&quot;&gt;bashsnippets.xyz/snippets/bash-functions-arguments&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21095/a-function-without-local-overwrote-my-variable-and-rm-rf-deleted-the-wrong-directory</guid>
<pubDate>Mon, 22 Jun 2026 02:45:23 +0000</pubDate>
</item>
<item>
<title>The Alert Never Fired Because the Loop Skipped the Last Line of the File</title>
<link>https://coderlegion.com/20988/the-alert-never-fired-because-the-loop-skipped-the-last-line-of-the-file</link>
<description>&lt;p&gt;We kept a plaintext file of hostnames, one per line, and a monitoring script read the file and pinged each host every five minutes. When a host failed to respond, the script sent an email alert. The system had been running for months and it worked — we had caught three actual outages with it, which gave us real confidence in the setup.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;app-07&lt;/code&gt; was added to the list on a Thursday afternoon. The engineer who added it was using VS Code on a Mac, and VS Code by default does not add a trailing newline to a file when you append to it using certain editing workflows. The file had ended in a newline before the edit. After the edit, the last line — &lt;code&gt;app-07&lt;/code&gt; — had no trailing newline.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;app-07&lt;/code&gt; went down the following Sunday afternoon at 2:17pm. The monitoring script ran at 2:20, 2:25, 2:30, all the way through the evening. No alert ever fired. The on-call engineer found out at 8pm when a client emailed. The system had been down for almost six hours.&lt;/p&gt;
&lt;p&gt;When I looked at the script, the bug was immediately obvious once I knew what to look for. But I had written that script, I had tested it, and I had been looking at the monitoring confirmation emails for months without ever noticing. The confirmation email listed the hosts it checked. &lt;code&gt;app-07&lt;/code&gt; was never in the list. I had been reading those emails without actually counting the hosts. I just scanned for the OK lines and moved on.&lt;/p&gt;
&lt;h3 id=&quot;why-read-drops-the-last-line&quot;&gt;Why read drops the last line&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;read&lt;/code&gt; returns a success exit status when it reads a line and finds the newline that terminates it. When the file does not end in a newline, &lt;code&gt;read&lt;/code&gt; still populates the variable with the final line's content, but it returns a non-zero (failure) exit status because it hit end-of-file before finding a terminator. A &lt;code&gt;while read host&lt;/code&gt; loop checks the return status to decide whether to execute the loop body. On the final, newline-less line, &lt;code&gt;read&lt;/code&gt; puts &lt;code&gt;app-07&lt;/code&gt; into &lt;code&gt;host&lt;/code&gt; and then returns failure. The &lt;code&gt;while&lt;/code&gt; loop sees failure and exits without running the body. The content is there. The variable is populated. The loop throws it away.&lt;/p&gt;
&lt;p&gt;This behavior is documented in the POSIX spec for &lt;code&gt;read&lt;/code&gt;. It is not a bash quirk. Any POSIX shell handles the missing-final-newline case this way. A plain &lt;code&gt;while read line&lt;/code&gt; loop is incorrect for any file you do not personally control the formatting of, which in practice means nearly any file.&lt;/p&gt;
&lt;p&gt;The fix is one extra clause:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash

# Script: check-hosts.sh

# Purpose: ping every host in a file, including a newline-less final line

# Usage: ./check-hosts.sh hosts.txt

set -euo pipefail

CHECK=&quot;✓&quot;
CROSS=&quot;✗&quot;
HOST_FILE=&quot;${1:?Usage: check-hosts.sh &amp;lt;host-file&amp;gt;}&quot;

while IFS= read -r host || [[ -n &quot;$host&quot; ]]; do
  [[ -z &quot;$host&quot; || &quot;$host&quot; == \#* ]] &amp;amp;&amp;amp; continue
  if ping -c1 -W2 &quot;$host&quot; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
    echo &quot;$CHECK up:   $host&quot;
  else
    echo &quot;$CROSS down: $host&quot;
  fi
done &amp;lt; &quot;$HOST_FILE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;|| [[ -n &quot;$host&quot; ]]&lt;/code&gt; says: if &lt;code&gt;read&lt;/code&gt; returned failure but the variable is non-empty, run the loop body anyway. That is precisely the leftover-final-line case. &lt;code&gt;read&lt;/code&gt; failed because it hit end-of-file, but it populated &lt;code&gt;host&lt;/code&gt; with &lt;code&gt;app-07&lt;/code&gt; before returning. The &lt;code&gt;||&lt;/code&gt; catches it. &lt;code&gt;app-07&lt;/code&gt; gets pinged.&lt;/p&gt;
&lt;h3 id=&quot;what-ifs-and--r-actually-do&quot;&gt;What IFS= and -r actually do&lt;/h3&gt;
&lt;p&gt;You see &lt;code&gt;while IFS= read -r line&lt;/code&gt; written in every correct read-loop example, and it is worth being specific about what each piece prevents because both have their own failure mode.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;IFS=&lt;/code&gt; sets the field separator to empty for the duration of the &lt;code&gt;read&lt;/code&gt; command. Without it, &lt;code&gt;read&lt;/code&gt; strips leading and trailing whitespace from each line. A hostname like &lt;code&gt;  app-07  &lt;/code&gt; (with leading spaces, which some editors produce) becomes &lt;code&gt;app-07&lt;/code&gt;, which might be correct. An indented config value, a Python-style YAML string, a log line that starts with spaces for alignment — all of these are silently modified. Setting &lt;code&gt;IFS=&lt;/code&gt; tells &lt;code&gt;read&lt;/code&gt; to take the line exactly as it appears.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;-r&lt;/code&gt; prevents &lt;code&gt;read&lt;/code&gt; from interpreting backslash sequences. Without &lt;code&gt;-r&lt;/code&gt;, a line like &lt;code&gt;C:\temp\logs&lt;/code&gt; has its backslashes consumed as escape characters and arrives as &lt;code&gt;C:templogs&lt;/code&gt;. This matters less for hostname files and enormously for any script that processes Windows paths, config files that use backslash as a line-continuation character, or log files from mixed-OS environments. The &lt;code&gt;-r&lt;/code&gt; flag is essentially free protection; there is no reason not to include it.&lt;/p&gt;
&lt;p&gt;A bare &lt;code&gt;read line&lt;/code&gt; without either flag silently mangles both whitespace and backslashes. The script works correctly on clean input and produces wrong output on input with edge cases. The wrong output does not produce an error. You find out when the data that mattered was the indented or backslash-containing kind.&lt;/p&gt;
&lt;h3 id=&quot;the-subshell-trap-that-kills-your-counters&quot;&gt;The subshell trap that kills your counters&lt;/h3&gt;
&lt;p&gt;This is the one that is most likely to make you question your sanity:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;
# This looks correct. It is not.

fails=0
cat hosts.txt | while IFS= read -r host; do
  ping -c1 -W2 &quot;$host&quot; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 || ((fails++))
done
echo &quot;Total failures: $fails&quot;   # Always prints 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The pipe creates a subshell for the right side. The &lt;code&gt;while&lt;/code&gt; loop runs inside that subshell. &lt;code&gt;fails&lt;/code&gt; increments correctly inside the subshell. When the subshell exits, the parent shell's &lt;code&gt;fails&lt;/code&gt; is still &lt;code&gt;0&lt;/code&gt;, because the increment happened in a different process. The parent echo sees the original value.&lt;/p&gt;
&lt;p&gt;This catches people because the loop body itself works — the pings happen, the increment logic is correct — but any state the loop was supposed to accumulate for later use is silently discarded. I spent forty minutes on a version of this problem before I remembered that pipes create subshells. It is the kind of thing that feels like a bash bug until you understand that it is behaving exactly as documented.&lt;/p&gt;
&lt;p&gt;The fix is to redirect the file into the loop instead of piping into it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;fails=0
while IFS= read -r host || [[ -n &quot;$host&quot; ]]; do
  ping -c1 -W2 &quot;$host&quot; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 || ((fails++))
done &amp;lt; hosts.txt
echo &quot;Total failures: $fails&quot;   # Now correct
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;done &amp;lt; hosts.txt&lt;/code&gt; feeds the file to the loop's stdin without a pipe. The loop runs in the current shell. &lt;code&gt;fails&lt;/code&gt; accumulates in the current shell. The echo sees the real count.&lt;/p&gt;
&lt;h3 id=&quot;why-for-loop-is-wrong-for-this&quot;&gt;Why for loop is wrong for this&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;
# Never do this — iterates words, not lines

for line in $(cat hosts.txt); do
  ping -c1 &quot;$line&quot;
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;$(cat hosts.txt)&lt;/code&gt; is command substitution. Bash captures the text output and word-splits it on IFS — spaces, tabs, newlines. For a file with one hostname per line and no spaces in the hostnames, this accidentally produces the right behavior. For any file with spaces — log lines, config values, paths with spaces, anything a non-developer might have generated — it splits lines into fragments and each fragment becomes a loop iteration.&lt;/p&gt;
&lt;p&gt;There is no version of &lt;code&gt;for line in $(cat file)&lt;/code&gt; that is correct for reading lines. The right tool is always &lt;code&gt;while IFS= read -r line || [[ -n &quot;$line&quot; ]]; do ... done &amp;lt; file&lt;/code&gt;. The for loop is the right tool for iterating a known list that you control directly, not for reading file content.&lt;/p&gt;
&lt;h3 id=&quot;the-monitoring-system-after-the-fix&quot;&gt;The monitoring system, after the fix&lt;/h3&gt;
&lt;p&gt;After the &lt;code&gt;app-07&lt;/code&gt; incident we made three changes. The obvious one was fixing the read loop with the &lt;code&gt;|| [[ -n &quot;$host&quot; ]]&lt;/code&gt; guard. The second was adding a sanity check at the top of the script that counted the lines in the hosts file and compared it to the number of hosts the loop actually processed — a mismatch meant the file was malformed or something else was wrong. The third was adding a nightly email that included the count of hosts checked, not just the status of each one, so a future addition to the file that somehow got lost would show up as &quot;expected 12 hosts, checked 11.&quot;&lt;/p&gt;
&lt;p&gt;The confirmation email count was something I should have had from the start. If I had been looking at &quot;checked 11/12 hosts&quot; instead of a list of OK lines, I would have noticed &lt;code&gt;app-07&lt;/code&gt; missing on the first night. The monitoring was working. The observability of the monitoring was not.&lt;/p&gt;
&lt;p&gt;Full version with CSV-field parsing, comment-skipping, and the subshell-safe redirect form: &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-read-file-line-by-line&quot;&gt;https://bashsnippets.xyz/snippets/bash-read-file-line-by-line&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;To iterate a list of files rather than a file's contents, reach for a &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-for-loop-examples&quot;&gt;for loop&lt;/a&gt; instead, and wrap anything that acts on what it reads in &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-error-handling&quot;&gt;set -euo pipefail&lt;/a&gt;. More at &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz&quot;&gt;https://bashsnippets.xyz&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-read-file-line-by-line&quot;&gt;bashsnippets.xyz/snippets/bash-read-file-line-by-line&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20988/the-alert-never-fired-because-the-loop-skipped-the-last-line-of-the-file</guid>
<pubDate>Fri, 19 Jun 2026 17:58:59 +0000</pubDate>
</item>
<item>
<title>Stop Guessing 'chmod'  I Built A Free Visual Permissions Builder for Linux</title>
<link>https://coderlegion.com/16987/stop-guessing-chmod-i-built-a-free-visual-permissions-builder-for-linux</link>
<description>&lt;h3 id=&quot;the-problem&quot;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;You're deploying a script and need the right file permissions. You type &lt;code&gt;chmod 754&lt;/code&gt;... or was it &lt;code&gt;744&lt;/code&gt;? You Google it. You find a Stack Overflow thread from 2011. You get it wrong anyway.&lt;/p&gt;
&lt;p&gt;chmod's octal notation is one of those things every Linux user has to look up constantly — and the mental model for &lt;em&gt;why&lt;/em&gt; &lt;code&gt;755&lt;/code&gt; means what it means isn't obvious unless you've memorized the bit table.&lt;/p&gt;
&lt;p&gt;I built a free browser tool to fix that permanently.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-tool-chmod-permissions-builder&quot;&gt;The Tool: chmod Permissions Builder&lt;/h3&gt;
&lt;p&gt; &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/tools/chmod-permissions-builder.html&quot;&gt;bashsnippets.xyz/tools/chmod-permissions-builder.html&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Free, no-login, browser-based. Click checkboxes for &lt;strong&gt;Owner&lt;/strong&gt;, &lt;strong&gt;Group&lt;/strong&gt;, and &lt;strong&gt;Others&lt;/strong&gt; — and it instantly generates:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;chmod&lt;/code&gt; octal command (&lt;code&gt;chmod 755 filename&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The symbolic command (&lt;code&gt;chmod u=rwx,g=rx,o=rx filename&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A plain-English explanation of what those permissions do&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Copy and go. No fluff.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-bit-math-once-you-see-it-you-can-t-unsee-it-&quot;&gt;The Bit Math (Once You See It, You Can't Unsee It)&lt;/h3&gt;
&lt;p&gt;Each permission set is a 3-bit binary number:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Permission &lt;/th&gt;&lt;th&gt;Binary &lt;/th&gt;&lt;th&gt;Octal&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;---&lt;/code&gt; &lt;/td&gt;&lt;td&gt;000 &lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--x&lt;/code&gt; &lt;/td&gt;&lt;td&gt;001 &lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;-w-&lt;/code&gt; &lt;/td&gt;&lt;td&gt;010 &lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;r--&lt;/code&gt; &lt;/td&gt;&lt;td&gt;100 &lt;/td&gt;&lt;td&gt;4&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;r-x&lt;/code&gt; &lt;/td&gt;&lt;td&gt;101 &lt;/td&gt;&lt;td&gt;5&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;rw-&lt;/code&gt; &lt;/td&gt;&lt;td&gt;110 &lt;/td&gt;&lt;td&gt;6&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;rwx&lt;/code&gt; &lt;/td&gt;&lt;td&gt;111 &lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;So &lt;code&gt;chmod 755&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;7&lt;/strong&gt; = owner → &lt;code&gt;rwx&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;5&lt;/strong&gt; = group → &lt;code&gt;r-x&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;5&lt;/strong&gt; = others → &lt;code&gt;r-x&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The tool builds this visual live as you click checkboxes, so the connection between binary, octal, and the actual permissions is always visible.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;most-used-permission-combos&quot;&gt;Most-Used Permission Combos&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;chmod 755 script.sh      # Executable script — owner edits, others run
chmod 644 config.conf    # Config — owner edits, others read-only
chmod 600 .ssh/id_rsa    # SSH private key — owner only, absolutely no one else
chmod 700 ~/scripts/     # Private directory — owner access only
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;recursive-permission-application&quot;&gt;Recursive Permission Application&lt;/h3&gt;
&lt;p&gt;The builder outputs a single-file command. For full directories:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# The safe way — directories and files get different permissions
find /var/www/html -type d -exec chmod 755 {} \;
find /var/www/html -type f -exec chmod 644 {} \;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ Avoid &lt;code&gt;chmod -R 777&lt;/code&gt; on anything that matters. It removes every security boundary between your files and other processes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;check-what-s-there-first&quot;&gt;Check What's There First&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ls -la filename          # Human-readable
stat -c &quot;%a&quot; filename    # Just the octal number
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;stat&lt;/code&gt; when you need to replicate permissions from one server to another — it gives you the exact octal to paste into the builder or &lt;code&gt;chmod&lt;/code&gt; directly.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;try-it&quot;&gt;Try It&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/tools/chmod-permissions-builder.html&quot;&gt;→ chmod Permissions Builder — bashsnippets.xyz&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;No login. No install. Works on mobile. If it saves you one bad deployment, it's done its job.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;More free bash tools and copy-paste scripts at &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz&quot;&gt;bashsnippets.xyz&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/16987/stop-guessing-chmod-i-built-a-free-visual-permissions-builder-for-linux</guid>
<pubDate>Thu, 07 May 2026 20:27:16 +0000</pubDate>
</item>
<item>
<title>Bash exit codes 0-255: what they mean and how to write the handler</title>
<link>https://coderlegion.com/16902/bash-exit-codes-0-255-what-they-mean-and-how-to-write-the-handler</link>
<description>&lt;h3 id=&quot;-holds-the-exit-code-of-the-last-command-that-ran-you-ve-seen-it-you-ve-probably-checked-it-what-you-might-not-have-is-a-reliable-reference-for-what-each-value-actually-means-and-more-importantly-what-to-do-about-it-in-your-script-&quot;&gt;&lt;code&gt;$?&lt;/code&gt; holds the exit code of the last command that ran. You've seen it. You've probably checked it. What you might not have is a reliable reference for what each value actually means — and more importantly, what to &lt;em&gt;do&lt;/em&gt; about it in your script.&lt;/h3&gt;
&lt;h4 id=&quot;here-s-the-most-common-codes-with-the-detail-that-s-usually-missing-from-quick-reference-tables-&quot;&gt;Here's the most common codes with the detail that's usually missing from quick-reference tables.&lt;/h4&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;0 — Success&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;the-command-exited-cleanly-zero-is-the-only-value-that-means-success-everything-else-is-some-form-of-failure-or-interruption-&quot;&gt;The command exited cleanly. Zero is the only value that means success. Everything else is some form of failure or interruption.&lt;/h4&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;1 — General error&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;this-is-the-catch-all-a-lot-of-programs-use-1-to-signal-something-went-wrong-but-i-m-not-going-to-be-more-specific-when-you-see-1-you-need-to-look-at-the-command-s-own-output-or-error-messages-to-find-out-what-actually-failed-&quot;&gt;This is the catch-all. A lot of programs use 1 to signal &quot;something went wrong but I'm not going to be more specific.&quot; When you see 1, you need to look at the command's own output or error messages to find out what actually failed.&lt;/h4&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;2 — Misuse of shell built-in&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;you-called-a-bash-built-in-with-wrong-arguments-common-cause-if-var-with-a-missing-right-hand-operand-the-command-exits-2-when-the-syntax-is-wrong-&quot;&gt;You called a bash built-in with wrong arguments. Common cause: &lt;code&gt;if [ $var = ]&lt;/code&gt; with a missing right-hand operand. The &lt;code&gt;[&lt;/code&gt; command exits 2 when the syntax is wrong.&lt;/h4&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;126 — Command found, not executable&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;the-binary-exists-but-can-t-be-run-most-common-cause-missing-execute-permission-fix-chmod-x-yourscript-sh-worth-noting-this-is-different-from-127-the-system-found-the-file-it-just-can-t-execute-it-&quot;&gt;The binary exists but can't be run. Most common cause: missing execute permission. Fix: &lt;code&gt;chmod +x yourscript.sh&lt;/code&gt;. Worth noting: this is &lt;em&gt;different&lt;/em&gt; from 127. The system found the file — it just can't execute it.&lt;/h4&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;127 — Command not found&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;the-binary-doesn-t-exist-or-isn-t-in-path-most-common-causes-typo-in-command-name-package-not-installed-or-path-issue-in-cron-see-the-cron-job-post-above-when-you-see-127-in-a-cron-job-context-check-path-first-&quot;&gt;The binary doesn't exist or isn't in PATH. Most common causes: typo in command name, package not installed, or PATH issue in cron (see the cron job post above). When you see 127 in a cron job context, check PATH first.&lt;/h4&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;130 — Killed with Ctrl-C (SIGINT)&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;the-formula-is-128-signal-number-sigint-signal-2-128-2-130-if-your-script-sees-130-from-a-subprocess-someone-hit-ctrl-c-or-the-process-received-sigint-programmatically-&quot;&gt;The formula is 128 + signal number. SIGINT = signal 2. 128 + 2 = 130. If your script sees 130 from a subprocess, someone hit Ctrl-C (or the process received SIGINT programmatically).&lt;/h4&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;137 — Killed with SIGKILL&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;128-9-sigkill-this-is-the-oom-killer-when-linux-runs-out-of-memory-and-needs-to-reclaim-it-fast-sigkill-is-what-it-uses-if-your-script-exits-137-unexpectedly-on-a-server-check-available-memory-free--h-and-dmesg-grep--i-kill-&quot;&gt;128 + 9 (SIGKILL). This is the OOM killer. When Linux runs out of memory and needs to reclaim it fast, SIGKILL is what it uses. If your script exits 137 unexpectedly on a server, check available memory — &lt;code&gt;free -h&lt;/code&gt; and &lt;code&gt;dmesg | grep -i kill&lt;/code&gt;.&lt;/h4&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;139 — Segmentation fault&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;the-process-tried-to-access-memory-it-wasn-t-allowed-to-access-in-a-bash-context-this-usually-means-a-compiled-binary-your-script-called-has-a-bug-you-re-not-going-to-fix-the-segfault-from-bash-but-you-can-detect-it-and-handle-it-cleanly-&quot;&gt;The process tried to access memory it wasn't allowed to access. In a bash context, this usually means a compiled binary your script called has a bug. You're not going to fix the segfault from bash — but you can detect it and handle it cleanly.&lt;/h4&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;143 — Killed with SIGTERM&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;128-15-sigterm-this-is-the-polite-kill-what-kill-pid-sends-by-default-and-what-systemd-sends-when-stopping-a-service-unlike-sigkill-sigterm-can-be-caught-and-handled-&quot;&gt;128 + 15 (SIGTERM). This is the polite kill — what &lt;code&gt;kill PID&lt;/code&gt; sends by default, and what systemd sends when stopping a service. Unlike SIGKILL, SIGTERM can be caught and handled.&lt;/h4&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;The error handler problem&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;knowing-what-a-code-means-is-step-one-writing-a-handler-that-does-something-useful-with-it-is-step-two-most-bash-error-handling-i-see-in-the-wild-looks-like-this-&quot;&gt;Knowing what a code means is step one. Writing a handler that does something useful with it is step two. Most bash error handling I see in the wild looks like this:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;if [ $? -ne 0 ]; then
  echo &quot;error&quot;
  exit 1
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;which-logs-nothing-meaningful-and-loses-the-original-exit-code-a-better-pattern-&quot;&gt;Which logs nothing meaningful and loses the original exit code. A better pattern:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;if [ $? -eq 127 ]; then
  echo &quot;ERROR: command not found — check PATH or installation&quot; &amp;gt;&amp;amp;2
  exit 127
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;log-to-stderr-preserve-the-original-exit-code-be-explicit-about-what-went-wrong-&quot;&gt;Log to stderr, preserve the original exit code, be explicit about what went wrong.&lt;/h3&gt;
&lt;hr&gt;
&lt;h3 id=&quot;free-tool-that-generates-these-handlers&quot;&gt;&lt;strong&gt;Free tool that generates these handlers&lt;/strong&gt;&lt;/h3&gt;
&lt;h4 id=&quot;i-built-bash-exit-code-lookup-https-bashsnippets-xyz-tools-bash-exit-code-lookup-html-1-to-remove-the-google-it-then-write-the-handler-by-hand-step-you-type-a-code-or-click-one-of-the-common-ones-pick-a-handler-style-if-else-block-case-statement-or-inline-fallback-and-copy-the-generated-handler-directly-into-your-script-&quot;&gt;I built [Bash Exit Code Lookup][(https://bashsnippets.xyz/tools/bash-exit-code-lookup.html)]&lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/tools/bash-exit-code-lookup.html&quot;&gt;1&lt;/a&gt; to remove the &quot;Google it, then write the handler by hand&quot; step. You type a code (or click one of the common ones), pick a handler style — if/else block, case statement, or inline &lt;code&gt;||&lt;/code&gt; fallback — and copy the generated handler directly into your script.&lt;/h4&gt;
&lt;h4 id=&quot;all-codes-0-255-are-in-there-free-no-login-&quot;&gt;All codes 0-255 are in there. Free, no login.&lt;/h4&gt;
&lt;p&gt;→ &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/tools/bash-exit-code-lookup.html&quot;&gt;https://bashsnippets.xyz/tools/bash-exit-code-lookup.html&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16902/bash-exit-codes-0-255-what-they-mean-and-how-to-write-the-handler</guid>
<pubDate>Wed, 06 May 2026 22:27:13 +0000</pubDate>
</item>
<item>
<title>The 4 things every cronjob needs that most tutorials skip</title>
<link>https://coderlegion.com/16899/the-4-things-every-cronjob-needs-that-most-tutorials-skip</link>
<description>&lt;h3 id=&quot;cron-job-tutorials-have-a-problem-they-teach-you-the-five-field-syntax-show-you-a-couple-of-examples-like-0-2-and-send-you-on-your-way-what-they-don-t-cover-is-everything-that-needs-to-go-around-the-expression-for-the-job-to-actually-work-reliably-in-production-&quot;&gt;Cron job tutorials have a problem. They teach you the five-field syntax, show you a couple of examples like &lt;code&gt;0 2 * * *&lt;/code&gt;, and send you on your way. What they don't cover is everything that needs to go &lt;em&gt;around&lt;/em&gt; the expression for the job to actually work reliably in production.&lt;/h3&gt;
&lt;h4 id=&quot;here-s-what-i-mean-&quot;&gt;Here's what I mean.&lt;/h4&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Problem 1: Cron's PATH is not your PATH&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;when-you-run-a-command-in-your-terminal-your-shell-has-a-full-path-all-the-binary-directories-your-distro-configured-plus-whatever-you-ve-added-when-cron-runs-it-uses-a-stripped-down-default-path-binaries-that-are-available-to-you-interactively-may-not-be-visible-to-cron-&quot;&gt;When you run a command in your terminal, your shell has a full PATH — all the binary directories your distro configured, plus whatever you've added. When cron runs, it uses a stripped-down default PATH. Binaries that are available to you interactively may not be visible to cron.&lt;/h4&gt;
&lt;h4 id=&quot;the-fix-is-to-set-path-explicitly-in-your-crontab-or-at-the-top-of-your-wrapper-script-&quot;&gt;The fix is to set PATH explicitly in your crontab or at the top of your wrapper script:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;PATH=/usr/local/bin:/usr/bin:/bin
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;this-single-line-prevents-the-most-common-class-of-cron-job-runs-but-does-nothing-failures-&quot;&gt;This single line prevents the most common class of &quot;cron job runs but does nothing&quot; failures.&lt;/h4&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Problem 2: Cron's default shell is sh, not bash&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;if-your-script-uses-any-bash-specific-syntax-arrays-conditionals-substitution-styles-and-cron-is-running-it-with-sh-you-ll-get-silent-failures-or-unexpected-behavior-&quot;&gt;If your script uses any bash-specific syntax — arrays, &lt;code&gt;[[ ]]&lt;/code&gt; conditionals, &lt;code&gt;$()&lt;/code&gt; substitution styles — and cron is running it with sh, you'll get silent failures or unexpected behavior.&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;SHELL=/bin/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;add-this-to-the-top-of-your-crontab-one-line-&quot;&gt;Add this to the top of your crontab. One line.&lt;/h4&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Problem 3: No logging means no debugging&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;if-a-cron-job-fails-where-does-the-error-go-by-default-nowhere-it-vanishes-adding-log-redirection-to-every-crontab-entry-is-the-fix-&quot;&gt;If a cron job fails, where does the error go? By default: nowhere. It vanishes. Adding log redirection to every crontab entry is the fix:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;0 2 * * * /path/to/script.sh &amp;gt;&amp;gt; /var/log/mybackup.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-2-gt-amp-1-captures-stderr-alongside-stdout-so-you-see-command-errors-not-just-your-script-s-output-&quot;&gt;The &lt;code&gt;2&amp;gt;&amp;amp;1&lt;/code&gt; captures stderr alongside stdout so you see command errors, not just your script's output.&lt;/h4&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Problem 4: Long-running jobs can stack on top of themselves&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;if-you-have-a-job-that-runs-every-5-minutes-but-sometimes-takes-8-minutes-a-second-instance-starts-before-the-first-one-finishes-depending-on-what-the-script-is-doing-writing-to-a-file-syncing-a-directory-running-a-database-query-this-can-corrupt-output-&quot;&gt;If you have a job that runs every 5 minutes but sometimes takes 8 minutes, a second instance starts before the first one finishes. Depending on what the script is doing (writing to a file, syncing a directory, running a database query), this can corrupt output.&lt;/h4&gt;
&lt;h4 id=&quot;lock-files-via-flock-prevent-this-&quot;&gt;Lock files via &lt;code&gt;flock&lt;/code&gt; prevent this:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;/usr/bin/flock -n /tmp/myjob.lock /path/to/script.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-second-instance-sees-the-lock-is-held-and-exits-immediately-&quot;&gt;The second instance sees the lock is held and exits immediately.&lt;/h4&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;All of this is in a free tool&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;i-built-cron-job-builder-to-handle-all-four-of-these-automatically-you-pick-your-schedule-from-dropdowns-check-the-options-you-want-logging-path-shell-mailto-lock-file-and-it-generates-the-complete-crontab-entry-expression-wrappers-bash-header-ready-to-paste-into-crontab--e-&quot;&gt;I built &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/tools/cron-job-builder.html&quot;&gt;Cron Job Builder&lt;/a&gt; to handle all four of these automatically. You pick your schedule from dropdowns, check the options you want (logging, PATH, SHELL, MAILTO, lock file), and it generates the complete crontab entry — expression + wrappers + bash header — ready to paste into &lt;code&gt;crontab -e&lt;/code&gt;.&lt;/h4&gt;
&lt;h4 id=&quot;it-also-shows-you-the-next-10-scheduled-run-times-so-you-can-verify-your-expression-is-actually-doing-what-you-intend-before-you-deploy-it-&quot;&gt;It also shows you the next 10 scheduled run times so you can verify your expression is actually doing what you intend before you deploy it.&lt;/h4&gt;
&lt;h4 id=&quot;there-are-quick-presets-for-the-schedules-you-use-constantly-daily-2am-every-6-hours-weekdays-9am-monthly-on-the-1st-every-15-minutes-&quot;&gt;There are quick presets for the schedules you use constantly: daily 2am, every 6 hours, weekdays 9am, monthly on the 1st, every 15 minutes.&lt;/h4&gt;
&lt;h4 id=&quot;no-login-no-account-runs-entirely-in-the-browser-free-&quot;&gt;No login, no account, runs entirely in the browser. Free.&lt;/h4&gt;
&lt;p&gt;→ &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/tools/cron-job-builder.html&quot;&gt;https://bashsnippets.xyz/tools/cron-job-builder.html&lt;/a&gt;&lt;/p&gt;
&lt;h5 id=&quot;the-rest-of-the-library-free-bash-scripts-for-backup-log-cleanup-disk-monitoring-uptime-checking-lives-at-https-bashsnippets-xyz-&quot;&gt;The rest of the library — free bash scripts for backup, log cleanup, disk monitoring, uptime checking — lives at &lt;a rel=&quot;nofollow&quot; href=&quot;https://https://bashsnippets.xyz&quot;&gt;https://bashsnippets.xyz&lt;/a&gt;.&lt;/h5&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/16899/the-4-things-every-cronjob-needs-that-most-tutorials-skip</guid>
<pubDate>Wed, 06 May 2026 21:23:10 +0000</pubDate>
</item>
<item>
<title>Bash Script to Automatically Renew and Reload SSL Certificates</title>
<link>https://coderlegion.com/13256/bash-script-to-automatically-renew-and-reload-ssl-certificates</link>
<description>&lt;p&gt;Keeping your website secure isn’t optional anymore. Users expect it, browsers enforce it, and search engines reward it. SSL certificates are a core part of that security, but managing them manually can quickly become a headache, especially when they expire every 90 days.&lt;/p&gt;
&lt;p&gt;If you’ve ever had a site go down or show a &quot;Not Secure&quot; warning because of an expired certificate, you know how frustrating it can be.&lt;/p&gt;
&lt;p&gt;The good news is that you can automate the entire process. In this post, you’ll learn how to use a simple Bash script to automatically renew your SSL certificates and reload your web server without any downtime.&lt;/p&gt;
&lt;h3 id=&quot;why-automating-ssl-renewal-matters&quot;&gt;Why Automating SSL Renewal Matters&lt;/h3&gt;
&lt;p&gt;Let’s start with the obvious. SSL certificates expire. If you’re using Let’s Encrypt, they typically last 90 days. That’s great for security, but it means you need a reliable renewal process.&lt;/p&gt;
&lt;p&gt;Manual renewal comes with risks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You forget to renew on time&lt;/li&gt;
&lt;li&gt;You renew but forget to reload your server&lt;/li&gt;
&lt;li&gt;Your site goes offline unexpectedly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Automation solves all of this. Once set up, your system quietly handles renewals in the background.&lt;/p&gt;
&lt;p&gt;You get:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Continuous HTTPS protection&lt;/li&gt;
&lt;li&gt;Zero manual effort&lt;/li&gt;
&lt;li&gt;No surprise outages&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;what-this-script-does&quot;&gt;What This Script Does&lt;/h3&gt;
&lt;p&gt;The Bash script we’re working with handles three main tasks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Renews SSL certificates using Certbot&lt;/li&gt;
&lt;li&gt;Checks if renewal was successful&lt;/li&gt;
&lt;li&gt;Reloads your web server (like Nginx or Apache) to apply changes&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It’s simple, efficient, and reliable.&lt;/p&gt;
&lt;h3 id=&quot;the-bash-script&quot;&gt;The Bash Script&lt;/h3&gt;
&lt;p&gt;Here’s a clean version of the script:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash

LOG_FILE=&quot;/var/log/ssl_renewal.log&quot;
DATE=$(date '+%Y-%m-%d %H:%M:%S')

echo &quot;[$DATE] Starting SSL renewal process...&quot; &amp;gt;&amp;gt; $LOG_FILE

# Renew certificates
certbot renew --quiet

# Check if renewal was successful
if [ $? -eq 0 ]; then
    echo &quot;[$DATE] SSL certificates renewed successfully.&quot; &amp;gt;&amp;gt; $LOG_FILE
    
    # Reload web server
    systemctl reload nginx
    
    if [ $? -eq 0 ]; then
        echo &quot;[$DATE] Nginx reloaded successfully.&quot; &amp;gt;&amp;gt; $LOG_FILE
    else
        echo &quot;[$DATE] Failed to reload Nginx.&quot; &amp;gt;&amp;gt; $LOG_FILE
    fi
else
    echo &quot;[$DATE] SSL renewal failed.&quot; &amp;gt;&amp;gt; $LOG_FILE
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;breaking-it-down&quot;&gt;Breaking It Down&lt;/h3&gt;
&lt;p&gt;If you’re not deeply familiar with Bash, here’s what’s happening in plain terms.&lt;/p&gt;
&lt;h4 id=&quot;logging-activity&quot;&gt;Logging Activity&lt;/h4&gt;
&lt;p&gt;The script logs everything to a file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;LOG_FILE=&quot;/var/log/ssl_renewal.log&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is useful for troubleshooting. If something goes wrong, you can check the log.&lt;/p&gt;
&lt;h4 id=&quot;running-certbot&quot;&gt;Running Certbot&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;certbot renew --quiet
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command attempts to renew any certificates that are close to expiration. The &lt;code&gt;--quiet&lt;/code&gt; flag keeps things clean unless there’s an error.&lt;/p&gt;
&lt;h4 id=&quot;checking-success&quot;&gt;Checking Success&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;if [ $? -eq 0 ];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This checks whether the previous command worked. A return value of &lt;code&gt;0&lt;/code&gt; means success.&lt;/p&gt;
&lt;h4 id=&quot;reloading-the-server&quot;&gt;Reloading the Server&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This applies the new certificates without restarting the server, which means no downtime.&lt;/p&gt;
&lt;p&gt;If you’re using Apache instead, you’d replace that with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;systemctl reload apache2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;automating-with-cron&quot;&gt;Automating with Cron&lt;/h3&gt;
&lt;p&gt;The script is only useful if it runs automatically. That’s where cron jobs come in.&lt;/p&gt;
&lt;p&gt;Open your crontab:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;crontab -e
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add this line to run the script daily at 2:30 AM:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;30 2 * * * /path/to/your/script.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make sure your script is executable:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;chmod +x /path/to/your/script.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s it. Your server now handles SSL renewal on its own.&lt;/p&gt;
&lt;h3 id=&quot;best-practices-for-ssl-automation&quot;&gt;Best Practices for SSL Automation&lt;/h3&gt;
&lt;p&gt;To keep things running smoothly, follow a few simple guidelines:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Test your setup&lt;/strong&gt;&lt;br&gt;
Run the script manually before relying on automation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./script.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. Use staging mode first&lt;/strong&gt;&lt;br&gt;
If you’re testing renewal logic:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;certbot renew --dry-run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. Monitor logs&lt;/strong&gt;&lt;br&gt;
Check your log file occasionally:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cat /var/log/ssl_renewal.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;4. Set up alerts (optional)&lt;/strong&gt;&lt;br&gt;
For production systems, consider email alerts if renewal fails.&lt;/p&gt;
&lt;h3 id=&quot;common-issues-to-watch-for&quot;&gt;Common Issues to Watch For&lt;/h3&gt;
&lt;p&gt;Even with automation, things can go wrong. Here are a few common problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Port 80/443 blocked&lt;/strong&gt;: Certbot needs access for validation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Incorrect domain configuration&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server reload failures due to config errors&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If renewal fails, the logs will usually point you in the right direction.&lt;/p&gt;
&lt;h3 id=&quot;seo-benefits-of-ssl-yes-it-matters-&quot;&gt;SEO Benefits of SSL (Yes, It Matters)&lt;/h3&gt;
&lt;p&gt;Beyond security, SSL certificates also impact your search rankings from the eyes of a developer.&lt;/p&gt;
&lt;p&gt;Google uses HTTPS as a ranking signal. Sites without SSL may:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rank lower in search results&lt;/li&gt;
&lt;li&gt;Show &quot;Not Secure&quot; warnings in browsers&lt;/li&gt;
&lt;li&gt;Lose user trust&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Automating SSL renewal helps maintain both your security and your SEO performance.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;Automating SSL certificate renewal with a Bash script is one of those small improvements that pays off constantly. It saves time, prevents downtime, and keeps your site secure without ongoing effort.&lt;/p&gt;
&lt;p&gt;Once you set it up, you can forget about expiration dates and focus on what actually matters, like building and growing your site.&lt;/p&gt;
&lt;p&gt;If you manage multiple servers or websites, this approach becomes even more valuable.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If you found this helpful, consider sharing it with someone who manages servers or websites. It might save them from their next &quot;certificate expired&quot; panic.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/13256/bash-script-to-automatically-renew-and-reload-ssl-certificates</guid>
<pubDate>Fri, 20 Mar 2026 10:34:54 +0000</pubDate>
</item>
<item>
<title>How to Create a CLI Tool Using Bash: A Practical Guide for Developers</title>
<link>https://coderlegion.com/13137/how-to-create-a-cli-tool-using-bash-a-practical-guide-for-developers</link>
<description>&lt;p&gt;Command line tools are the backbone of productivity for developers, system administrators, and DevOps engineers. From automating repetitive tasks to managing servers efficiently, a well built CLI tool can save hours of manual work.&lt;/p&gt;
&lt;p&gt;If you’ve ever wanted to &lt;strong&gt;build your own CLI tool using Bash&lt;/strong&gt;, this guide will walk you through the process step by step. By the end of this article, you’ll understand how to create a &lt;strong&gt;functional, reusable, and user friendly command line tool&lt;/strong&gt; that you can use in real world projects.&lt;/p&gt;
&lt;p&gt;Whether you're working on &lt;strong&gt;Linux automation, DevOps workflows, or personal productivity tools&lt;/strong&gt;, this tutorial is designed to help you level up.&lt;/p&gt;
&lt;h3 id=&quot;why-build-a-cli-tool-with-bash-&quot;&gt;Why Build a CLI Tool with Bash?&lt;/h3&gt;
&lt;p&gt;Before diving into the code, let’s understand why Bash is still a powerful choice for CLI tools.&lt;/p&gt;
&lt;h4 id=&quot;1-native-to-most-systems&quot;&gt;1. Native to Most Systems&lt;/h4&gt;
&lt;p&gt;Bash comes pre installed on most Unix based systems, including Linux and macOS. This makes your CLI tool &lt;strong&gt;portable and easy to distribute&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id=&quot;2-perfect-for-automation&quot;&gt;2. Perfect for Automation&lt;/h4&gt;
&lt;p&gt;Bash excels at:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;File manipulation&lt;/li&gt;
&lt;li&gt;Process management&lt;/li&gt;
&lt;li&gt;System monitoring&lt;/li&gt;
&lt;li&gt;Task automation&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;3-lightweight-and-fast&quot;&gt;3. Lightweight and Fast&lt;/h4&gt;
&lt;p&gt;Unlike heavier programming environments, Bash scripts are quick to write and execute with minimal setup.&lt;/p&gt;
&lt;h3 id=&quot;what-you-ll-build&quot;&gt;What You’ll Build&lt;/h3&gt;
&lt;p&gt;In this tutorial, we’ll create a simple CLI tool called:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;devtool
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This tool will:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Accept command line arguments&lt;/li&gt;
&lt;li&gt;Support multiple commands (like &lt;code&gt;greet&lt;/code&gt;, &lt;code&gt;cleanup&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Provide a help menu&lt;/li&gt;
&lt;li&gt;Handle errors gracefully&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;step-1-create-your-script-file&quot;&gt;Step 1: Create Your Script File&lt;/h3&gt;
&lt;p&gt;Start by creating a new Bash script:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;touch devtool.sh
chmod +x devtool.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the shebang at the top:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This tells the system to execute the script using Bash.&lt;/p&gt;
&lt;h3 id=&quot;step-2-add-a-basic-structure&quot;&gt;Step 2: Add a Basic Structure&lt;/h3&gt;
&lt;p&gt;A good CLI tool follows a clear structure.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash

# Tool Name: devtool
# Description: A simple CLI tool for automation tasks

show_help() {
    echo &quot;Usage: devtool [command]&quot;
    echo &quot;&quot;
    echo &quot;Commands:&quot;
    echo &quot;  greet        Print a greeting message&quot;
    echo &quot;  cleanup      Remove temporary files&quot;
    echo &quot;  help         Show this help message&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates a &lt;strong&gt;help function&lt;/strong&gt;, which is essential for usability and self documentation.&lt;/p&gt;
&lt;h3 id=&quot;step-3-handle-commands&quot;&gt;Step 3: Handle Commands&lt;/h3&gt;
&lt;p&gt;Now let’s process user input using a &lt;code&gt;case&lt;/code&gt; statement.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;COMMAND=$1

case $COMMAND in
    greet)
        echo &quot;Hello, welcome to devtool!&quot;
        ;;
    cleanup)
        echo &quot;Cleaning up temporary files...&quot;
        rm -rf /tmp/*
        echo &quot;Cleanup completed.&quot;
        ;;
    help|--help|-h)
        show_help
        ;;
    *)
        echo &quot;Unknown command: $COMMAND&quot;
        show_help
        ;;
esac
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;why-this-matters&quot;&gt;Why this matters&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;case&lt;/code&gt; makes your CLI tool &lt;strong&gt;clean and scalable&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Easy to add new commands later&lt;/li&gt;
&lt;li&gt;Improves readability compared to multiple &lt;code&gt;if&lt;/code&gt; statements&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;step-4-accept-arguments&quot;&gt;Step 4: Accept Arguments&lt;/h3&gt;
&lt;p&gt;Let’s make the &lt;code&gt;greet&lt;/code&gt; command more dynamic by accepting a name.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;greet_user() {
    NAME=$1

    if [ -z &quot;$NAME&quot; ]; then
        echo &quot;Please provide a name.&quot;
        exit 1
    fi

    echo &quot;Hello, $NAME! Welcome to devtool.&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update your case block:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;greet)
    greet_user $2
    ;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you can run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./devtool.sh greet Gift
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;step-5-improve-error-handling&quot;&gt;Step 5: Improve Error Handling&lt;/h3&gt;
&lt;p&gt;A professional CLI tool should handle errors gracefully.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;if [ $# -eq 0 ]; then
    echo &quot;No command provided.&quot;
    show_help
    exit 1
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ensures users always know what went wrong.&lt;/p&gt;
&lt;h3 id=&quot;step-6-make-it-globally-accessible&quot;&gt;Step 6: Make It Globally Accessible&lt;/h3&gt;
&lt;p&gt;To use your CLI tool anywhere, move it into a directory in your system’s PATH.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mv devtool.sh /usr/local/bin/devtool
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you can run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;devtool greet Gift
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;from anywhere on your system.&lt;/p&gt;
&lt;h3 id=&quot;step-7-add-colors-for-better-ux-optional-&quot;&gt;Step 7: Add Colors for Better UX (Optional)&lt;/h3&gt;
&lt;p&gt;Make your CLI tool more engaging with colored output.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;GREEN='\033[0;32m'
NC='\033[0m'

echo -e &quot;${GREEN}Operation successful!${NC}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This improves readability, especially for logs and feedback messages.&lt;/p&gt;
&lt;h3 id=&quot;step-8-organize-like-a-real-tool&quot;&gt;Step 8: Organize Like a Real Tool&lt;/h3&gt;
&lt;p&gt;As your CLI tool grows, structure it properly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;devtool/
├── devtool.sh
├── commands/
│   ├── greet.sh
│   └── cleanup.sh
└── utils/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes your project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scalable&lt;/li&gt;
&lt;li&gt;Maintainable&lt;/li&gt;
&lt;li&gt;Easier to collaborate on&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;real-world-use-cases-for-bash-cli-tools&quot;&gt;Real World Use Cases for Bash CLI Tools&lt;/h3&gt;
&lt;p&gt;You can extend your CLI tool to handle real world tasks like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Deploying applications&lt;/li&gt;
&lt;li&gt;Monitoring server health&lt;/li&gt;
&lt;li&gt;Automating backups&lt;/li&gt;
&lt;li&gt;Managing Docker containers&lt;/li&gt;
&lt;li&gt;Running cron jobs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For developers working with &lt;strong&gt;Linux, VPS hosting, or DevOps pipelines&lt;/strong&gt;, Bash CLI tools are incredibly valuable.&lt;/p&gt;
&lt;h3 id=&quot;best-practices-for-building-bash-cli-tools&quot;&gt;Best Practices for Building Bash CLI Tools&lt;/h3&gt;
&lt;p&gt;To ensure your tool is professional and reliable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use meaningful function and variable names&lt;/li&gt;
&lt;li&gt;Always include a help command&lt;/li&gt;
&lt;li&gt;Validate user input&lt;/li&gt;
&lt;li&gt;Avoid hardcoding sensitive values&lt;/li&gt;
&lt;li&gt;Log important actions&lt;/li&gt;
&lt;li&gt;Keep scripts modular&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;final-thoughts-1&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;Creating a CLI tool using Bash is one of the fastest ways to improve your productivity as a developer. With just a few lines of code, you can automate tasks, simplify workflows, and build tools tailored to your needs.&lt;/p&gt;
&lt;p&gt;The beauty of Bash lies in its simplicity. You don’t need complex frameworks or heavy dependencies, just a clear idea and good structure.&lt;/p&gt;
&lt;p&gt;Start small, keep improving, and before long, you’ll have a collection of powerful CLI tools that make your development process faster and more efficient.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;If you found this guide helpful, share it with fellow developers and explore more tutorials on Bash scripting, DevOps, and automation. The more you build, the better you get.&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/13137/how-to-create-a-cli-tool-using-bash-a-practical-guide-for-developers</guid>
<pubDate>Wed, 18 Mar 2026 14:26:07 +0000</pubDate>
</item>
<item>
<title>Timeout Handling in Bash: Preventing Hanging Scripts in Production</title>
<link>https://coderlegion.com/12372/timeout-handling-in-bash-preventing-hanging-scripts-in-production</link>
<description>&lt;p&gt;Nothing is more frustrating than a Bash script that just… hangs.&lt;/p&gt;
&lt;p&gt;No errors. No logs. No output. Just a stuck process quietly consuming resources while your deployment pipeline waits indefinitely.&lt;/p&gt;
&lt;p&gt;If you're a backend developer, DevOps engineer, or system administrator, understanding &lt;strong&gt;timeout handling in Bash&lt;/strong&gt; is essential for writing reliable automation scripts. Whether you're calling external APIs, running database migrations, or executing remote SSH commands, timeouts protect your systems from unpredictable failures.&lt;/p&gt;
&lt;p&gt;In this guide, we’ll explore how to implement proper timeout handling in Bash, why it matters in production environments, and practical techniques you can start using immediately.&lt;/p&gt;
&lt;h3 id=&quot;why-timeout-handling-in-bash-matters&quot;&gt;Why Timeout Handling in Bash Matters&lt;/h3&gt;
&lt;p&gt;Bash scripts often interact with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;External APIs&lt;/li&gt;
&lt;li&gt;Remote servers (via SSH)&lt;/li&gt;
&lt;li&gt;Databases&lt;/li&gt;
&lt;li&gt;File systems&lt;/li&gt;
&lt;li&gt;Long-running processes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Any of these can hang due to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Network latency&lt;/li&gt;
&lt;li&gt;Server overload&lt;/li&gt;
&lt;li&gt;Deadlocks&lt;/li&gt;
&lt;li&gt;Misconfigured services&lt;/li&gt;
&lt;li&gt;Infinite loops&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without proper timeout handling, your automation pipeline can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Block CI/CD deployments&lt;/li&gt;
&lt;li&gt;Exhaust system resources&lt;/li&gt;
&lt;li&gt;Cause cascading failures&lt;/li&gt;
&lt;li&gt;Create silent production incidents&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Production-minded engineers design scripts that fail fast and recover gracefully. That’s where timeouts come in.&lt;/p&gt;
&lt;h3 id=&quot;the-simplest-solution-using-the-timeout-command&quot;&gt;The Simplest Solution: Using the &lt;code&gt;timeout&lt;/code&gt; Command&lt;/h3&gt;
&lt;p&gt;The easiest way to enforce a timeout in Bash is with the built-in &lt;code&gt;timeout&lt;/code&gt; utility (available on most Linux systems).&lt;/p&gt;
&lt;h4 id=&quot;basic-syntax&quot;&gt;Basic Syntax&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;timeout 10s command
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This runs &lt;code&gt;command&lt;/code&gt; and kills it if it exceeds 10 seconds.&lt;/p&gt;
&lt;h4 id=&quot;example-prevent-a-hanging-curl-request&quot;&gt;Example: Prevent a Hanging Curl Request&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;timeout 15s curl https://api.example.com/data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the API doesn’t respond within 15 seconds, the process is terminated.&lt;/p&gt;
&lt;p&gt;This approach is ideal for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;API calls&lt;/li&gt;
&lt;li&gt;Database checks&lt;/li&gt;
&lt;li&gt;Remote commands&lt;/li&gt;
&lt;li&gt;Backup scripts&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;understanding-exit-codes-after-a-timeout&quot;&gt;Understanding Exit Codes After a Timeout&lt;/h3&gt;
&lt;p&gt;When &lt;code&gt;timeout&lt;/code&gt; kills a process, it returns exit code &lt;code&gt;124&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You can detect this in your script:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;timeout 5s some_command
if [ $? -eq 124 ]; then
  echo &quot;Command timed out.&quot;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This allows you to log, retry, or fail gracefully instead of crashing silently.&lt;/p&gt;
&lt;h3 id=&quot;adding-graceful-termination-with-signals&quot;&gt;Adding Graceful Termination with Signals&lt;/h3&gt;
&lt;p&gt;By default, &lt;code&gt;timeout&lt;/code&gt; sends a &lt;code&gt;SIGTERM&lt;/code&gt; signal. You can control this behavior.&lt;/p&gt;
&lt;h4 id=&quot;example&quot;&gt;Example&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;timeout --signal=SIGKILL 5s command
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This forcefully kills the process if needed.&lt;/p&gt;
&lt;p&gt;For production environments, it’s often better to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Send &lt;code&gt;SIGTERM&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Allow cleanup&lt;/li&gt;
&lt;li&gt;Escalate to &lt;code&gt;SIGKILL&lt;/code&gt; if necessary&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;timeout --kill-after=5s 10s command
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives the command 10 seconds, and if it doesn’t exit cleanly within 5 additional seconds, it gets forcefully terminated.&lt;/p&gt;
&lt;h3 id=&quot;timeout-handling-without-the-timeout-command&quot;&gt;Timeout Handling Without the &lt;code&gt;timeout&lt;/code&gt; Command&lt;/h3&gt;
&lt;p&gt;In minimal environments (like some containers), &lt;code&gt;timeout&lt;/code&gt; may not be available.&lt;/p&gt;
&lt;p&gt;You can implement a manual timeout using background processes.&lt;/p&gt;
&lt;h4 id=&quot;example-custom-timeout-pattern&quot;&gt;Example: Custom Timeout Pattern&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash

long_running_task &amp;amp;
pid=$!

(
  sleep 10
  kill $pid 2&amp;gt;/dev/null
) &amp;amp;

wait $pid
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This runs a task and kills it after 10 seconds.&lt;/p&gt;
&lt;p&gt;While this method works, it’s more complex and less readable than using the &lt;code&gt;timeout&lt;/code&gt; utility.&lt;/p&gt;
&lt;h3 id=&quot;setting-timeouts-for-network-commands&quot;&gt;Setting Timeouts for Network Commands&lt;/h3&gt;
&lt;p&gt;Many network tools support native timeout flags.&lt;/p&gt;
&lt;h4 id=&quot;curl-timeout-example&quot;&gt;Curl Timeout Example&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl --max-time 10 https://example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;ssh-timeout-example&quot;&gt;SSH Timeout Example&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh -o ConnectTimeout=10 user@host
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Whenever possible, use native timeout options instead of wrapping commands externally. It gives you finer control.&lt;/p&gt;
&lt;h3 id=&quot;combining-timeouts-with-retries&quot;&gt;Combining Timeouts with Retries&lt;/h3&gt;
&lt;p&gt;Timeouts are powerful but retries make them resilient.&lt;/p&gt;
&lt;p&gt;Example retry logic:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;for i in {1..3}; do
  timeout 5s curl https://api.example.com &amp;amp;&amp;amp; break
  echo &quot;Retry $i failed...&quot;
  sleep 2
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This prevents one temporary failure from breaking your entire pipeline.&lt;/p&gt;
&lt;p&gt;In CI/CD workflows, this pattern dramatically improves stability.&lt;/p&gt;
&lt;h3 id=&quot;preventing-infinite-loops-in-bash&quot;&gt;Preventing Infinite Loops in Bash&lt;/h3&gt;
&lt;p&gt;Sometimes the hang isn’t external it’s your own loop.&lt;/p&gt;
&lt;p&gt;Example of dangerous code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;while true; do
  process_data
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;process_data&lt;/code&gt; never fails, this runs forever.&lt;/p&gt;
&lt;p&gt;Better approach:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;counter=0
while [ $counter -lt 10 ]; do
  process_data
  ((counter++))
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Always add bounds to loops when possible.&lt;/p&gt;
&lt;h3 id=&quot;production-best-practices-for-bash-timeout-handling&quot;&gt;Production Best Practices for Bash Timeout Handling&lt;/h3&gt;
&lt;p&gt;If you're serious about writing reliable automation scripts, follow these guidelines:&lt;/p&gt;
&lt;h4 id=&quot;1-always-set-timeouts-for-external-calls&quot;&gt;1. Always Set Timeouts for External Calls&lt;/h4&gt;
&lt;p&gt;Never trust networks or third-party APIs to respond instantly.&lt;/p&gt;
&lt;h4 id=&quot;2-log-timeout-events&quot;&gt;2. Log Timeout Events&lt;/h4&gt;
&lt;p&gt;Silent failures are dangerous. Log timeouts clearly.&lt;/p&gt;
&lt;h4 id=&quot;3-combine-timeouts-with-exit-code-checks&quot;&gt;3. Combine Timeouts with Exit Code Checks&lt;/h4&gt;
&lt;p&gt;Detect exit code &lt;code&gt;124&lt;/code&gt; and handle it intentionally.&lt;/p&gt;
&lt;h4 id=&quot;4-use-native-tool-timeouts-when-available&quot;&gt;4. Use Native Tool Timeouts When Available&lt;/h4&gt;
&lt;p&gt;Prefer &lt;code&gt;curl --max-time&lt;/code&gt; over wrapping with &lt;code&gt;timeout&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;5-test-scripts-under-failure-conditions&quot;&gt;5. Test Scripts Under Failure Conditions&lt;/h4&gt;
&lt;p&gt;Simulate slow APIs or network drops before deploying.&lt;/p&gt;
&lt;h3 id=&quot;why-this-matters-for-devops-and-backend-developers&quot;&gt;Why This Matters for DevOps and Backend Developers&lt;/h3&gt;
&lt;p&gt;Modern infrastructure is distributed.&lt;/p&gt;
&lt;p&gt;Your Bash scripts may interact with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Microservices&lt;/li&gt;
&lt;li&gt;Cloud APIs&lt;/li&gt;
&lt;li&gt;Docker containers&lt;/li&gt;
&lt;li&gt;Remote servers&lt;/li&gt;
&lt;li&gt;CI/CD pipelines&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without proper timeout handling, a single slow dependency can block your entire deployment.&lt;/p&gt;
&lt;p&gt;Timeouts are not optional they’re part of writing production-grade Bash scripts.&lt;/p&gt;
&lt;h2 id=&quot;final-thoughts-fail-fast-recover-smart&quot;&gt;Final Thoughts: Fail Fast, Recover Smart&lt;/h2&gt;
&lt;p&gt;Timeout handling in Bash is not just about killing processes. It’s about building automation that behaves predictably under stress.&lt;/p&gt;
&lt;p&gt;Reliable systems are not the ones that never fail they’re the ones that fail intelligently.&lt;/p&gt;
&lt;p&gt;If you want your deployments to be stable, your monitoring to be meaningful, and your automation to be production-ready, start treating timeouts as a design requirement not an afterthought.&lt;/p&gt;
&lt;p&gt;If you found this helpful, share it with your team or bookmark it for your next deployment review. And if you're interested in more practical guides on Bash scripting, DevOps automation, and production best practices, stay connected for upcoming posts.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/12372/timeout-handling-in-bash-preventing-hanging-scripts-in-production</guid>
<pubDate>Wed, 04 Mar 2026 16:59:26 +0000</pubDate>
</item>
<item>
<title>AI for Writing Better Bash Scripts</title>
<link>https://coderlegion.com/11991/ai-for-writing-better-bash-scripts</link>
<description>&lt;p&gt;Bash scripting has always been one of the most powerful and intimidating tools in a developer’s toolkit. It’s lightweight, fast, and deeply integrated into Linux systems. But let’s be honest: writing clean, safe, and maintainable Bash scripts can be frustrating.&lt;/p&gt;
&lt;p&gt;Subtle syntax rules. Quoting issues. Silent failures. Edge cases you didn’t think about.&lt;/p&gt;
&lt;p&gt;Now, AI is changing the game.&lt;/p&gt;
&lt;p&gt;Used correctly, AI can help developers write &lt;strong&gt;better Bash scripts&lt;/strong&gt;, reduce errors, improve automation workflows, and speed up debugging without sacrificing control or understanding.&lt;/p&gt;
&lt;p&gt;Let’s explore how to use AI effectively for Bash scripting and where you still need human judgment.&lt;/p&gt;
&lt;h3 id=&quot;why-bash-scripts-often-go-wrong&quot;&gt;Why Bash Scripts Often Go Wrong&lt;/h3&gt;
&lt;p&gt;Before we talk about AI, it’s worth acknowledging why Bash can be tricky.&lt;/p&gt;
&lt;p&gt;Common problems include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Improper variable quoting&lt;/li&gt;
&lt;li&gt;Unsafe handling of file paths&lt;/li&gt;
&lt;li&gt;Lack of error checking&lt;/li&gt;
&lt;li&gt;Hardcoded values&lt;/li&gt;
&lt;li&gt;Silent command failures&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Unlike higher level languages, Bash doesn’t always protect you from subtle mistakes. A missing quote or unchecked exit code can cause unexpected behavior in production environments.&lt;/p&gt;
&lt;p&gt;This is where AI assistance becomes valuable.&lt;/p&gt;
&lt;h3 id=&quot;1-ai-as-a-bash-syntax-assistant&quot;&gt;1. AI as a Bash Syntax Assistant&lt;/h3&gt;
&lt;p&gt;One of the fastest wins is using AI to generate or refine shell commands.&lt;/p&gt;
&lt;p&gt;Instead of manually searching documentation, you can ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Write a Bash script to rotate logs older than 7 days.”&lt;/li&gt;
&lt;li&gt;“How do I safely iterate over files with spaces in their names?”&lt;/li&gt;
&lt;li&gt;“Create a script that checks disk usage and sends a notification if above 80%.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AI can produce structured examples instantly, saving time and reducing friction.&lt;/p&gt;
&lt;p&gt;But the key is review. Always read and understand what the script does before running it.&lt;/p&gt;
&lt;h3 id=&quot;2-improving-script-safety-with-ai&quot;&gt;2. Improving Script Safety with AI&lt;/h3&gt;
&lt;p&gt;AI can help identify risky patterns in existing scripts.&lt;/p&gt;
&lt;p&gt;For example, you can paste your script and ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Is this script safe for file names with spaces?”&lt;/li&gt;
&lt;li&gt;“Where could this fail silently?”&lt;/li&gt;
&lt;li&gt;“How can I improve error handling?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Often, AI will suggest improvements like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Adding &lt;code&gt;set  euo pipefail&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Validating input arguments&lt;/li&gt;
&lt;li&gt;Checking exit codes&lt;/li&gt;
&lt;li&gt;Using proper quoting&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These small adjustments dramatically improve reliability.&lt;/p&gt;
&lt;h3 id=&quot;3-refactoring-messy-bash-scripts&quot;&gt;3. Refactoring Messy Bash Scripts&lt;/h3&gt;
&lt;p&gt;Over time, Bash scripts grow messy. Hardcoded values creep in. Logic becomes difficult to follow.&lt;/p&gt;
&lt;p&gt;AI can help you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Refactor repeated code into functions&lt;/li&gt;
&lt;li&gt;Improve variable naming&lt;/li&gt;
&lt;li&gt;Add inline comments&lt;/li&gt;
&lt;li&gt;Simplify complex conditionals&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead of rewriting everything manually, you can iteratively improve your scripts with guided suggestions.&lt;/p&gt;
&lt;h3 id=&quot;4-learning-through-explanation&quot;&gt;4. Learning Through Explanation&lt;/h3&gt;
&lt;p&gt;One of the most underrated benefits of AI is explanation.&lt;/p&gt;
&lt;p&gt;If you encounter a confusing Bash one liner like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;find /var/log  type f  mtime +7  exec rm {} \;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can ask AI to explain each part step by step.&lt;/p&gt;
&lt;p&gt;This transforms AI from a code generator into a learning tool helping you understand pipes, redirection, subshells, and process control more deeply.&lt;/p&gt;
&lt;h3 id=&quot;5-generating-automation-workflows-faster&quot;&gt;5. Generating Automation Workflows Faster&lt;/h3&gt;
&lt;p&gt;Bash is often used for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Deployment scripts&lt;/li&gt;
&lt;li&gt;Database backups&lt;/li&gt;
&lt;li&gt;CI/CD steps&lt;/li&gt;
&lt;li&gt;Server health checks&lt;/li&gt;
&lt;li&gt;Cron job tasks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AI can help draft these workflows quickly. You still need to customize and validate them, but the initial structure can be generated in seconds.&lt;/p&gt;
&lt;p&gt;This is especially useful for backend developers managing servers or Docker based deployments.&lt;/p&gt;
&lt;h3 id=&quot;where-ai-can-mislead-you&quot;&gt;Where AI Can Mislead You&lt;/h3&gt;
&lt;p&gt;AI is powerful but not infallible.&lt;/p&gt;
&lt;p&gt;Common pitfalls include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Incorrect assumptions about your environment&lt;/li&gt;
&lt;li&gt;Using non portable commands&lt;/li&gt;
&lt;li&gt;Suggesting unsafe practices&lt;/li&gt;
&lt;li&gt;Ignoring performance implications&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bash interacts directly with the operating system. Running blindly generated scripts without review is risky.&lt;/p&gt;
&lt;p&gt;AI should assist your thinking, not replace it.&lt;/p&gt;
&lt;h3 id=&quot;best-practices-for-using-ai-with-bash&quot;&gt;Best Practices for Using AI with Bash&lt;/h3&gt;
&lt;p&gt;To get the most value:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Be specific in your prompts&lt;/li&gt;
&lt;li&gt;Always test scripts in a safe environment&lt;/li&gt;
&lt;li&gt;Ask for explanations, not just code&lt;/li&gt;
&lt;li&gt;Validate edge cases manually&lt;/li&gt;
&lt;li&gt;Treat AI suggestions like peer input not authority&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When used responsibly, AI becomes a productivity multiplier.&lt;/p&gt;
&lt;h3 id=&quot;ai-bash-a-practical-workflow&quot;&gt;AI + Bash: A Practical Workflow&lt;/h3&gt;
&lt;p&gt;Here’s a simple approach:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Define the problem clearly&lt;/li&gt;
&lt;li&gt;Ask AI for a draft script&lt;/li&gt;
&lt;li&gt;Review and question the output&lt;/li&gt;
&lt;li&gt;Test in a staging or local environment&lt;/li&gt;
&lt;li&gt;Refine iteratively&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This process keeps you in control while benefiting from speed and insight.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts-ai-as-a-bash-co-pilot&quot;&gt;Final Thoughts: AI as a Bash Co Pilot&lt;/h3&gt;
&lt;p&gt;Bash scripting isn’t going away. In fact, as automation grows, it becomes even more important. AI doesn’t eliminate the need to understand Bash it enhances your ability to write cleaner, safer, and more efficient scripts.&lt;/p&gt;
&lt;p&gt;The future isn’t about choosing between AI and traditional skills. It’s about combining them.&lt;/p&gt;
&lt;p&gt;Learn Bash fundamentals. Then use AI to accelerate your growth, reduce mistakes, and automate smarter.&lt;/p&gt;
&lt;p&gt;If this article helped you rethink how you approach Bash scripting, share it with another developer who lives in the terminal. And if you’re interested in practical AI assisted workflows, there’s plenty more to explore.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/11991/ai-for-writing-better-bash-scripts</guid>
<pubDate>Sat, 21 Feb 2026 04:47:26 +0000</pubDate>
</item>
<item>
<title>Bash Scripting in 2026: What Still Matters (and What Doesn’t)</title>
<link>https://coderlegion.com/9807/bash-scripting-in-2026-what-still-matters-and-what-doesnt</link>
<description>&lt;p&gt;Every few years, someone declares Bash “dead.”&lt;/p&gt;
&lt;p&gt;And yet here we are in 2026 still relying on Bash scripts to deploy applications, automate servers, glue systems together, and keep production running at 3 a.m.&lt;/p&gt;
&lt;p&gt;Bash scripting hasn’t disappeared. It has simply &lt;strong&gt;evolved&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In this article, we’ll take an honest look at Bash scripting in 2026: what skills are still essential, what practices are outdated, and how developers can use Bash effectively without overengineering or falling into legacy traps.&lt;/p&gt;
&lt;p&gt;Whether you’re a backend developer, DevOps engineer, or Linux power user, this guide will help you focus on what actually matters today.&lt;/p&gt;
&lt;p&gt; ##&lt;strong&gt;Why Bash Is Still Relevant in 2026&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Despite the rise of modern languages, containers, and automation platforms, Bash remains relevant for one simple reason:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Bash lives where your infrastructure lives.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Bash is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Installed by default on almost every Linux system&lt;/li&gt;
&lt;li&gt;The fastest way to automate simple tasks&lt;/li&gt;
&lt;li&gt;Ideal for orchestration, not application logic&lt;/li&gt;
&lt;li&gt;Perfect for glue code between tools&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In 2026, Bash isn’t competing with Python, Go, or Rust, it’s &lt;strong&gt;complementing&lt;/strong&gt; them.&lt;/p&gt;
&lt;h2 id=&quot;what-still-matters-in-bash-scripting&quot;&gt;&lt;strong&gt;What Still Matters in Bash Scripting&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Let’s start with the skills and practices that are still absolutely worth mastering.&lt;/p&gt;
&lt;h3 id=&quot;1-writing-defensive-predictable-scripts&quot;&gt;&lt;strong&gt;1. Writing Defensive, Predictable Scripts&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;The days of “it ran once, so it’s fine” are long gone.&lt;/p&gt;
&lt;p&gt;In 2026, &lt;strong&gt;defensive Bash scripting&lt;/strong&gt; is non-negotiable.&lt;/p&gt;
&lt;p&gt;What still matters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Handling errors explicitly&lt;/li&gt;
&lt;li&gt;Failing fast when something goes wrong&lt;/li&gt;
&lt;li&gt;Avoiding silent failures&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Developers increasingly rely on Bash for automation in CI/CD pipelines, cron jobs, and system services, places where silent errors are dangerous.&lt;/p&gt;
&lt;h3 id=&quot;2-understanding-exit-codes-and-error-handling&quot;&gt;&lt;strong&gt;2. Understanding Exit Codes and Error Handling&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Exit codes remain the backbone of reliable shell scripting.&lt;/p&gt;
&lt;p&gt;A solid Bash script:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Checks whether commands succeed&lt;/li&gt;
&lt;li&gt;Stops execution when failures occur&lt;/li&gt;
&lt;li&gt;Communicates failure clearly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This matters more than ever in automated environments, where scripts are triggered by other systems, not humans watching a terminal.&lt;/p&gt;
&lt;h3 id=&quot;3-using-bash-for-orchestration-not-business-logic&quot;&gt;&lt;strong&gt;3. Using Bash for Orchestration, Not Business Logic&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;One of the healthiest trends in 2026 is &lt;strong&gt;using Bash for what it’s good at&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Bash still shines at:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Starting and stopping services&lt;/li&gt;
&lt;li&gt;Running scheduled jobs&lt;/li&gt;
&lt;li&gt;Managing files and processes&lt;/li&gt;
&lt;li&gt;Calling APIs or other programs&lt;/li&gt;
&lt;li&gt;Wiring together tools&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It does &lt;em&gt;not&lt;/em&gt; shine at complex data processing, large codebases, or application logic and trust when i say that’s okay.&lt;/p&gt;
&lt;h3 id=&quot;4-readability-and-maintainability&quot;&gt;&lt;strong&gt;4. Readability and Maintainability&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Modern Bash scripts are written for &lt;strong&gt;other humans&lt;/strong&gt;, not just machines.&lt;/p&gt;
&lt;p&gt;What still matters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Clear variable names&lt;/li&gt;
&lt;li&gt;Comments explaining &lt;em&gt;why&lt;/em&gt;, not just &lt;em&gt;what&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Logical structure and spacing&lt;/li&gt;
&lt;li&gt;Predictable input and output&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In teams, Bash scripts are often shared, copied, and modified. Readable scripts save time and prevent mistakes.&lt;/p&gt;
&lt;h3 id=&quot;5-bash-systemd-is-the-new-normal&quot;&gt;&lt;strong&gt;5. Bash + systemd Is the New Normal&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;In 2026, serious Bash scripts rarely live alone.&lt;/p&gt;
&lt;p&gt;Instead of running scripts with &lt;code&gt;nohup&lt;/code&gt; or &lt;code&gt;screen&lt;/code&gt;, developers increasingly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Wrap scripts as &lt;strong&gt;systemd services&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Use systemd for restarts, logging, and supervision&lt;/li&gt;
&lt;li&gt;Let Bash handle the logic, systemd handle the lifecycle&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This combination has become a best practice for long-running or critical scripts.&lt;/p&gt;
&lt;h3 id=&quot;6-lightweight-automation-still-wins&quot;&gt;&lt;strong&gt;6. Lightweight Automation Still Wins&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Not every problem needs Kubernetes, Terraform, or a full workflow engine.&lt;/p&gt;
&lt;p&gt;Bash still excels at:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Small server automation&lt;/li&gt;
&lt;li&gt;Monitoring checks&lt;/li&gt;
&lt;li&gt;Maintenance tasks&lt;/li&gt;
&lt;li&gt;One-off but repeatable jobs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In fact, many modern DevOps setups intentionally mix &lt;strong&gt;simple Bash scripts&lt;/strong&gt; with more advanced tools to avoid unnecessary complexity.&lt;/p&gt;
&lt;h2 id=&quot;what-doesn-t-matter-anymore-or-matters-less-&quot;&gt;&lt;strong&gt;What Doesn’t Matter Anymore (or Matters Less)&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Now for the harder truth: some Bash habits haven’t aged well.&lt;/p&gt;
&lt;h3 id=&quot;1-overengineering-small-scripts&quot;&gt;&lt;strong&gt;1. Overengineering Small Scripts&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;In the past, it was common to see massive Bash scripts doing everything.&lt;/p&gt;
&lt;p&gt;In 2026, this is a red flag.&lt;/p&gt;
&lt;p&gt;What doesn’t scale:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Thousands of lines in one Bash file&lt;/li&gt;
&lt;li&gt;Deeply nested conditionals&lt;/li&gt;
&lt;li&gt;Complex state management&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When scripts grow beyond a certain point, they should be rewritten or offloaded to a more suitable language.&lt;/p&gt;
&lt;h3 id=&quot;2-ignoring-portability-concerns&quot;&gt;&lt;strong&gt;2. Ignoring Portability Concerns&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Assuming every system behaves exactly the same is no longer safe.&lt;/p&gt;
&lt;p&gt;What matters less now:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hardcoding paths&lt;/li&gt;
&lt;li&gt;Assuming GNU tools everywhere&lt;/li&gt;
&lt;li&gt;Ignoring shell compatibility&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Modern environments include containers, minimal OS images, and cloud platforms. Bash scripts must be more intentional about where and how they run.&lt;/p&gt;
&lt;h3 id=&quot;3-silent-failures-and-best-effort-scripts&quot;&gt;&lt;strong&gt;3. Silent Failures and “Best Effort” Scripts&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Scripts that fail quietly are no longer acceptable.&lt;/p&gt;
&lt;p&gt;In 2026:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Automation is trusted&lt;/li&gt;
&lt;li&gt;Scripts trigger deployments, alerts, and backups&lt;/li&gt;
&lt;li&gt;Silent failure equals broken systems&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If a script fails, it should &lt;em&gt;say so clearly&lt;/em&gt;, through logs, exit codes, or alerts.&lt;/p&gt;
&lt;h3 id=&quot;4-using-bash-where-it-clearly-doesn-t-fit&quot;&gt;&lt;strong&gt;4. Using Bash Where It Clearly Doesn’t Fit&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Bash is powerful!, but it has limits.&lt;/p&gt;
&lt;p&gt;What matters less:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Complex JSON manipulation without proper tools&lt;/li&gt;
&lt;li&gt;Heavy math or data processing&lt;/li&gt;
&lt;li&gt;Long-running stateful applications&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Using the wrong tool increases maintenance cost and cognitive load.&lt;/p&gt;
&lt;h3 id=&quot;how-bash-fits-into-a-modern-dev-workflow&quot;&gt;&lt;strong&gt;How Bash Fits Into a Modern Dev Workflow&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;The most effective developers in 2026 use Bash intentionally.&lt;/p&gt;
&lt;p&gt;A healthy setup looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bash for automation and orchestration&lt;/li&gt;
&lt;li&gt;Python/Go/Node for complex logic&lt;/li&gt;
&lt;li&gt;systemd for service management&lt;/li&gt;
&lt;li&gt;CI/CD pipelines to run scripts reliably&lt;/li&gt;
&lt;li&gt;Monitoring and alerts to catch failures&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bash isn’t the star, it’s the &lt;strong&gt;connective tissue&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-real-skill-knowing-when-to-use-bash&quot;&gt;&lt;strong&gt;The Real Skill: Knowing When to Use Bash&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;The most valuable Bash skill today isn’t memorizing syntax.&lt;/p&gt;
&lt;p&gt;It’s knowing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When Bash is the right choice&lt;/li&gt;
&lt;li&gt;When it’s time to stop and switch tools&lt;/li&gt;
&lt;li&gt;How to keep scripts small, readable, and reliable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That judgment comes from experience, not trends.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-bash-isn-t-dead-no-it-s-mature&quot;&gt;&lt;strong&gt;Conclusion: Bash Isn’t Dead No, It’s Mature&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Bash scripting in 2026 isn’t about clever one-liners or massive scripts that do everything.&lt;/p&gt;
&lt;p&gt;It’s about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reliability over cleverness&lt;/li&gt;
&lt;li&gt;Simplicity over complexity&lt;/li&gt;
&lt;li&gt;Intentional use over habit&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bash still matters, not because it’s trendy, but because it works. And as long as Linux servers exist, Bash will continue to quietly power the systems we rely on every day.&lt;/p&gt;
&lt;p&gt;If this article resonated with you, share it with another developer who still lives in the terminal. Chances are, they’ll agree: &lt;strong&gt;Bash isn’t going anywhere, it’s just grown up.&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/9807/bash-scripting-in-2026-what-still-matters-and-what-doesnt</guid>
<pubDate>Fri, 16 Jan 2026 06:41:30 +0000</pubDate>
</item>
<item>
<title>Welcome to 2026, Bash Scripting Legends!</title>
<link>https://coderlegion.com/9175/welcome-to-2026-bash-scripting-legends</link>
<description>&lt;p&gt;New year, new opportunities, new scripts to automate our lives.&lt;/p&gt;
&lt;p&gt;To everyone in this Bash Scripting Group — welcome to 2026! This is the year we write cleaner scripts, break fewer servers, automate smarter, and finally stop copy-pasting that same cron job without comments.&lt;/p&gt;
&lt;p&gt;Whether you’re here to:&lt;/p&gt;
&lt;p&gt;master Bash from the basics&lt;/p&gt;
&lt;p&gt;automate Linux and server tasks&lt;/p&gt;
&lt;p&gt;improve DevOps workflows&lt;/p&gt;
&lt;p&gt;or just learn practical scripting that actually gets used&lt;/p&gt;
&lt;p&gt;You’re in the right place.&lt;/p&gt;
&lt;p&gt;Let’s share:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;tips &amp;amp; tricks&lt;/li&gt;
&lt;li&gt;real-world scripts&lt;/li&gt;
&lt;li&gt;mistakes we’ve learned from&lt;/li&gt;
&lt;li&gt;questions (no matter how small)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s to open doors, clean logs, and scripts that just work.&lt;br&gt;
Happy New Year, and happy scripting! &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/9175/welcome-to-2026-bash-scripting-legends</guid>
<pubDate>Thu, 01 Jan 2026 20:25:25 +0000</pubDate>
</item>
<item>
<title>Hello World! Welcome to the Bash Scripting Group!</title>
<link>https://coderlegion.com/8973/hello-world-welcome-to-the-bash-scripting-group</link>
<description>&lt;p&gt;We’re excited to have you here &lt;/p&gt;
&lt;p&gt;This group is a space for learning, sharing, and collaborating around Bash scripting, Linux automation, and command-line productivity.&lt;/p&gt;
&lt;p&gt;Whether you’re just getting started with shell scripts or you’ve been automating servers for years, you’re in the right place.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Start by saying hi!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Introduce yourself in the comments below — tell us:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Your name&lt;/li&gt;
&lt;li&gt;What you use Bash for (or want to learn it for)&lt;/li&gt;
&lt;li&gt;Your OS or environment (Linux, macOS, WSL, servers, etc.)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We’d love to get to know you.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Once you’ve said hi, you can:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Share Bash scripts, tips, or one-liners that save time Ask questions&lt;/li&gt;
&lt;li&gt;about shell scripting, cron jobs, or automation &lt;/li&gt;
&lt;li&gt;Post real-world problems and get help breaking them down &lt;/li&gt;
&lt;li&gt;Comment on and support other members’ posts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Our goal is to make this a practical, beginner-friendly, and respectful space where everyone can grow.&lt;/p&gt;
&lt;p&gt;We’re glad you joined the Bash Scripting Group — looking forward to learning and building together&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/8973/hello-world-welcome-to-the-bash-scripting-group</guid>
<pubDate>Sat, 27 Dec 2025 13:19:45 +0000</pubDate>
</item>
<item>
<title>Understanding Systemctl: How to Create &amp; Manage Your Own Linux Services</title>
<link>https://coderlegion.com/8353/understanding-systemctl-how-to-create-manage-your-own-linux-services</link>
<description>&lt;p&gt;Managing processes manually on Linux can get messy—especially when you’re working on production servers, automation pipelines, or applications that need to run reliably in the background. That’s where &lt;strong&gt;systemctl&lt;/strong&gt; and &lt;strong&gt;systemd services&lt;/strong&gt; come in. With them, you can create fully managed, auto-restarting, long-running background services for any application you’re building.&lt;/p&gt;
&lt;p&gt;In this guide I have broken down &lt;strong&gt;how systemctl works&lt;/strong&gt;, &lt;strong&gt;why systemd services are powerful&lt;/strong&gt;, and &lt;strong&gt;how you can create and manage your own services&lt;/strong&gt; from scratch.&lt;/p&gt;
&lt;p&gt;This topic came to mind recently when I needed to turn a simple script into an always-running background service. Instead of using screen, nohup, or manual commands, systemd gave me a clean, scalable solution—and it reminded me how many developers still underestimate its power.&lt;/p&gt;
&lt;p&gt;Let’s dive in.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-is-systemctl-&quot;&gt;&lt;strong&gt;What is systemctl?&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;systemctl&lt;/code&gt; is the command-line tool used to control and manage &lt;strong&gt;systemd&lt;/strong&gt;, the init system that boots your machine and manages services, logs, and system processes.&lt;/p&gt;
&lt;p&gt;With it you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Start, stop, and restart services&lt;/li&gt;
&lt;li&gt;Enable services to run at boot&lt;/li&gt;
&lt;li&gt;Disable or mask services&lt;/li&gt;
&lt;li&gt;Inspect status and logs&lt;/li&gt;
&lt;li&gt;Create your own custom services&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;systemctl&lt;/code&gt; is the modern replacement for older tools like &lt;code&gt;service&lt;/code&gt;, &lt;code&gt;init.d&lt;/code&gt;, and Upstart.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-systemd-services-are-powerful&quot;&gt;&lt;strong&gt;Why Systemd Services Are Powerful&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Creating your own systemd service allows you to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Run scripts or apps automatically at boot&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Restart on failure&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run programs in the background cleanly&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Manage long-running apps like bots, APIs, workers, and listeners&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Centralize logs&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Avoid hacks like screen sessions or crontab loops&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re building anything that needs to run constantly—API listeners, queue workers, bots, servers—this is the cleanest way to do it.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;how-to-create-your-own-systemd-service&quot;&gt;&lt;strong&gt;How to Create Your Own Systemd Service&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Let’s walk through it step-by-step.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-create-a-service-file&quot;&gt;&lt;strong&gt;1. Create a Service File&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;All services live in:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/etc/systemd/system/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let’s create one:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo nano /etc/systemd/system/myapp.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then add this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;[Unit]
Description=My Custom Linux Service
After=network.target

[Service]
ExecStart=/usr/bin/python3 /home/user/myapp.py
WorkingDirectory=/home/user
Restart=always
User=user
Environment=&quot;ENV=production&quot;

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;what-each-section-means-&quot;&gt;&lt;strong&gt;What each section means:&lt;/strong&gt;&lt;/h4&gt;
&lt;h5 id=&quot;-unit-&quot;&gt;&lt;strong&gt;[Unit]&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;Defines metadata and dependencies.&lt;/p&gt;
&lt;h5 id=&quot;-service-&quot;&gt;&lt;strong&gt;[Service]&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;Defines what is actually run, its user, environment variables, restart behavior, etc.&lt;/p&gt;
&lt;h5 id=&quot;-install-&quot;&gt;&lt;strong&gt;[Install]&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;Defines how the service integrates with system boot.&lt;/p&gt;
&lt;p&gt;This structure works for scripts, Python apps, Node.js apps, compiled binaries, queue workers, etc.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-reload-systemd&quot;&gt;&lt;strong&gt;2. Reload Systemd&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Whenever you add or modify a service:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo systemctl daemon-reload
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-start-the-service&quot;&gt;&lt;strong&gt;3. Start the Service&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo systemctl start myapp.service
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-check-the-status&quot;&gt;&lt;strong&gt;4. Check the Status&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo systemctl status myapp.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will show:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PID&lt;/li&gt;
&lt;li&gt;logs&lt;/li&gt;
&lt;li&gt;errors&lt;/li&gt;
&lt;li&gt;active/inactive status&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-enable-the-service-at-boot&quot;&gt;&lt;strong&gt;5. Enable the Service at Boot&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo systemctl enable myapp.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now your app starts automatically after every reboot.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-stop-restart-disable&quot;&gt;&lt;strong&gt;6. Stop / Restart / Disable&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo systemctl stop myapp.service
sudo systemctl restart myapp.service
sudo systemctl disable myapp.service
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;managing-logs-for-your-service&quot;&gt;&lt;strong&gt;Managing Logs for Your Service&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;systemd integrates with the journal for clean, centralized log management.&lt;/p&gt;
&lt;p&gt;View logs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;journalctl -u myapp.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Follow logs live:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;journalctl -u myapp.service -f
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Clear logs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo journalctl --vacuum-time=2d
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;advanced-options-for-production-services&quot;&gt;&lt;strong&gt;Advanced Options for Production Services&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;You can customize services with:&lt;/p&gt;
&lt;h4 id=&quot;-restart-policies&quot;&gt;✔ Restart policies&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;Restart=on-failure
RestartSec=5
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;-resource-limits&quot;&gt;✔ Resource limits&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;LimitNOFILE=10000
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;-environment-files&quot;&gt;✔ Environment files&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;EnvironmentFile=/etc/myapp.env
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;-execstartpre-run-commands-before-the-service-starts-&quot;&gt;✔ ExecStartPre (run commands before the service starts)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;ExecStartPre=/usr/bin/bash /home/user/check.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;-run-as-a-specific-user-or-group&quot;&gt;✔ Run as a specific user or group&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;User=www-data
Group=www-data
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;-timeout-configurations&quot;&gt;✔ Timeout configurations&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;TimeoutStopSec=20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Systemd is extremely flexible—perfect for complex applications and enterprise environments.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;use-cases-for-your-own-services&quot;&gt;&lt;strong&gt;Use Cases for Your Own Services&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Developers typically use systemd services for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Queue workers (Laravel Horizon, custom workers)&lt;/li&gt;
&lt;li&gt;Node.js servers&lt;/li&gt;
&lt;li&gt;Python bots and automation&lt;/li&gt;
&lt;li&gt;Webhook listeners&lt;/li&gt;
&lt;li&gt;Background schedulers&lt;/li&gt;
&lt;li&gt;IoT device controllers&lt;/li&gt;
&lt;li&gt;Backup scripts&lt;/li&gt;
&lt;li&gt;Discord/Telegram bots&lt;/li&gt;
&lt;li&gt;Custom APIs running on VPS&lt;/li&gt;
&lt;li&gt;Docker container launchers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If it needs to run reliably… turn it into a service.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-thoughts-your-system-your-rules&quot;&gt;&lt;strong&gt;Final Thoughts: Your System, Your Rules&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Understanding how &lt;strong&gt;systemctl&lt;/strong&gt; and &lt;strong&gt;systemd services&lt;/strong&gt; work gives you serious power over your Linux environment. Instead of relying on unstable manual processes, you can build reliable, fully-managed background services that survive reboots, restart on failure, and stay organized.&lt;/p&gt;
&lt;p&gt;Whether you’re a DevOps engineer, backend developer, sysadmin, or someone managing VPS deployments, mastering systemd will make your automation cleaner and your applications far more stable.&lt;/p&gt;
&lt;p&gt;And for me, this came to life again when I recently needed to convert a simple script into a background service—it reminded me why systemctl remains one of the most underrated tools in a Linux developer’s toolbox.&lt;/p&gt;
</description>
<category>Shell Scripts</category>
<guid isPermaLink="true">https://coderlegion.com/8353/understanding-systemctl-how-to-create-manage-your-own-linux-services</guid>
<pubDate>Wed, 10 Dec 2025 14:28:20 +0000</pubDate>
</item>
<item>
<title>CloudRay</title>
<link>https://coderlegion.com/5892/cloudray</link>
<description>&lt;h3 id=&quot;the-problem-1&quot;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;Managing Bash scripts across multiple servers is often messy and error-prone. Engineers and sysadmins typically juggle SSH sessions, copy scripts manually, and struggle to keep track of versions, outputs, and schedules. Scaling this across dozens or even hundreds of servers makes the challenge even bigger.&lt;/p&gt;
&lt;h3 id=&quot;the-solution&quot;&gt;The Solution&lt;/h3&gt;
&lt;p&gt;CloudRay provides a single, centralised dashboard to create, manage, and monitor all your Bash scripts. &lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i.postimg.cc/DfHK3Pwh/Screenshot-2025-09-30-at-12-32-55-PM.png&quot; alt=&quot;CloudRay Dashboard&quot;&gt;&lt;/p&gt;
&lt;p&gt;You can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Write and edit scripts directly in the CloudRay dashboard&lt;/li&gt;
&lt;li&gt;Run them on one or many servers simultaneously&lt;/li&gt;
&lt;li&gt;View logs and outputs in real-time&lt;/li&gt;
&lt;li&gt;Automate execution with Schedules &lt;a rel=&quot;nofollow&quot; href=&quot;https://cloudray.io/docs/schedules&quot;&gt;learn more&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With CloudRay, Bash script automation becomes simple, reliable, and observable, whether for one server or a fleet.&lt;/p&gt;
&lt;h3 id=&quot;how-it-works&quot;&gt;How It Works&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Install the lightweight CloudRay Agent on your servers (&lt;a rel=&quot;nofollow&quot; href=&quot;https://cloudray.io/docs/agent&quot;&gt;docs here&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Connect your servers to CloudRay in just a few steps&lt;/li&gt;
&lt;li&gt;Manage, run, and schedule Bash scripts from a centralized dashboard&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This setup eliminates repetitive manual work while giving you full visibility into your automation.&lt;/p&gt;
&lt;h3 id=&quot;try-it-out&quot;&gt;Try It Out&lt;/h3&gt;
&lt;p&gt; Visit &lt;a rel=&quot;nofollow&quot; href=&quot;https://cloudray.io/&quot;&gt;CloudRay&lt;/a&gt; to get started today.&lt;/p&gt;
&lt;h3 id=&quot;get-involved&quot;&gt;Get Involved&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;http://github.com/cloudray-io&quot;&gt;github.com/cloudray-io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Feedback:&lt;/strong&gt; Send us your thoughts, ideas, or issues at &lt;em&gt;Emails are not allowed&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>SaaS</category>
<guid isPermaLink="true">https://coderlegion.com/5892/cloudray</guid>
<pubDate>Thu, 02 Oct 2025 10:32:14 +0000</pubDate>
</item>
</channel>
</rss>