<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>scripting · wleeaf field notes</title><link>https://articles.wleeaf.dev/tags/scripting/</link><description>Hands-on guides and build logs from the wleeaf studio: self-hosting, Linux, networking, Docker, the command line and reverse engineering.</description><language>en-us</language><lastBuildDate>Wed, 25 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://articles.wleeaf.dev/tags/scripting/feed.xml" rel="self" type="application/rss+xml"/><item><title>Bash Traps and the Art of Not Leaving a Mess</title><link>https://articles.wleeaf.dev/posts/bash-traps-error-handling/</link><pubDate>Wed, 25 Mar 2026 00:00:00 +0000</pubDate><guid>https://articles.wleeaf.dev/posts/bash-traps-error-handling/</guid><description>How trap, set -e, and a cleanup function can save you from half-finished deployments.</description><category>bash</category><category>linux</category><category>scripting</category><content:encoded><![CDATA[<p>I once ran a deploy script that got killed halfway through an rsync. The remote server had half the files from the new version and half from the old one. The site was broken for twenty minutes while I figured out what happened.</p>
<p>That is when I started using traps. <code>trap cleanup EXIT</code> runs a cleanup function however the script ends: a normal exit, an error or a Ctrl-C.</p>
<h2 id="the-problem">The Problem<a class="anchor" href="#the-problem" aria-label="Link to “The Problem”">#</a></h2>
<p>Bash scripts are fragile by default. If a command fails, the script keeps going. If you hit Ctrl-C, whatever state you left things in is where they stay. If your SSH connection drops mid-script, same thing.</p>
<p>Most people know about <code>set -e</code> which exits on the first error. But that only covers one failure mode. What about cleanup?</p>
<h2 id="enter-trap">Enter trap<a class="anchor" href="#enter-trap" aria-label="Link to “Enter trap”">#</a></h2>
<p>The <code>trap</code> builtin lets you run a command when the script receives a signal or exits:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">TMPDIR</span><span class="o">=</span><span class="k">$(</span>mktemp -d<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">cleanup<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Cleaning up </span><span class="nv">$TMPDIR</span><span class="s2">...&#34;</span>
</span></span><span class="line"><span class="cl">  rm -rf <span class="s2">&#34;</span><span class="nv">$TMPDIR</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">trap</span> cleanup EXIT
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Your actual work here</span>
</span></span><span class="line"><span class="cl">cp important-files <span class="s2">&#34;</span><span class="nv">$TMPDIR</span><span class="s2">/&#34;</span>
</span></span><span class="line"><span class="cl">process <span class="s2">&#34;</span><span class="nv">$TMPDIR</span><span class="s2">/important-files&#34;</span>
</span></span><span class="line"><span class="cl">mv <span class="s2">&#34;</span><span class="nv">$TMPDIR</span><span class="s2">/important-files&#34;</span> /final/destination/</span></span></code></pre></div>
</div>
<p>The <code>EXIT</code> trap fires no matter how the script ends. Normal exit, error, Ctrl-C, even <code>kill</code>. The temp directory always gets cleaned up.</p>
<h2 id="a-real-deploy-script">A Real Deploy Script<a class="anchor" href="#a-real-deploy-script" aria-label="Link to “A Real Deploy Script”">#</a></h2>
<p>Here is the pattern I use for deployments:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">SERVER</span><span class="o">=</span><span class="s2">&#34;myserver&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">REMOTE_DIR</span><span class="o">=</span><span class="s2">&#34;/opt/app&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">BACKUP_DIR</span><span class="o">=</span><span class="s2">&#34;/opt/app-backup-</span><span class="k">$(</span>date +%s<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">ROLLED_BACK</span><span class="o">=</span><span class="nb">false</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">rollback<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$ROLLED_BACK</span><span class="s2">&#34;</span> <span class="o">=</span> <span class="nb">true</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span> <span class="k">return</span><span class="p">;</span> <span class="k">fi</span>
</span></span><span class="line"><span class="cl">  <span class="nv">ROLLED_BACK</span><span class="o">=</span><span class="nb">true</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Deploy failed. Rolling back...&#34;</span>
</span></span><span class="line"><span class="cl">  ssh <span class="s2">&#34;</span><span class="nv">$SERVER</span><span class="s2">&#34;</span> <span class="s2">&#34;rm -rf </span><span class="nv">$REMOTE_DIR</span><span class="s2"> &amp;&amp; mv </span><span class="nv">$BACKUP_DIR</span><span class="s2"> </span><span class="nv">$REMOTE_DIR</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Rollback complete.&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">trap</span> rollback ERR
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Backup current version</span>
</span></span><span class="line"><span class="cl">ssh <span class="s2">&#34;</span><span class="nv">$SERVER</span><span class="s2">&#34;</span> <span class="s2">&#34;cp -r </span><span class="nv">$REMOTE_DIR</span><span class="s2"> </span><span class="nv">$BACKUP_DIR</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Deploy</span>
</span></span><span class="line"><span class="cl">rsync -a --delete ./dist/ <span class="s2">&#34;</span><span class="nv">$SERVER</span><span class="s2">:</span><span class="nv">$REMOTE_DIR</span><span class="s2">/&#34;</span>
</span></span><span class="line"><span class="cl">ssh <span class="s2">&#34;</span><span class="nv">$SERVER</span><span class="s2">&#34;</span> <span class="s2">&#34;cd </span><span class="nv">$REMOTE_DIR</span><span class="s2"> &amp;&amp; docker compose up -d&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Success - remove backup</span>
</span></span><span class="line"><span class="cl">ssh <span class="s2">&#34;</span><span class="nv">$SERVER</span><span class="s2">&#34;</span> <span class="s2">&#34;rm -rf </span><span class="nv">$BACKUP_DIR</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Deploy complete.&#34;</span></span></span></code></pre></div>
</div>
<p>If anything after the backup fails, the old version gets restored automatically. No manual intervention, no half-broken state.</p>
<h2 id="set--euo-pipefail">set -euo pipefail<a class="anchor" href="#set--euo-pipefail" aria-label="Link to “set -euo pipefail”">#</a></h2>
<p>This trio should be at the top of every script you write:</p>
<ul>
<li><code>set -e</code> exits on any command failure</li>
<li><code>set -u</code> treats unset variables as errors instead of empty strings</li>
<li><code>set -o pipefail</code> makes pipes fail if any command in the chain fails, not just the last one</li>
</ul>
<p>Without <code>pipefail</code>, this silently succeeds even when <code>curl</code> fails:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl https://example.com/data <span class="p">|</span> jq .result
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Done&#34;</span></span></span></code></pre></div>
</div>
<p><code>jq</code> gets an empty input, outputs null, and the script continues. With <code>pipefail</code>, the whole pipe fails and <code>set -e</code> kills the script.</p>
<h2 id="trapping-multiple-signals">Trapping Multiple Signals<a class="anchor" href="#trapping-multiple-signals" aria-label="Link to “Trapping Multiple Signals”">#</a></h2>
<p>You can trap specific signals separately:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">trap</span> <span class="s2">&#34;echo Interrupted; cleanup; exit 130&#34;</span> INT
</span></span><span class="line"><span class="cl"><span class="nb">trap</span> <span class="s2">&#34;echo Terminated; cleanup; exit 143&#34;</span> TERM
</span></span><span class="line"><span class="cl"><span class="nb">trap</span> cleanup EXIT</span></span></code></pre></div>
</div>
<p>But usually just trapping <code>EXIT</code> is enough. It covers everything.</p>
<p>The key insight is that bash does not protect you from yourself. You have to opt into safety. But once you do, scripts become surprisingly reliable.</p>
]]></content:encoded></item></channel></rss>