<?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>AdminJitsu</title>
    <link>https://adminjitsu.com/</link>
    <description>Recent content on AdminJitsu</description>
    <generator>Hugo -- 0.147.0</generator>
    <language>en-us</language>
    <lastBuildDate>Sat, 15 Nov 2025 11:14:32 -0600</lastBuildDate>
    <atom:link href="https://adminjitsu.com/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>nextcube Part 3 - Next Users Guide</title>
      <link>https://adminjitsu.com/posts/next-users-guide/</link>
      <pubDate>Sat, 15 Nov 2025 11:14:32 -0600</pubDate>
      <guid>https://adminjitsu.com/posts/next-users-guide/</guid>
      <description>&lt;h2 id=&#34;intro&#34;&gt;Intro&lt;/h2&gt;
&lt;p&gt;In this third installment we&amp;rsquo;ll do some more NeXT hacking with our nextcube setup and make it more livable.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;In &lt;a href=&#34;https://adminjitsu.com/posts/virtual-nextcube/&#34;&gt;Part 1&lt;/a&gt;, we got OpenStep installed and booting cleanly on VirtualBox.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In &lt;a href=&#34;https://adminjitsu.com/posts/next-config/&#34;&gt;Part 2&lt;/a&gt;, we configured the Unix and network environments, installed development tools, and added some Unix ports.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The complete OVA Appliance build published on &lt;a href=&#34;https://archive.org/details/openstep_ova&#34;&gt;Archive.org&lt;/a&gt; is available if you want to hop right in.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;h2 id=&#34;telnet-refresher&#34;&gt;Telnet Refresher&lt;/h2&gt;
&lt;p&gt;Before SSH and encryption became standard security practices, &lt;strong&gt;Telnet&lt;/strong&gt; was the default way to access remote systems. It uses plain TCP connections and transmits everything — including usernames and passwords — in &lt;strong&gt;plaintext&lt;/strong&gt;. Despite that (or because of it), it was widely used for administration, file transfers, and troubleshooting and testing network services.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<p>In this third installment we&rsquo;ll do some more NeXT hacking with our nextcube setup and make it more livable.</p>
<ul>
<li>
<p>In <a href="/posts/virtual-nextcube/">Part 1</a>, we got OpenStep installed and booting cleanly on VirtualBox.</p>
</li>
<li>
<p>In <a href="/posts/next-config/">Part 2</a>, we configured the Unix and network environments, installed development tools, and added some Unix ports.</p>
</li>
<li>
<p>The complete OVA Appliance build published on <a href="https://archive.org/details/openstep_ova">Archive.org</a> is available if you want to hop right in.</p>
</li>
</ul>
<br>
<h2 id="telnet-refresher">Telnet Refresher</h2>
<p>Before SSH and encryption became standard security practices, <strong>Telnet</strong> was the default way to access remote systems. It uses plain TCP connections and transmits everything — including usernames and passwords — in <strong>plaintext</strong>. Despite that (or because of it), it was widely used for administration, file transfers, and troubleshooting and testing network services.</p>
<p>At its simplest, Telnet connects to a host and port number as a generic TCP client:</p>
<pre tabindex="0"><code>telnet host-or-ip [port-number]
</code></pre><p>Examples:</p>
<pre tabindex="0"><code>telnet nextcube
telnet nextcube 25
telnet 127.0.0.1 9
</code></pre><p>Yes, you can telnet into <code>127.0.0.1</code> or <code>localhost</code> as well as your own IP. This works because the <code>inetd</code> super-server automatically starts <code>telnetd</code> at boot and binds to all available interfaces. The configuration line that enables it is found in <code>/etc/inetd.conf</code>:</p>
<pre tabindex="0"><code>telnet  stream  tcp     nowait  root    /usr/etc/telnetd        telnetd
</code></pre><p>The actual <code>telnetd</code> binary lives in <code>/usr/etc</code>, and is launched by <code>inetd</code> whenever a connection request comes in on TCP port 23.</p>
<p>Telnet’s simplicity makes it an excellent tool for exploring how old network services work. You can use it to:</p>
<ul>
<li>Log in to the OpenStep system from another host.</li>
<li>Connect to local ports for debugging or service checks (<code>telnet localhost 80</code>).</li>
<li>Interact with simple text-based protocols such as SMTP (<code>telnet host 25</code>), POP3 (<code>telnet host 110</code>), or even the built-in services like <code>finger</code>, <code>daytime</code>, or <code>echo</code>.</li>
</ul>
<p>Because Telnet sends everything unencrypted, you can easily visualize what’s happening with a packet capture tool like <strong>tshark</strong> or <strong>Wireshark</strong>. A short capture of a login attempt clearly shows the username and password as ASCII text — a powerful reminder of why SSH replaced it.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <p>Telnet made troubleshooting a lot easier but it transmitted everything, including passwords, in the clear.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo tcpdump -i eth0 -n -s <span class="m">0</span> -w telnet_nextcube.pcap host 192.168.1.50 and port <span class="m">23</span>
</span></span></code></pre></div><p>After telnetting in and exiting normally, the tcpdump session was stopped and examined with <code>tshark</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">┌──<span class="o">[</span> grumble@shinobi <span class="o">]</span>:~
</span></span><span class="line"><span class="cl">└─$ tshark -r telnet_nextcube.pcap -Y <span class="s2">&#34;telnet&#34;</span> -T fields -e frame.number -e telnet.data
</span></span><span class="line"><span class="cl"><span class="m">4</span>
</span></span><span class="line"><span class="cl"><span class="m">6</span>
</span></span><span class="line"><span class="cl"><span class="m">8</span>
</span></span><span class="line"><span class="cl"><span class="m">9</span>
</span></span><span class="line"><span class="cl"><span class="m">11</span>
</span></span><span class="line"><span class="cl"><span class="m">12</span>
</span></span><span class="line"><span class="cl"><span class="m">14</span>
</span></span><span class="line"><span class="cl"><span class="m">15</span>
</span></span><span class="line"><span class="cl"><span class="m">16</span>
</span></span><span class="line"><span class="cl"><span class="m">17</span>
</span></span><span class="line"><span class="cl"><span class="m">18</span>
</span></span><span class="line"><span class="cl"><span class="m">19</span>
</span></span><span class="line"><span class="cl"><span class="m">20</span>
</span></span><span class="line"><span class="cl"><span class="m">21</span>
</span></span><span class="line"><span class="cl"><span class="m">22</span>
</span></span><span class="line"><span class="cl"><span class="m">23</span>      <span class="se">\r\n</span>,<span class="se">\r\n</span>,NeXT Mach <span class="o">(</span>nextcube<span class="o">)</span> <span class="o">(</span>ttyp1<span class="o">)</span><span class="se">\r\n</span>,<span class="se">\r</span>,<span class="se">\r\n</span>,<span class="se">\r</span>
</span></span><span class="line"><span class="cl"><span class="m">24</span>
</span></span><span class="line"><span class="cl"><span class="m">25</span>      login:
</span></span><span class="line"><span class="cl"><span class="m">27</span>      me<span class="se">\r\n</span>
</span></span><span class="line"><span class="cl"><span class="m">28</span>
</span></span><span class="line"><span class="cl"><span class="m">30</span>
</span></span><span class="line"><span class="cl"><span class="m">31</span>      Password:
</span></span><span class="line"><span class="cl"><span class="m">34</span>      password<span class="se">\r\n</span>
</span></span><span class="line"><span class="cl"><span class="m">35</span>      <span class="se">\r\n</span>
</span></span><span class="line"><span class="cl"><span class="m">37</span>
</span></span><span class="line"><span class="cl"><span class="m">39</span>
</span></span><span class="line"><span class="cl"><span class="m">40</span>      Last login: Thu Nov <span class="m">13</span> 15:35:16 from 192.168.1.51<span class="se">\r\n</span>
</span></span><span class="line"><span class="cl"><span class="m">42</span>      <span class="se">\r\n</span>
</span></span><span class="line"><span class="cl"><span class="m">44</span>      <span class="se">\t</span>The Priest<span class="s1">&#39;s grey nimbus in a niche where he dressed discreetly.\r\n
</span></span></span><span class="line"><span class="cl"><span class="s1">46      I will not sleep here tonight. Home also I cannot go.\r\n,\tA voice, sweetened and sustained, called to him from the sea.\r\n,Turning the curve he waved his hand.  A sleek brown head, a seal&#39;</span>s, far<span class="se">\r\n</span>,out on the water, round.  Usurper.<span class="se">\r\n</span>,<span class="se">\t\t</span>-- James Joyce, <span class="s2">&#34;Ulysses&#34;</span><span class="se">\r\n</span>,<span class="se">\r\n</span>
</span></span><span class="line"><span class="cl"><span class="m">48</span>      ����
</span></span><span class="line"><span class="cl"><span class="m">50</span>      ��
</span></span><span class="line"><span class="cl"><span class="m">51</span>      <span class="o">[</span> me@nextcube <span class="o">]</span>:~ $
</span></span><span class="line"><span class="cl"><span class="m">53</span>
</span></span></code></pre></div><p>So keep that in mind. OpenSTEP was built for a kinder, gentler network environment.</p>
<hr>
<p>This makes for a striking visual demonstration in Wireshark: open the <code>.pcap</code> and follow <strong>Telnet → Follow TCP Stream</strong> — you’ll see your username and password scroll past in red and blue plaintext.</p>

  </div>
</details>

<p>If you want to know more, you can take a look at the following:</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <blockquote>
<h4 id="-telnetd-openstep-42-reference">🧠 Telnetd (OpenStep 4.2) Reference</h4>
<p><strong>Path:</strong> <code>/usr/etc/telnetd</code><br>
<strong>Managed by:</strong> <code>inetd(8)</code> via <code>/etc/inetd.conf</code><br>
<strong>Service mapping:</strong> <code>telnet 23/tcp</code> in <code>/etc/services</code></p>
<p><strong>Usage:</strong></p>
<pre tabindex="0"><code>/usr/etc/telnetd [-debug] [-h] [-l] [port]
</code></pre><ul>
<li><code>-debug</code> — run in foreground, print diagnostic output</li>
<li><code>-h</code> — <em>(listed, but nonfunctional — legacy stub option)</em></li>
<li><code>-l</code> — disable reverse hostname lookup on incoming connections</li>
<li><code>port</code> — optional alternate TCP port (for testing)</li>
</ul>
<p><strong>Version:</strong> NeXT <code>telnetd</code> 5.65 (NeXT 1.0) — 4.3BSD–Net/2 derivative<br>
(Built Jan 26 1999, same toolchain as OpenStep 4.2 Developer CD)</p>
<p><strong>Documentation:</strong></p>
<ul>
<li><code>man 8 telnetd</code> (local, short BSD synopsis)</li>
<li><code>man 5 inetd.conf</code> (for service control)</li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc854">RFC 854 – Telnet Protocol Specification</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc855">RFC 855 – Telnet Option Specifications</a></li>
<li><a href="https://www.nic.funet.fi/pub/OS/4.3bsd/reno/usr.bin/telnet/">Telnet client source</a></li>
<li><a href="https://www.nic.funet.fi/pub/OS/4.3bsd/reno/libexec/telnetd/">Telnetd source</a></li>
</ul>
<p><strong>Notes:</strong></p>
<ul>
<li>No encryption; credentials and session data transmitted in plaintext.</li>
<li>Typically launched by <code>inetd</code>, not standalone.</li>
<li>Uses <code>/usr/etc/login</code> for authentication, <code>/etc/motd</code> for post-login message.</li>
<li>Logs via <code>syslogd</code> (facility <code>daemon.notice</code>) if configured.</li>
<li>Relies on BSD <code>getty</code> and <code>tty</code> infrastructure for pseudo-terminals.</li>
<li>Safe to use only on an isolated or NATed virtual network.</li>
</ul></blockquote>

  </div>
</details>

<br> 
<h2 id="working-with-files">Working with Files</h2>
<p>Next up, let&rsquo;s explore how to work with files on nextcube. Since we don&rsquo;t have the benefit of VirtualBox Shared Folders, we&rsquo;ll start by looking at ftp.</p>
<h3 id="ftp-refresher">FTP refresher</h3>
<p>OpenSTEP runs ftpd out of the box thanks to this line in <code>/etc/inetd.conf</code></p>
<pre tabindex="0"><code>ftp     stream  tcp     nowait  root    /usr/etc/ftpd           ftpd
</code></pre><p>That makes it easy to transfer files between your VirtualBox host and the nextcube guest. Again I would suggest a nice modern ftp client like FileZilla.</p>
<p>You can connect to another host directly from OpenStep using the BSD client:</p>
<pre tabindex="0"><code>/usr/ucb/ftp hostname
</code></pre><h4 id="remote-operations">Remote operations</h4>
<ul>
<li><code>open host [port]</code> — connect to a remote FTP server</li>
<li><code>user name [password]</code> — authenticate manually</li>
<li><code>cd dirname</code> — change remote directory</li>
<li><code>cdup</code> — go up one directory</li>
<li><code>pwd</code> — print current remote directory</li>
<li><code>ls [dir]</code> — list files on the remote side</li>
<li><code>dir [dir]</code> — long listing (like <code>ls -l</code>)</li>
<li><code>get remote [local]</code> — download a file</li>
<li><code>put local [remote]</code> — upload a file</li>
<li><code>mget pattern</code> — download multiple files (with <code>prompt</code> off)</li>
<li><code>mput pattern</code> — upload multiple files</li>
<li><code>delete file</code> — remove a file on the remote system</li>
<li><code>rmdir dirname</code> — remove a directory</li>
<li><code>mkdir dirname</code> — create a directory</li>
<li><code>rename old new</code> — rename a remote file</li>
<li><code>status</code> — show current connection and mode</li>
<li><code>close</code> or <code>bye</code> — end the session</li>
</ul>
<hr>
<h4 id="local-operations">Local operations</h4>
<ul>
<li><code>lcd dirname</code> — change local working directory</li>
<li><code>lpwd</code> — print local working directory</li>
<li><code>!command</code> — run a shell command locally (e.g. <code>!ls</code>, <code>!cat file</code>)</li>
<li><code>!</code> — open an interactive shell temporarily</li>
<li><code>ls</code> with no <code>!</code> — still shows remote files; prefix with <code>!</code> for local</li>
</ul>
<hr>
<h4 id="transfer-mode">Transfer mode</h4>
<ul>
<li><code>ascii</code> — convert text line endings; <strong>use only for .txt or config files</strong></li>
<li><code>binary</code> — raw byte transfer; <strong>always use for .app, .tar, .gz, .iso, etc.</strong></li>
</ul>
<p>To switch:</p>
<pre tabindex="0"><code>ftp&gt; binary
ftp&gt; get archive.tar.gz
</code></pre><hr>
<h4 id="example-fetching-the-ftp-specification-rfc-959">Example: Fetching the FTP Specification (RFC 959)</h4>
<p>You can use OpenStep’s native <code>/usr/ucb/ftp</code> client to retrieve files directly from ftp servers like <strong>RFC Editor</strong> FTP archive.</p>
<p>Lets fetch <strong>RFC 959</strong>, the original <em>File Transfer Protocol</em> specification so we have a local copy:</p>
<pre tabindex="0"><code>[ me@nextcube ]:~ $ ftp ftp.rfc-editor.org
Connected to rsync.rfc-editor.org.
220 &#34;FTP Server Ready&#34;
Name (ftp.rfc-editor.org:me): anonymous
331 Please specify the password.
Password:
230 Login successful.
ftp&gt; cd in-notes
250 Directory successfully changed.
ftp&gt; get rfc959.txt
200 PORT command successful. Consider using PASV.
150 Opening ASCII mode data connection for rfc959.txt (147316 bytes).
226 Transfer complete.
local: rfc959.txt remote: rfc959.txt
151249 bytes received in 1.59 seconds (92.83 Kbytes/s)
ftp&gt; bye
221 Goodbye.
</code></pre><p>Verify the download:</p>
<pre tabindex="0"><code>[ me@nextcube ]:~ $ more rfc959.txt
Network Working Group                                          J. Postel
Request for Comments: 959                                    J. Reynolds
                                                                     ISI
Obsoletes RFC: 765 (IEN 149)                                October 1985

                      FILE TRANSFER PROTOCOL (FTP)
</code></pre><p><strong>Notes</strong></p>
<ul>
<li>The client uses <code>/usr/ucb/ftp</code>, while the server runs <code>/usr/etc/ftpd</code>.</li>
<li>All credentials and data are unencrypted.</li>
<li>Use binary mode for any non-text file to avoid corruption.</li>
</ul>
<p><strong>FUN FACT:</strong><br>
<code>ftp</code> and many other OpenStep binaries live in <code>/usr/ucb</code> — named after the <em>University of California, Berkeley</em>, origin of BSD UNIX. Sun Solaris also kept a <code>ucb/</code> directory for legacy BSD compatibility.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <blockquote>
<h4 id="-ftpd-openstep-42-reference">🧠 Ftpd (OpenStep 4.2) Reference</h4>
<p><strong>Path:</strong> <code>/usr/etc/ftpd</code><br>
<strong>Managed by:</strong> <code>inetd(8)</code> via <code>/etc/inetd.conf</code><br>
<strong>Service mapping:</strong> <code>ftp 21/tcp</code> in <code>/etc/services</code></p>
<p><strong>Usage:</strong></p>
<pre tabindex="0"><code>/usr/etc/ftpd [ -d ] [ -l ] [ -t timeout ] [ -u umask ] [ -T maxtimeout ]
</code></pre><ul>
<li><code>-d</code> — debug mode (log each command)</li>
<li><code>-l</code> — log each FTP session via <code>syslog</code></li>
<li><code>-t</code> — set idle timeout in seconds</li>
<li><code>-u</code> — set default file-creation umask</li>
<li><code>-T</code> — set maximum timeout</li>
</ul>
<p><strong>Version:</strong><br>
<code>@(#)PROGRAM:ftpd  PROJECT:etc-113.2  BUILT:Tue Jan 26 17:59:07 PST 1999</code><br>
Based on <strong>4.3BSD ftpd 5.28 (4/20/89)</strong> — same lineage as BSD-Reno.<br>
Implements classic commands: <code>USER</code>, <code>PASS</code>, <code>PORT</code>, <code>PASV</code>, <code>STOR</code>, <code>RETR</code>, <code>LIST</code>, <code>CHMOD</code>, <code>UMASK</code>, <code>IDLE</code>, etc.</p>
<p><strong>Documentation:</strong></p>
<ul>
<li><code>man 8 ftpd</code> (local)</li>
<li><code>man 5 ftpusers</code>, <code>man 5 services</code>, <code>man 5 inetd.conf</code></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc959">RFC 959 – File Transfer Protocol Specification</a></li>
</ul>
<p><strong>Notes:</strong></p>
<ul>
<li>No encryption; credentials and data channels are plaintext.</li>
<li>Supports anonymous logins (<code>ftpusers</code> restricts disallowed accounts).</li>
<li>Logs sessions to <code>/usr/adm/wtmp</code> and syslog.</li>
<li>Uses <code>/bin/sh</code> or <code>/bin/csh</code> for shell commands (<code>LIST</code>, <code>NLST</code>).</li>
<li>Consult <code>/etc/ftpusers</code> to deny system accounts (root, daemon, etc.).</li>
<li>Safe only on isolated or NATed networks.</li>
</ul>
<p><strong>Related files:</strong></p>
<pre tabindex="0"><code>/etc/inetd.conf     # service entry
/etc/ftpusers       # disallowed accounts
/etc/shells         # valid shells
/usr/adm/wtmp       # login accounting
</code></pre><p><strong>Source lineage (SCCS tags in binary):</strong></p>
<pre tabindex="0"><code>ftpcmd.y     5.20  (Berkeley)  2/28/89
ftpd.c       5.28  (Berkeley)  4/20/89
glob.c       5.7   (Berkeley) 12/14/88
logwtmp.c    5.5   (Berkeley)  4/2/89
popen.c      5.7   (Berkeley)  2/14/89
</code></pre><p><span class="tag orange"><strong>Fun fact:</strong></span>
You can telnet directly to port 21 and type FTP commands by hand to watch the RFC 959 dialogue — a great demo of the protocol’s simplicity.</p></blockquote>

  </div>
</details>

<br> 
<h3 id="network-file-system-nfs">Network File System (NFS)</h3>
<p>OpenStep includes a complete BSD-style NFSv2 implementation. File systems can be shared directly from the command line with <code>/usr/etc/exportfs</code> or through the graphical <strong>NFSManager.app</strong> located at <code>/NextAdmin/NFSManager.app</code>.<br>
The GUI stores its configuration in NetInfo under <code>/exports</code> and <code>/mounts</code>, applying it immediately with <code>exportfs</code>. (See <em>System Administration, Chapter 9: “Setting Up the Network File System”</em> in the <strong>Librarian</strong> app for the original instructions.)</p>
<p>It&rsquo;s not really useful in a modern network which is a shame since it has a really nice GUI and good documentation. I was hoping to make a side-channel VirtualBox shared folder.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="next-NFS.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    OpenSTEP has a nice gui for managing NFS 
  </figcaption>
</figure>
<hr>
<h4 id="scanning-the-nfs-service">Scanning the NFS Service</h4>
<p>After creating an NFS export on nextcube, we are unable to connect from Linux.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">┌──<span class="o">[</span> grumble@shinobi <span class="o">]</span>:~/codelab/NeXT
</span></span><span class="line"><span class="cl">└─$ mount -t nfs nextcube:/Public ./Public/
</span></span><span class="line"><span class="cl">mount.nfs: failed to apply fstab options
</span></span></code></pre></div><p>From our remote machine, <code>nmap</code> reveals the following:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">┌──<span class="o">[</span> grumble@shinobi <span class="o">]</span>:~/codelab/NeXT
</span></span><span class="line"><span class="cl">└─$ sudo nmap -sU -sT -p 111,2049 --script nfs-ls,nfs-showmount,nfs-statfs nextcube
</span></span><span class="line"><span class="cl">Starting Nmap 7.80 <span class="o">(</span> https://nmap.org <span class="o">)</span> at 2025-11-15 15:15 CST
</span></span><span class="line"><span class="cl">Nmap scan report <span class="k">for</span> nextcube <span class="o">(</span>192.168.1.50<span class="o">)</span>
</span></span><span class="line"><span class="cl">Host is up <span class="o">(</span>0.12s latency<span class="o">)</span>.
</span></span><span class="line"><span class="cl">rDNS record <span class="k">for</span> 192.168.1.50: nextcube.darkstar.home
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">PORT     STATE  SERVICE
</span></span><span class="line"><span class="cl">111/tcp  open   rpcbind
</span></span><span class="line"><span class="cl">2049/tcp closed nfs
</span></span><span class="line"><span class="cl">111/udp  open   rpcbind
</span></span><span class="line"><span class="cl">2049/udp open   nfs
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Nmap <span class="k">done</span>: <span class="m">1</span> IP address <span class="o">(</span><span class="m">1</span> host up<span class="o">)</span> scanned in 3.36 seconds
</span></span></code></pre></div><p>Again from our (modern Linux) host, <code>rpcinfo</code> clearly shows which RPC programs are registered on the OpenStep server:</p>
<pre tabindex="0"><code>┌──[ grumble@shinobi ]:~/codelab/NeXT
└─$ rpcinfo -p nextcube
   program vers proto   port  service
    100000    2   tcp    111  portmapper
    100000    2   udp    111  portmapper
 200100001    1   udp    706
 200100001    1   tcp    709
    100026    1   udp    741  bootparam
    100011    1   udp   2585  rquotad
    100001    1   udp   2586  rstatd
    100001    2   udp   2586  rstatd
    100001    3   udp   2586  rstatd
    100002    1   udp   2587  rusersd
    100002    2   udp   2587  rusersd
    100012    1   udp   2589  sprayd
    100008    1   udp   2590  walld
 200100002    1   udp   2592
    100003    2   udp   2049  nfs
    100005    1   udp    659  mountd
    100005    1   tcp    663  mountd
</code></pre><p>Only the <code>mountd</code> service (100005) registers on TCP; the main NFS daemon (100003) provides <strong>version 2 over UDP only</strong>.<br>
Modern Linux and BSD clients, which default to NFSv3 or NFSv4 over TCP, can contact <code>mountd</code> but fail when the file transfer channel negotiates—resulting in the message<br>
<code>mount.nfs: requested NFS version or transport protocol is not supported</code>.</p>
<p>Interestingly, our NFS settings get stored in netinfo:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">su-2.00# nidump exports /
</span></span><span class="line"><span class="cl">/me/Public      -access<span class="o">=</span>shinobi
</span></span></code></pre></div><hr>
<h4 id="compatibility-notes">Compatibility Notes</h4>
<ul>
<li>✅ Two OpenStep or NeXTSTEP systems on the same LAN can mount each other’s exports normally.<br>
Example:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">su
</span></span><span class="line"><span class="cl">mkdir -p /Net/nextcube/Public
</span></span><span class="line"><span class="cl">mount -t nfs nextcube:/Public /Net/nextcube/Public
</span></span></code></pre></div></li>
<li>⚠️ Modern NFS clients cannot mount from OpenStep because they no longer support NFSv2/UDP.</li>
<li>📚 Reference: <a href="https://datatracker.ietf.org/doc/html/rfc1094">RFC 1094 – Network File System Specification</a></li>
</ul>
<p>On modern systems, NFSv2 has been removed from the kernel and would require recompiling in order to talk to NeXT. So move on and use ftp unless you are talking to other vintage OSes.</p>
<br>
<h3 id="the-workspace-manager-file-viewer">The Workspace Manager File Viewer</h3>
<p>The File Viewer in OpenStep is pretty snazzy for the time. It supported common file operations via drag and drop and the Workspace menu.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="next-list-view.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 900px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    The list view is promising, but you'll want to stick with the default icons view for drag and drop simplicity.
  </figcaption>
</figure>
<br>
<h3 id="-sneakernet-isos">👟 Sneakernet ISOs</h3>
<p>One handy technique to transfer files is with a sneakernet ISO image. Just build a small ISO on your host containing files you wish to transfer, attach it in VirtualBox, and copy the files inside NeXT.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="sneaker-net.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 900px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    burning a virtual cd is an easy way to transfer files to NeXT
  </figcaption>
</figure>
<p><strong>On the host (macOS/Linux):</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">genisoimage -r -J -joliet-long -iso-level <span class="m">3</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -V <span class="s2">&#34;SNEAKERNET&#34;</span> -o SNEAKERNET.iso /path/to/files/
</span></span></code></pre></div><ul>
<li><code>-r</code> Rock Ridge (UNIX metadata)</li>
<li><code>-J -joliet-long</code> long filenames</li>
<li><code>-iso-level 3</code> long names/deeper dirs</li>
<li><code>-V &quot;SNEAKERNET&quot;</code> sets the <strong>volume label</strong></li>
<li><code>-o SNEAKERNET.iso</code> output image</li>
</ul>
<p>Attach <code>SNEAKERNET.iso</code> as a <strong>CD-ROM</strong> in VirtualBox.</p>
<p><strong>Inside NeXT:</strong></p>
<ul>
<li>The ISO mounts at <code>/&lt;volume-label&gt;</code>, so here it will be <code>/SNEAKERNET</code>:</li>
<li>Copy the files out:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cp -R /SNEAKERNET/* /me/Downloads/
</span></span></code></pre></div></li>
</ul>
<br> 
<h2 id="a-tour-of-the-filesystem">A Tour of the Filesystem</h2>
<p>OpenStep inherits its BSD roots but layers on NeXT’s own structure for apps, libraries, and documentation — the ancestor of macOS’s <code>/System</code> and <code>/Library</code>.<br>
The root level is tidy: core UNIX paths alongside distinctive NeXT directories like <code>/NextLibrary</code>, <code>/NextApps</code>, and <code>/NextDeveloper</code>.</p>
<h3 id="key-areas">Key Areas</h3>
<p><strong>Binaries</strong></p>
<ul>
<li><code>/bin</code>, <code>/usr/bin</code> — core user utilities</li>
<li><code>/usr/etc</code> — system daemons (<code>ftpd</code>, <code>inetd</code>, <code>sendmail</code>, <code>lpd</code>)</li>
<li><code>/usr/ucb</code> — BSD-compatibility commands</li>
<li><code>/usr/gnu/bin</code> — GNU tools (<code>gcc</code>, <code>make</code>, <code>emacs</code>)</li>
<li><code>/usr/local/bin</code> — local additions<br>
<em>(No <code>/sbin</code>; management tools live in <code>/usr/etc</code>.)</em></li>
</ul>
<p><strong>Configuration</strong></p>
<ul>
<li><code>/etc</code> → <code>/private/etc</code> — symbolic link to real configs</li>
<li><code>/etc/hostconfig</code>, <code>/etc/rc*</code> — system identity and startup scripts</li>
<li><code>/etc/netinfo/local.nidb</code> — NetInfo database (users, hosts, mounts)</li>
<li><code>/etc/fstab</code>, <code>/etc/services</code>, <code>/etc/hosts</code> — familiar UNIX files</li>
</ul>
<p><strong>Logs &amp; Runtime</strong></p>
<ul>
<li><code>/usr/adm</code> → <code>/private/adm</code> — system logs</li>
<li><code>/usr/spool</code> → <code>/private/spool</code> — mail and print queues</li>
<li><code>/tmp</code>, <code>/dev</code> → <code>/private/*</code> — temporary and device files</li>
</ul>
<p><strong>Documentation</strong></p>
<ul>
<li><code>/usr/man</code> → <code>/NextLibrary/Documentation/ManPages</code></li>
<li><code>/NextLibrary/Bookshelves</code> — system and developer manuals</li>
</ul>
<p><strong>Shared Resources</strong></p>
<ul>
<li><code>/NextLibrary/Frameworks</code> — AppKit, Foundation, etc.</li>
<li><code>/NextLibrary/</code> — global assets</li>
</ul>
<p><strong>Development</strong></p>
<ul>
<li><code>/NextDeveloper/Apps</code> — Interface Builder, Project Builder</li>
<li><code>/NextDeveloper/Examples</code>, <code>/NextDeveloper/Headers</code> — source and APIs</li>
<li><code>/NextDeveloper/Source/GNU</code> — compiler and toolchain sources</li>
</ul>
<hr>
<p>NeXT’s filesystem design was clean, logical, and modular — decades ahead of its time.<br>
It separates binaries, configuration, and writable state clearly, and its <code>/Next*</code> hierarchy became the blueprint for macOS’s modern system layout.</p>
<br>
<h2 id="system-documentation">System Documentation</h2>
<p>Time to RTFM. OpenSTEP installs a good bit of documentation in the form of Bookshelf files and man pages. You can open the <code>Librarian.app</code> in /NextApps and the documentation for the OS and the Developer Tools lives in <code>/NextLibrary/Bookshelves</code></p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="next-bookshelf.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Just like the mac, NeXT uses rtf and rtfd bundles
  </figcaption>
</figure>
<p>At the terminal you can use <code>man</code> or search for a topic with <code>apropos</code> like so:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="o">[</span> me@nextcube <span class="o">]</span>:~ $ man -k inetd
</span></span><span class="line"><span class="cl">inetd <span class="o">(</span>8<span class="o">)</span>               - internet <span class="sb">``</span>super<span class="se">\-</span>server<span class="s1">&#39;&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">[</span> me@nextcube <span class="o">]</span>:~ $ apropos inetd
</span></span><span class="line"><span class="cl">inetd <span class="o">(</span>8<span class="o">)</span>               - internet <span class="sb">``</span>super<span class="se">\-</span>server<span class="s1">&#39;&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">[</span> me@nextcube <span class="o">]</span>:~ $ man inetd
</span></span></code></pre></div><h3 id="recommended-reading">Recommended Reading</h3>
<p>The <em>NeXT System Administration Manual (Release 4.0)</em> closed with a “Suggested Reading” appendix.<br>
Repeated here for historical and vintage-reading pleasure, it reflects the technical canon of the era: the books NeXT engineers themselves considered essential. I had most of these at one point!</p>
<p><strong>General UNIX</strong></p>
<ul>
<li><em>The UNIX Programming Environment</em> — Brian W. Kernighan &amp; Rob Pike</li>
<li><em>The C Programming Language, 2nd Ed.</em> — Kernighan &amp; Ritchie</li>
<li><em>UNIX Power Tools</em> — Jerry Peek, Mike Loukides, &amp; Tim O’Reilly</li>
<li><em>sed &amp; awk</em> — Dale Dougherty</li>
<li><em>The Design and Implementation of the 4.3BSD UNIX Operating System</em> — Leffler, McKusick, Karels, &amp; Quarterman</li>
<li><em>Programming Perl</em> — Larry Wall &amp; Randal L. Schwartz</li>
</ul>
<p><strong>System Administration</strong></p>
<ul>
<li><em>UNIX System Administration Handbook</em> — Nemeth, Snyder, &amp; Seebass</li>
<li><em>TCP/IP Network Administration</em> — Craig Hunt</li>
<li><em>Managing UUCP and Usenet</em> — Grace Todino &amp; Tim O’Reilly</li>
</ul>
<p><strong>Networking</strong></p>
<ul>
<li><em>DNS and BIND</em> — Paul Albitz &amp; Cricket Liu</li>
<li><em>Internetworking with TCP/IP (Vols. I–III)</em> — Douglas Comer &amp; David Stevens</li>
<li><em>TCP/IP Illustrated, Vol. 1</em> — W. Richard Stevens</li>
<li><em>UNIX Network Programming</em> — W. Richard Stevens</li>
<li><em>Managing NFS and NIS</em> — Hal Stern</li>
</ul>
<p><strong>Security</strong></p>
<ul>
<li><em>Practical UNIX Security</em> — Simson Garfinkel &amp; Gene Spafford</li>
<li><em>Firewalls and Internet Security</em> — Cheswick &amp; Bellovin</li>
<li><em>The Cuckoo’s Egg</em> — Clifford Stoll</li>
</ul>
<p><strong>Internet</strong></p>
<ul>
<li><em>The Whole Internet User’s Guide &amp; Catalog</em> — Ed Krol</li>
<li><em>Zen and the Art of the Internet</em> — Brendan Kehoe</li>
<li><em>The Internet Companion</em> — Tracy LaQuey &amp; Jeanne Ryer</li>
</ul>
<p><strong>Hardware</strong></p>
<ul>
<li><em>The Winn L. Rosch Hardware Bible, 3rd Edition</em> — Winn L. Rosch</li>
</ul>
<br>
<h2 id="preferences">Preferences</h2>
<figure style="text-align:left; margin: 1em 0;">
  <img src="next-prefs-icon.jpg" 
       alt="Preferences.app icon from NeXTSTEP/OpenStep"
       style="display:block; float:left; margin:0 1em 1em 0; width:200px; height:auto;">
  <figcaption style="font-size:85%; font-weight:normal; color:#666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
<p>The <strong>Preferences.app</strong> in <code>/NextApps/</code> provides a central place to adjust system behavior — window styles, sounds, networking, time, and more — concepts that later evolved into macOS System Preferences.</p>
<p>Most of these options are undocumented but self-explanatory.</p>
<div style="clear:both"></div>
<hr>
<p>Interestingly, the files<br>
<code>/me/.OpenStep/.NeXTdefaults.L</code> <em>(user settings)</em> and<br>
<code>/.NeXT/.NeXTdefaults.L</code> <em>(system-wide settings)</em><br>
are NeXTSTEP/OPENSTEP&rsquo;s early equivalent of macOS <code>.plist</code> files — compact <strong>binary databases</strong> that store preferences and GUI state for both the user and the system.</p>
<p>Each setting inside these files is serialized as key–value pairs used by the Preferences panels and Workspace Manager.</p>
<p>For example, setting the background color and then running strings on these files yields:</p>
<pre tabindex="0"><code>NeXT1BackgroundColor .846690 0.903455 0.837703
</code></pre><p>The OS represents the <strong>RGB components</strong> of the desktop background color as floating-point values between <strong>0.0 and 1.0</strong> (red, green, blue).<br>
Here, <code>(0.85, 0.90, 0.84)</code> corresponds to a light, slightly greenish gray — the color encoding used by NeXT’s <code>NXColor</code> system long before hex color notation became standard.</p>
<hr>
<p>There are plenty of settings available.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="next-time-prefs.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Another familiar preferences pane.
  </figcaption>
</figure>
<figure style="text-align:center; margin: 1em auto;">
  <img src="next-expert-preferences.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Unix expert? oh my.
  </figcaption>
</figure>
<figure style="text-align:center; margin: 1em auto;">
  <img src="next-display-preferences.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    There is no native support for Wallpaper but you can change the background color!
  </figcaption>
</figure>
<figure style="text-align:center; margin: 1em auto;">
  <img src="next-bullfrog.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 900px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    I am delighted everytime my system makes the bullfrog sound! 
  </figcaption>
</figure>
<br>
<h2 id="network-services">Network Services</h2>
<h3 id="inetd">inetd</h3>
<p>The <strong>Internet Super-Server</strong>, <code>inetd</code>, is the master network daemon that spawns dozens of smaller services on demand.<br>
Rather than running separate daemons for each port, OpenStep starts a single lightweight <code>inetd</code> process at boot (from <code>/etc/rc</code>), which listens for incoming TCP and UDP connections defined in <code>/etc/inetd.conf</code>. When a request arrives, <code>inetd</code> looks up the service, forks, and either handles the request internally (for simple services like <code>echo</code>, <code>discard</code>, <code>daytime</code>, or <code>time</code>) or launches the appropriate daemon (<code>telnetd</code>, <code>ftpd</code>, <code>fingerd</code>, etc.).</p>
<p>This model dates back to 4.3BSD and made UNIX systems far more efficient—only the active services consume memory. NeXT’s implementation adds RPC support and retains full compatibility with classic BSD-style configuration.</p>
<p>We can demonstrate their behavior directly from another host with <code>telnet</code> (or <code>nc</code>):</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <br>
<p><strong>echo service (port 7)</strong> — reflects input back to the sender. Useful for testing; dangerous when exposed publicly.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">┌──<span class="o">[</span> grumble@shinobi <span class="o">]</span>:/etc
</span></span><span class="line"><span class="cl">└─$ telnet nextcube <span class="m">7</span>
</span></span><span class="line"><span class="cl">Trying 192.168.1.50...
</span></span><span class="line"><span class="cl">Connected to nextcube.darkstar.home.
</span></span><span class="line"><span class="cl">Escape character is <span class="s1">&#39;^]&#39;</span>.
</span></span><span class="line"><span class="cl"><span class="nb">test</span>
</span></span><span class="line"><span class="cl"><span class="nb">test</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="nb">echo</span> <span class="nb">echo</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="nb">echo</span> <span class="nb">echo</span>
</span></span><span class="line"><span class="cl">^<span class="o">]</span>
</span></span><span class="line"><span class="cl">telnet&gt; quit
</span></span><span class="line"><span class="cl">Connection closed.
</span></span></code></pre></div><hr>
<p><strong>discard service (port 9)</strong> — silently receives and discards all data. This was essentially a network version of /dev/null that could simulate a network connection for tests where the response doesn&rsquo;t matter.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">┌──<span class="o">[</span> grumble@shinobi <span class="o">]</span>:/etc
</span></span><span class="line"><span class="cl">└─$ telnet nextcube <span class="m">9</span>
</span></span><span class="line"><span class="cl">Trying 192.168.1.50...
</span></span><span class="line"><span class="cl">Connected to nextcube.darkstar.home.
</span></span><span class="line"><span class="cl">Escape character is <span class="s1">&#39;^]&#39;</span>.
</span></span><span class="line"><span class="cl">this message will self destruct
</span></span><span class="line"><span class="cl">^<span class="o">]</span>
</span></span><span class="line"><span class="cl">telnet&gt; quit
</span></span><span class="line"><span class="cl">Connection closed.
</span></span></code></pre></div><hr>
<p><strong>daytime service (port 13)</strong> — returns a plain-text timestamp revealing local time, timezone, and uptime.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">┌──<span class="o">[</span> grumble@shinobi <span class="o">]</span>:/etc
</span></span><span class="line"><span class="cl">└─$ telnet nextcube <span class="m">13</span>
</span></span><span class="line"><span class="cl">Trying 192.168.1.50...
</span></span><span class="line"><span class="cl">Connected to nextcube.darkstar.home.
</span></span><span class="line"><span class="cl">Escape character is <span class="s1">&#39;^]&#39;</span>.
</span></span><span class="line"><span class="cl">Thu Nov <span class="m">13</span> 15:16:40 <span class="m">2025</span>
</span></span><span class="line"><span class="cl">Connection closed by foreign host.
</span></span></code></pre></div><hr>
<p><strong>time service (port 37)</strong> — sends a 32-bit binary timestamp (seconds since 1900). Obsolete precursor to NTP.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">┌──<span class="o">[</span> grumble@shinobi <span class="o">]</span>:/etc
</span></span><span class="line"><span class="cl">└─$ telnet nextcube <span class="m">37</span>
</span></span><span class="line"><span class="cl">Trying 192.168.1.50...
</span></span><span class="line"><span class="cl">Connected to nextcube.darkstar.home.
</span></span><span class="line"><span class="cl">Escape character is <span class="s1">&#39;^]&#39;</span>.
</span></span><span class="line"><span class="cl">���AConnection closed by foreign host.
</span></span></code></pre></div><p>This returns a 4 byte binary value that gets rendered as visual gibberish. We can convert it into an actual date (accurate according to nextcube) but we need to jump through a few hoops.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">┌──<span class="o">[</span> grumble@shinobi <span class="o">]</span>:~/codelab/NeXT
</span></span><span class="line"><span class="cl">└─$ <span class="c1"># Perl one-liner (also forces network order)</span>
</span></span><span class="line"><span class="cl">nc -w1 nextcube <span class="m">37</span> <span class="p">|</span> perl -we <span class="s1">&#39;read STDIN, $_, 4 or die; ($n)=unpack(&#34;N&#34;, $_); print scalar gmtime($n-2208988800), &#34;\n&#34;;&#39;</span>
</span></span><span class="line"><span class="cl">Sat Nov <span class="m">15</span> 22:23:42 <span class="m">2025</span>
</span></span></code></pre></div><p>In case your antique perl is rusty, that oneliner takes the 4bytes from port 37 and does the following:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-perl" data-lang="perl"><span class="line"><span class="cl"><span class="nb">read</span> <span class="bp">STDIN</span><span class="p">,</span> <span class="nv">$_</span><span class="p">,</span> <span class="mi">4</span> <span class="ow">or</span> <span class="nb">die</span><span class="p">;</span>                 <span class="c1"># read exactly 4 bytes from stdin (the RFC 868 response)</span>
</span></span><span class="line"><span class="cl"><span class="p">(</span><span class="nv">$n</span><span class="p">)</span><span class="o">=</span><span class="nb">unpack</span><span class="p">(</span><span class="s">&#34;N&#34;</span><span class="p">,</span> <span class="nv">$_</span><span class="p">);</span>                     <span class="c1"># unpack them as a 32-bit unsigned integer in network (big-endian) order</span>
</span></span><span class="line"><span class="cl"><span class="k">print</span> <span class="nb">scalar</span> <span class="nb">gmtime</span><span class="p">(</span><span class="nv">$n</span><span class="o">-</span><span class="mi">2208988800</span><span class="p">),</span> <span class="s">&#34;\n&#34;</span><span class="p">;</span> <span class="c1"># convert from 1900 epoch to 1970 by subtracting 2,208,988,800 seconds,</span>
</span></span><span class="line"><span class="cl">                                           <span class="c1"># then print the corresponding UTC time in human-readable form</span>
</span></span></code></pre></div><hr>
<p><strong>finger service (port 79)</strong> — leaks detailed user information, including usernames, shells, idle times, and home directories.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">┌──<span class="o">[</span> grumble@shinobi <span class="o">]</span>:/etc
</span></span><span class="line"><span class="cl">└─$ telnet nextcube <span class="m">79</span>
</span></span><span class="line"><span class="cl">Trying 192.168.1.50...
</span></span><span class="line"><span class="cl">Connected to nextcube.darkstar.home.
</span></span><span class="line"><span class="cl">Escape character is <span class="s1">&#39;^]&#39;</span>.
</span></span><span class="line"><span class="cl">me
</span></span><span class="line"><span class="cl">Login name: me                          In real life: My Account
</span></span><span class="line"><span class="cl">Directory: /me                          Shell: /usr/gnu/bin/bash
</span></span><span class="line"><span class="cl">On since Nov <span class="m">12</span> 15:53:07 on console     <span class="m">209</span> days Idle Time
</span></span><span class="line"><span class="cl">Plan:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">This .plan file has not been <span class="nb">set</span> up by its owner yet.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Login name: me                          In real life: My Account
</span></span><span class="line"><span class="cl">Directory: /me                          Shell: /usr/gnu/bin/bash
</span></span><span class="line"><span class="cl">On since Nov <span class="m">13</span> 11:54:35 on ttyp1 from 192.168.1.51
</span></span><span class="line"><span class="cl"><span class="m">1</span> minute <span class="m">50</span> seconds Idle Time
</span></span><span class="line"><span class="cl">Plan:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">This .plan file has not been <span class="nb">set</span> up by its owner yet.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Connection closed by foreign host.
</span></span></code></pre></div><hr>
<p><strong>rexecd (port 512)</strong> — executes remote commands using plaintext credentials. Disabled by default on most systems today.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">┌──<span class="o">[</span> grumble@shinobi <span class="o">]</span>:/etc
</span></span><span class="line"><span class="cl">└─$ telnet nextcube <span class="m">512</span>
</span></span><span class="line"><span class="cl">Trying 192.168.1.50...
</span></span><span class="line"><span class="cl">Connected to nextcube.darkstar.home.
</span></span><span class="line"><span class="cl">Escape character is <span class="s1">&#39;^]&#39;</span>.
</span></span><span class="line"><span class="cl">ls
</span></span><span class="line"><span class="cl">^<span class="o">]</span>
</span></span><span class="line"><span class="cl">telnet&gt; quit
</span></span><span class="line"><span class="cl">Connection closed.
</span></span></code></pre></div><hr>
<p><strong>rlogind (port 513)</strong> — remote login service using <code>.rhosts</code> trust. Won’t connect without matching trust entries.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">┌──<span class="o">[</span> grumble@shinobi <span class="o">]</span>:/etc
</span></span><span class="line"><span class="cl">└─$ telnet nextcube <span class="m">513</span>
</span></span><span class="line"><span class="cl">Trying 192.168.1.50...
</span></span><span class="line"><span class="cl">Connected to nextcube.darkstar.home.
</span></span><span class="line"><span class="cl">Escape character is <span class="s1">&#39;^]&#39;</span>.
</span></span><span class="line"><span class="cl">^<span class="o">]</span>
</span></span><span class="line"><span class="cl">telnet&gt; quit
</span></span><span class="line"><span class="cl">Connection closed.
</span></span></code></pre></div><hr>
<p><strong>talkd (port 517)</strong> — text-based chat between users on a LAN. Connection refused here; often disabled without <code>/etc/hosts</code> setup.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">┌──<span class="o">[</span> grumble@shinobi <span class="o">]</span>:/etc
</span></span><span class="line"><span class="cl">└─$ telnet nextcube <span class="m">517</span>
</span></span><span class="line"><span class="cl">Trying 192.168.1.50...
</span></span><span class="line"><span class="cl">telnet: Unable to connect to remote host: Connection refused
</span></span></code></pre></div><hr>
<p><strong>ident (port 113)</strong> — maps connections to usernames. Not active by default on NeXTSTEP.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">┌──<span class="o">[</span> grumble@shinobi <span class="o">]</span>:/etc
</span></span><span class="line"><span class="cl">└─$ telnet nextcube <span class="m">113</span>
</span></span><span class="line"><span class="cl">Trying 192.168.1.50...
</span></span><span class="line"><span class="cl">telnet: Unable to connect to remote host: Connection refused
</span></span></code></pre></div><hr>
<p>These “small services” were once standard diagnostics — but on a 1990s network, they expose a surprising amount of data.<br>
Today, all of them would be disabled or firewalled off by default.</p>

  </div>
</details>

<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <blockquote>
<h4 id="-inetd-openstep-42-reference">🧠 Inetd (OpenStep 4.2) Reference</h4>
<p><strong>Path:</strong> <code>/usr/etc/inetd</code><br>
<strong>Managed by:</strong> <code>/etc/rc</code> and <code>/etc/rc.standard</code> at boot<br>
<strong>Configuration:</strong> <code>/etc/inetd.conf</code> → symlinked to <code>../../usr/etc/inetd</code><br>
<strong>Service mapping:</strong> Defined in <code>/etc/services</code> (standard BSD ports 7–37, 79, etc.)</p>
<p><strong>Usage:</strong></p>
<pre tabindex="0"><code>/usr/etc/inetd [ -d ] [ configuration file ]
</code></pre><ul>
<li><code>-d</code> — debug mode (runs in foreground, logs activity to console)</li>
<li><code>configuration file</code> — optional path to alternate config</li>
</ul>
<p><strong>Version:</strong><br>
<code>@(#)PROGRAM:inetd  PROJECT:etc-113  BUILT:Thu Mar 27 21:30:05 PST 1997</code><br>
Based on <code>inetd.c 1.2 88/03/10 4.0NFSSRC</code> (BSD 4.3 / NFS Source release lineage)</p>
<p><strong>Internal Services (built in to inetd):</strong></p>
<ul>
<li><code>echo</code> — returns data sent to it (TCP 7 / UDP 7)</li>
<li><code>discard</code> — silently discards input (TCP 9 / UDP 9)</li>
<li><code>chargen</code> — outputs repeating ASCII stream (TCP 19 / UDP 19)</li>
<li><code>daytime</code> — human-readable date/time (TCP 13 / UDP 13)</li>
<li><code>time</code> — 32-bit machine timestamp (TCP 37 / UDP 37)</li>
</ul>
<p><strong>External Daemons (default entries):</strong><br>
<code>ftpd</code>, <code>telnetd</code>, <code>rshd</code>, <code>rlogind</code>, <code>rexecd</code>, <code>fingerd</code>, <code>tftpd</code>,<br>
<code>comsat</code>, <code>talkd</code>, <code>ntalkd</code>, <code>NSWSd</code>, plus several RPC services<br>
(<code>rquotad</code>, <code>rstatd</code>, <code>rusersd</code>, <code>sprayd</code>, <code>rwalld</code>, <code>renderd</code>).</p>
<p><strong>Signal Handling:</strong></p>
<ul>
<li><code>SIGHUP</code> — reload <code>/etc/inetd.conf</code> (add, remove, or modify services)</li>
<li>No PID file written under OpenStep</li>
</ul>
<p><strong>Logging:</strong></p>
<ul>
<li>Logs through <code>syslogd</code> (facility <code>daemon.notice</code>) when invoked with <code>-d</code></li>
<li>No TCP wrappers or host-based access control</li>
</ul>
<p><strong>Documentation:</strong></p>
<ul>
<li><code>man 8 inetd</code></li>
<li><code>man 5 inetd.conf</code></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc862">RFC 862 – Echo Protocol</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc863">RFC 863 – Discard Protocol</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc864">RFC 864 – Character Generator Protocol</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc867">RFC 867 – Daytime Protocol</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc868">RFC 868 – Time Protocol</a></li>
</ul>
<p><strong>Notes:</strong></p>
<ul>
<li>Provides multiple lightweight network services from a single parent daemon.</li>
<li>Runs continuously in the background after boot; reconfigurable via SIGHUP.</li>
<li>Intended for trusted LAN environments — no encryption or authentication.</li>
<li>Foundation for many BSD-derived “super-server” implementations later replaced by <code>xinetd</code>.</li>
</ul></blockquote>

  </div>
</details>

<hr>
<h3 id="sendmail-and-mailapp">Sendmail and Mail.app</h3>
<p>Email was a first-class citizen in the NeXT ecosystem. NeXTSTEP and OpenStep treated mail as an essential part of the workstation experience — not an add-on. Every system shipped with a polished graphical mail client, <strong>Mail.app</strong>, and a fully configured <strong>Sendmail</strong> daemon running quietly in the background as its local Mail Transfer Agent (MTA). Together they formed a complete messaging stack: Mail.app for the user interface, Sendmail for delivery and routing.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="Sendmail.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 400px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    I had a dog-eared copy of the O'reilly bat book on my desk for years
  </figcaption>
</figure>
<p><strong>Mail.app</strong></p>
<p>The mail app is pretty self explanatory and obviously the grandaddy of the modern Apple Mail.app.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="NeXT-mail.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
<figure style="text-align:center; margin: 1em auto;">
  <img src="NeXT-mail-app.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
<p><strong>Sendmail</strong></p>
<p>Beneath the NeXT graphical shell, <strong>Sendmail</strong> is the workhorse moving every message. It’s installed and enabled by default, acting as both a <strong>local delivery agent</strong> and a <strong>basic SMTP server</strong> for the system. You don’t need POP or IMAP here — messages between local accounts move entirely through the Sendmail queue in <code>/usr/spool/mqueue</code>, and each user’s mailbox lives as a simple text file under <code>/usr/spool/mail/</code>.</p>
<p>You can get the version with the following:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="o">[</span> me@nextcube <span class="o">]</span>:~ $ /usr/lib/sendmail -bt -d0.4 &lt; /dev/null
</span></span><span class="line"><span class="cl">Version NX5.67g
</span></span><span class="line"><span class="cl">canonical name: nextcube
</span></span><span class="line"><span class="cl">          fqdn: nextcube
</span></span><span class="line"><span class="cl">using configuration file /etc/sendmail/sendmail.cf -&gt; sendmail.subsidiary.cf
</span></span><span class="line"><span class="cl">ADDRESS TEST MODE
</span></span><span class="line"><span class="cl">Enter &lt;ruleset&gt; &lt;address&gt;
</span></span><span class="line"><span class="cl"><span class="o">[</span>Note: No automatic ruleset <span class="m">3</span> call<span class="o">]</span>
</span></span></code></pre></div><p>The fun part? You can talk to Sendmail directly. It listens on <strong>port 25</strong> and speaks plain <strong>SMTP</strong>, so you can open a connection with <code>telnet</code>, issue commands by hand, and watch your cube deliver its own mail like it’s 1995.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">telnet localhost <span class="m">25</span>
</span></span></code></pre></div><p>Expected banner:</p>
<pre tabindex="0"><code>Trying 127.0.0.1...
Connected to localhost.
Escape character is &#39;^]&#39;.
220 nextcube Sendmail NX5.67g/NX3.0S ready at Wed, 12 Nov 2025 16:02:11 -0600
</code></pre><p>Manual SMTP dialog:</p>
<pre tabindex="0"><code>HELO nextcube
MAIL FROM:&lt;me@nextcube&gt;
RCPT TO:&lt;me@nextcube&gt;
DATA
Subject: telnet test
From: me@nextcube
To: me@nextcube

hello me, this is your NeXT talking to itself over SMTP.
.
QUIT
</code></pre><p>Response:</p>
<pre tabindex="0"><code>250 OK
221 nextcube closing connection
Connection closed by foreign host.
</code></pre><p>You can refer to the rfc for the full protocol reference if you really want to poke around.
<a href="https://www.rfc-editor.org/rfc/rfc821">rfc821 – Simple Mail Transfer Protocol</a></p>
<p>The message is queued and immediately delivered to the local mailbox.<br>
You can verify delivery from the command line with the classic BSD tools:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="o">[</span> me@nextcube <span class="o">]</span>:~ $ Mail
</span></span><span class="line"><span class="cl">Mail version 8.1 6/6/93.  Type ? <span class="k">for</span> help.
</span></span><span class="line"><span class="cl"><span class="s2">&#34;/usr/spool/mail/me&#34;</span>: <span class="m">1</span> message <span class="m">1</span> new
</span></span><span class="line"><span class="cl">&gt;N  <span class="m">1</span> me                    Wed Nov <span class="m">12</span> 15:50  12/299   <span class="s2">&#34;telnet test&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">&amp;</span> more <span class="m">1</span>
</span></span><span class="line"><span class="cl">Message 1:
</span></span><span class="line"><span class="cl">From me@nextcube  Wed Nov <span class="m">12</span> 15:50:11 <span class="m">2025</span>
</span></span><span class="line"><span class="cl">Subject: telnet <span class="nb">test</span>
</span></span><span class="line"><span class="cl">To: me@nextcube
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">hello me, this is your NeXT talking to itself over SMTP.
</span></span><span class="line"><span class="cl"><span class="p">&amp;</span> quit
</span></span><span class="line"><span class="cl">Held <span class="m">1</span> message in /usr/spool/mail/me
</span></span></code></pre></div><p>The same message appears instantly inside <strong>Mail.app</strong>, proving that both the GUI and the CLI share the same local spool and Sendmail transport layer.</p>

  </div>
</details>

<p>Behind the scenes, Sendmail on NeXT isn’t just a standalone daemon — it’s deeply tied into the <strong>NetInfo</strong> system.<br>
The designated mail server is identified by the alias <code>mailhost</code>, and every client knows which Sendmail configuration to use via entries in the NetInfo <code>/locations/sendmail</code> directory.<br>
Even Mail.app’s address book and sender pictures come from shared NetInfo data, updated nightly by the <code>mailDBupdate</code> cron job.</p>
<br>
<h3 id="the-web-server">the Web Server</h3>
<p>In <a href="/posts/next-config/#w3c-httpd">Part 2</a> we built the w3c-httpd web server for Intel NeXT. It is historically pleasing to see a website served by nextcube. We&rsquo;ll take a closer look at it in this section.</p>
<figure style="text-align:center; margin:1em auto;">
  <img src="next-webserver.jpg"
       alt=""
       style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size:85%; font-weight:normal; color:#666; line-height:1.4; margin-top:0.4em;">
    beautiful 90s web design
  </figcaption>
</figure>
<hr>
<p>As configured, your installation lives entirely under <code>/usr/local/httpd</code>, with a tidy BSD-style layout:</p>
<table>
  <thead>
      <tr>
          <th>purpose</th>
          <th>path</th>
          <th>notes</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>daemon binary</td>
          <td><code>/usr/local/httpd/bin/httpd</code></td>
          <td>standalone CERN 3.0A server</td>
      </tr>
      <tr>
          <td>configuration file</td>
          <td><code>/etc/httpd.conf</code></td>
          <td>main configuration</td>
      </tr>
      <tr>
          <td>document root</td>
          <td><code>/usr/local/httpd/html/</code></td>
          <td>served at <code>/</code></td>
      </tr>
      <tr>
          <td>user directories</td>
          <td><code>/me/public_html/</code></td>
          <td>available as <code>http://host/~me/</code></td>
      </tr>
      <tr>
          <td>access log</td>
          <td><code>/usr/local/httpd/logs/access_log</code></td>
          <td>all requests</td>
      </tr>
      <tr>
          <td>error log</td>
          <td><code>/usr/local/httpd/logs/error_log</code></td>
          <td>startup + runtime diagnostics</td>
      </tr>
      <tr>
          <td>startup hook</td>
          <td><code>/etc/rc.local</code></td>
          <td>auto-launch at boot</td>
      </tr>
  </tbody>
</table>
<p>The daemon starts as root only long enough to bind port 80, then drops privileges to <code>nobody</code>.<br>
Everything — config, HTML, and logs — lives neatly inside <code>/usr/local/httpd</code>, so it’s easy to back up or snapshot.</p>
<p>When launched, it identifies itself with:</p>
<pre tabindex="0"><code>............ This is CERN-HTTPD, version 3.0A, using libwww version 2.17
Reading..... /usr/local/httpd/conf/httpd.conf
</code></pre><p>From there, you can fetch <code>http://192.168.1.50/</code> for the system page, or <code>http://192.168.1.50/~me/</code> for your personal space.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <p>You can watch it work the same way you did with Sendmail — by speaking the protocol over Telnet.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">┌──<span class="o">[</span> grumble@shinobi <span class="o">]</span> :~  
</span></span><span class="line"><span class="cl">└─$ telnet nextcube <span class="m">80</span>
</span></span><span class="line"><span class="cl">Trying 192.168.1.50...
</span></span><span class="line"><span class="cl">Connected to nextcube.darkstar.home.
</span></span><span class="line"><span class="cl">Escape character is <span class="s1">&#39;^]&#39;</span>.
</span></span><span class="line"><span class="cl">GET / HTTP/1.0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">HTTP/1.0 <span class="m">200</span> Document follows
</span></span><span class="line"><span class="cl">Server: CERN/3.0A
</span></span><span class="line"><span class="cl">Date: Wed, <span class="m">12</span> Nov <span class="m">2025</span> 21:24:40 GMT
</span></span><span class="line"><span class="cl">Content-Type: text/html
</span></span><span class="line"><span class="cl">Content-Length: <span class="m">43</span>
</span></span><span class="line"><span class="cl">Last-Modified: Sun, <span class="m">20</span> Apr <span class="m">1997</span> 12:42:55 GMT
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">&lt;h1&gt;Welcome to CERN httpd on OpenStep&lt;/h1&gt;
</span></span><span class="line"><span class="cl">Connection closed by foreign host.
</span></span></code></pre></div><p>And for a user directory:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">┌──<span class="o">[</span> grumble@shinobi <span class="o">]</span> :~  
</span></span><span class="line"><span class="cl">└─$ telnet nextcube <span class="m">80</span>
</span></span><span class="line"><span class="cl">Trying 192.168.1.50...
</span></span><span class="line"><span class="cl">Connected to nextcube.darkstar.home.
</span></span><span class="line"><span class="cl">Escape character is <span class="s1">&#39;^]&#39;</span>.
</span></span><span class="line"><span class="cl">GET /~me/ HTTP/1.0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">HTTP/1.0 <span class="m">200</span> Document follows
</span></span><span class="line"><span class="cl">Server: CERN/3.0A
</span></span><span class="line"><span class="cl">Date: Wed, <span class="m">12</span> Nov <span class="m">2025</span> 21:27:32 GMT
</span></span><span class="line"><span class="cl">Content-Type: text/html
</span></span><span class="line"><span class="cl">Content-Length: <span class="m">24</span>
</span></span><span class="line"><span class="cl">Last-Modified: Sun, <span class="m">20</span> Apr <span class="m">1997</span> 12:42:55 GMT
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">&lt;h1&gt;Hello from ~me&lt;/h1&gt;
</span></span><span class="line"><span class="cl">Connection closed by foreign host.
</span></span></code></pre></div>
  </div>
</details>

<hr>
<h4 id="web-browser">Web Browser</h4>
<p>Next we&rsquo;ll need a web browser so we can surf the Internet super-highway while quoting the movie <em>Hackers</em>!</p>
<p>The VirtualBox OVA appliance from <strong>Part 1</strong> already includes <strong>OmniWeb</strong>, the period-correct browser for NeXTSTEP.<br>
If you’re building your own system, you can find it in the <strong>Lighthouse Design Suite</strong> from<br>
<a href="https://fsck.technology/software/NeXT/NeXTSTEP%20Applications/Lighthouse%20Design%20Suite%20NeXTSTEP%20%28MDF%29/">Lighthouse (NeXTSTEP Applications)</a>.</p>
<p>Convert the <code>.mdf</code> image to ISO before mounting:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt install mdf2iso   <span class="c1"># or brew install mdf2iso on macOS</span>
</span></span><span class="line"><span class="cl">mdf2iso LIGHTHOUSE.mdf LIGHTHOUSE.iso
</span></span></code></pre></div><p>Mount <code>LIGHTHOUSE.iso</code> in your VM and install OmniWeb (and the rest of the suite).<br>
Once copied into <code>/LocalApps</code>, launch it from Workspace Manager or:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">open /LocalApps/OmniWeb.app
</span></span></code></pre></div><p>You can visit nextcube&rsquo;s own local site at <code>http://127.0.0.1/</code> or check out a few fun sites aimed at vintage machines:</p>
<ul>
<li><a href="https://www.nextcomputers.org/">NeXTComputers.org — Home of the international NeXT forums</a></li>
<li><a href="https://theoldnet.com/">TheOldNet.com — Retro-web portal optimized for vintage browsers</a></li>
<li><a href="http://frogfind.de/?lg=en-us">FrogFind — Lightweight search engine for vintage machines</a></li>
</ul>
<br>
<h3 id="directory-and-network-integration">Directory and Network Integration</h3>
<p>OpenStep supports several legacy directory and authentication systems common in mixed UNIX environments of the 1990s.<br>
<strong>NetInfo</strong> remains the system’s native database for users, groups, hosts, and network services — roughly analogous to early LDAP.<br>
It can coexist with <strong>NIS (Yellow Pages)</strong> for compatibility with older UNIX networks, and includes optional <strong>NetWare</strong> client support for file and print services.</p>
<p>In most standalone setups, only NetInfo is active, but these subsystems illustrate how NeXT aimed to fit seamlessly into larger multi-vendor networks of the time.</p>
<p>For more information, refer to the NFS section of the SysAdmins Guide in the <code>/NextLibrary/Bookshelves</code> folder.</p>
<br>
<h2 id="system-hardening">System Hardening</h2>
<p>OpenStep was built for a friendlier network era, but NeXT’s own <em>System Administration Guide (Release 4.0)</em> includes several practical ways to tighten things up. Here are a few quick checks worth doing — or at least exploring — before you put your cube on a modern LAN.</p>
<hr>
<h3 id="-entering-single-user-mode">🛠 Entering Single-User Mode</h3>
<p>On most OpenStep 4.x installations running under VirtualBox or genuine NeXT hardware, the <strong>boot screen appears automatically</strong> — a gray window with a ten-second countdown and a <code>boot:</code> prompt at the bottom.<br>
You don’t need to hold any special key combinations unless a hardware password is enabled.</p>
<p>When you see the countdown, simply click inside the window (or press any key) to stop the timer.<br>
You can then enter boot arguments directly at the prompt:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mach_kernel -v -s
</span></span></code></pre></div><p>and press <strong>Return</strong>.</p>
<p>The options mean:</p>
<ul>
<li><code>-v</code> — verbose mode (displays detailed startup messages)</li>
<li><code>-s</code> — single-user mode (boots to a root shell instead of multi-user login)</li>
</ul>
<p>The system loads the kernel, mounts the root filesystem read-only, and drops you at a minimal shell:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sh-2.00#
</span></span></code></pre></div><p>From here you can perform maintenance tasks such as:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">fsck -y /dev/sd0a        <span class="c1"># Check and repair the root filesystem</span>
</span></span><span class="line"><span class="cl">mount -a                 <span class="c1"># Mount all local filesystems</span>
</span></span><span class="line"><span class="cl">passwd &lt;username&gt;        <span class="c1"># Reset a user&#39;s password</span>
</span></span></code></pre></div><p>When finished, type:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">exit</span>
</span></span></code></pre></div><p>to continue booting normally, or:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">reboot
</span></span></code></pre></div><p>to restart cleanly.</p>
<blockquote>
<p><span class="tag gray">Note</span><br>
Earlier NeXTSTEP manuals refer to <code>bsd -s</code>, which was a link to the kernel on older releases.<br>
On OpenStep 4.x, the correct binary name is <strong><code>mach_kernel</code></strong>.<br>
The same prompt can load alternate kernels such as <code>cdrom</code> or <code>floppy</code> for installation or recovery.<br>
If a <strong>hardware password</strong> is set in Preferences, you must enter it before custom boot arguments are accepted.</p></blockquote>
<hr>
<h3 id="auditing-set-user-id-programs">Auditing Set-User-ID Programs</h3>
<p>NeXT systems ship with dozens of <strong>setuid</strong> and <strong>setgid</strong> binaries — around eighty in a default install — many of which allow normal users to perform administrative actions without <code>sudo</code> (which doesn’t exist on NeXT).<br>
You can review them with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">find / -perm -4000 -user root -print
</span></span></code></pre></div><p>or include all users:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">find / -perm -4000 -print
</span></span></code></pre></div><p>Each file you see runs with elevated privileges. You can remove the bit with <code>chmod u-s</code>, but some tools (like <em>Preferences</em>, <em>PrintManager</em>, or network utilities) rely on it to work properly.<br>
Consider this an exercise in exploration — compare, document, and test rather than disabling everything blindly.</p>
<hr>
<h3 id="trimming-inetd-services">Trimming inetd Services</h3>
<p><code>inetd</code> launches a variety of network services automatically at boot, many of which are only useful for diagnostics or legacy clients.<br>
Review <code>/etc/inetd.conf</code> and comment out anything you don’t plan to use. On a secure setup, you can safely disable:</p>
<pre tabindex="0"><code>echo, discard, chargen, daytime, time
finger, rshd, rlogind, rexecd
talkd, ntalkd, tftpd
rusersd, sprayd, walld
</code></pre><p>After editing, restart the daemon or send it a HUP signal:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">kill</span> -HUP <span class="sb">`</span>cat /etc/inetd.pid<span class="sb">`</span>
</span></span></code></pre></div><p>That trims away most of the &ldquo;small services&rdquo; that modern systems disable by default, leaving just Telnet or FTP if you actually need them.</p>
<hr>
<h3 id="strengthening-password-policy">Strengthening Password Policy</h3>
<p>Password rules and login behavior are managed through <strong>NetInfo</strong>.<br>
In NetInfoManager (or using <code>niutil</code>), you can enable stricter checks by setting the <code>security_options</code> property in the root domain:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">niutil -createprop . security_options secure_passwords
</span></span><span class="line"><span class="cl">niutil -appendprop . security_options lockout
</span></span><span class="line"><span class="cl">niutil -appendprop . security_options login_accounting
</span></span></code></pre></div><p>These enable stronger passwords, exponential lockout delays after failed logins, and basic login auditing to <code>/usr/adm/messages</code>.<br>
For convenience, NeXT also offered a “discourage_public_servers” flag to hide sound and display sharing options in Preferences.</p>
<hr>
<p>These simple changes go a long way toward tightening an OpenStep install while keeping it historically accurate. It will also accurately break under the ping of death and any number of buffer overrun attacks, so keep it off the Internet.</p>
<p>For a full list of available security options and examples, see <em>System Administration, Chapter 12: “Security”</em> in the <strong>Librarian</strong> documentation.</p>
<br>
<h2 id="links-and-stuff">Links and Stuff</h2>
<p><strong>Project Links</strong></p>
<ul>
<li><a href="/posts/virtual-nextcube/">Part 1 – An OpenStep 4.2 VM in 2025</a></li>
<li><a href="/posts/next-config/">Part 2 – Finishing the OpenStep 4.2 Appliance</a></li>
<li><a href="https://archive.org/details/openstep_ova">OpenStep 4.2 VirtualBox OVA (Appliance Download)</a></li>
</ul>
<p><strong>Archives and References</strong></p>
<ul>
<li><a href="https://archive.org/search?query=openstep">Archive.org – OpenStep Collection</a> — installation media, developer tools, and system images</li>
<li><a href="https://archive.org/search?query=nextworld">Archive.org – <em>NeXTWORLD Magazine</em> Archive</a> — full issues and ads from the ’90s</li>
</ul>
<p><strong>Community</strong></p>
<ul>
<li><a href="https://www.nextcomputers.org/forums/index.php">NeXTComputers.org Forums</a> — active NeXT/OPENSTEP community and hardware archives</li>
</ul>
<br>
<h2 id="conclusion">Conclusion</h2>
<p>I’ve used the terms <em>NeXT</em>, <em>NeXTSTEP</em>, and <em>OpenStep</em> interchangeably throughout this series — guilty as charged.<br>
Technically, <strong>OpenStep</strong> is the correct name, but <em>NeXT</em> is just more fun to type (inverse CamelCase has a certain charm).</p>
<p>It’s been a rewarding little trip through a sleek, prohibitively expensive at the time, slice of computing history.<br>
OpenStep runs beautifully under VirtualBox on Linux, and even on my Intel MacBook Pro running Ubuntu it feels snappy and solid.<br>
With a few GNU tools and a sensible shell, it’s a genuinely usable UNIX workstation — elegant in that NeXT way.</p>
<p>VirtualBox’s remote display feature never quite behaved, so I stuck to Telnet and FTP for remote access.<br>
It’s probably not worth the effort to chase perfect mouse capture so it remains too awkward to really use.</p>
<p>After all these years, it’s still easy to see why NeXT inspired so much devotion.<br>
OpenStep remains both a product of its time and a glimpse of the future it quietly became.</p>
<p>Feedback welcome <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
<br>
<figure style="text-align:center; margin: 1em auto;">
  <img src="unix-expert-check.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 300px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
]]></content:encoded>
    </item>
    <item>
      <title>nextcube, Part 2 – Finishing the OpenStep 4.2 Appliance</title>
      <link>https://adminjitsu.com/posts/next-config/</link>
      <pubDate>Tue, 04 Nov 2025 10:00:00 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/next-config/</guid>
      <description>This follow-up to the OpenStep 4.2 VM guide finishes configuration to match the public OVA — adding developer tools, GNU utilities, X11, and a full working environment.</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<p>In <a href="/posts/virtual-nextcube/">Part 1</a>, we got OpenStep 4.2 installed, patched, and running cleanly on modern hardware.<br>
In this sequel, we’ll <strong>finish the job</strong> — replicating the complete OVA Appliance build published on <a href="https://archive.org/details/openstep_ova">Archive.org</a>. This guide assumes that you are building your own VM and have completed Part1. If you downloaded the OVA Appliance, these steps are already complete.</p>
<p>By the end, you’ll have a full OpenStep workstation: compiler toolchain, X11, shells, text utilities, and even a few Easter eggs.</p>
<p><strong>Let&rsquo;s Unix!</strong></p>
<br>
<h2 id="developer-tools">Developer Tools</h2>
<p>We&rsquo;ll start by installing the Developer Tools.</p>
<p>You can download the iso from <a href="https://fsck.technology/software/NeXT/OpenStep%20Installation%20Media/OpenStep%204.2/OpenStep%204.2%20Developer/">here</a> or from archive.org.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="next-developer-cd.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
<p><strong>Steps</strong></p>
<ol>
<li>
<p>Log in as root after setting a password (see <a href="/posts/virtual-nextcube/#set-password">Set Password</a>).</p>
</li>
<li>
<p>Attach or insert the <strong>Developer CD ISO</strong>.</p>
</li>
<li>
<p>Navigate to:</p>
<pre tabindex="0"><code>/NextCD/Packages/
</code></pre></li>
<li>
<p>Install the following packages <strong>in order</strong>:</p>
<pre tabindex="0"><code>DeveloperTools.pkg
DeveloperLibs.pkg
GNUSource.pkg
ProfileLibs.pkg
DeveloperDoc.pkg
</code></pre><p>When logged in as root you merely double-click on each pkg file to run the installer.</p>
<p>You could also launch the installer from the terminal with <code>/NextAdmin/Installer.app/Installer /DeveloperCDname/NextCD/DevloperTools.pkg</code></p>
</li>
<li>
<p>Reboot or log out/in once finished.</p>
</li>
</ol>
<h3 id="verify-developer-tools-installation">Verify Developer Tools Installation</h3>
<p>After rebooting or logging back in, confirm the compiler toolchain is working:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Check that the C compiler (cc) is available and functioning</span>
</span></span><span class="line"><span class="cl">cc -v
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Expected output (varies slightly by build)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Reading specs from /lib/i386/specs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># NeXT Software, Inc. version cc-744.13, gcc version 2.7.2.1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">perl -v
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Expected output</span>
</span></span><span class="line"><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="cl"><span class="c1"># This is perl, version 5.001</span>
</span></span><span class="line"><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="cl"><span class="c1">#        Unofficial patchlevel 1m.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Copyright 1987-1994, Larry Wall</span>
</span></span><span class="line"><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Perl may be copied only under the terms of either the Artistic License or the</span>
</span></span><span class="line"><span class="cl"><span class="c1"># GNU General Public License, which may be found in the Perl 5.0 source kit.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Confirm other key tools exist</span>
</span></span><span class="line"><span class="cl">which cc
</span></span><span class="line"><span class="cl">which ld
</span></span><span class="line"><span class="cl">which make
</span></span><span class="line"><span class="cl">which ar
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check library and include directories</span>
</span></span><span class="line"><span class="cl">ls /NextDeveloper/Headers
</span></span><span class="line"><span class="cl">ls /NextDeveloper/Lib
</span></span></code></pre></div><details >
  <summary>Details</summary>
  <div class="collapse-body">
    <p>After installing the Developer Tools, verify that <code>cc</code> is working with this simple C program:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cm">/*
</span></span></span><span class="line"><span class="cl"><span class="cm"> * hello.c — NeXT-friendly Hello World demo
</span></span></span><span class="line"><span class="cl"><span class="cm"> * Compatible with OPENSTEP/NeXTSTEP cc
</span></span></span><span class="line"><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;stdlib.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;string.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;time.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;sys/types.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;sys/param.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="cm">/* Try to include uname if it exists */</span>
</span></span><span class="line"><span class="cl"><span class="cp">#ifdef HAVE_SYS_UTSNAME_H
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;sys/utsname.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#endif
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">main</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">time_t</span> <span class="n">now</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">char</span> <span class="o">*</span><span class="n">timestr</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">FILE</span> <span class="o">*</span><span class="n">fp</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">char</span> <span class="n">buf</span><span class="p">[</span><span class="mi">256</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cp">#ifdef HAVE_SYS_UTSNAME_H
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>    <span class="k">struct</span> <span class="n">utsname</span> <span class="n">sys</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="cp">#endif
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl">    <span class="n">now</span> <span class="o">=</span> <span class="nf">time</span><span class="p">((</span><span class="kt">time_t</span> <span class="o">*</span><span class="p">)</span><span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="n">timestr</span> <span class="o">=</span> <span class="nf">ctime</span><span class="p">(</span><span class="o">&amp;</span><span class="n">now</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">timestr</span> <span class="o">&amp;&amp;</span> <span class="n">timestr</span><span class="p">[</span><span class="nf">strlen</span><span class="p">(</span><span class="n">timestr</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span> <span class="o">==</span> <span class="sc">&#39;\n&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">timestr</span><span class="p">[</span><span class="nf">strlen</span><span class="p">(</span><span class="n">timestr</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="sc">&#39;\0&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;=====================================</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;   Hello from NeXT/OpenStep Unix!</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;=====================================</span><span class="se">\n\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cp">#ifdef HAVE_SYS_UTSNAME_H
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>    <span class="k">if</span> <span class="p">(</span><span class="nf">uname</span><span class="p">(</span><span class="o">&amp;</span><span class="n">sys</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;System: %s %s (%s)</span><span class="se">\n</span><span class="s">Node: %s</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">               <span class="n">sys</span><span class="p">.</span><span class="n">sysname</span><span class="p">,</span> <span class="n">sys</span><span class="p">.</span><span class="n">release</span><span class="p">,</span> <span class="n">sys</span><span class="p">.</span><span class="n">machine</span><span class="p">,</span> <span class="n">sys</span><span class="p">.</span><span class="n">nodename</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;System information unavailable.</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="cp">#else
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;System information unavailable (no uname support).</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="cp">#endif
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;Date:   %s</span><span class="se">\n\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">timestr</span> <span class="o">?</span> <span class="nl">timestr</span> <span class="p">:</span> <span class="s">&#34;(unknown)&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;Running &#39;whoami&#39; and &#39;uptime&#39;...</span><span class="se">\n\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">fp</span> <span class="o">=</span> <span class="nf">popen</span><span class="p">(</span><span class="s">&#34;whoami&#34;</span><span class="p">,</span> <span class="s">&#34;r&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">fp</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nf">fgets</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">buf</span><span class="p">),</span> <span class="n">fp</span><span class="p">)</span> <span class="o">!=</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="n">buf</span><span class="p">[</span><span class="nf">strcspn</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="s">&#34;</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">)]</span> <span class="o">=</span> <span class="sc">&#39;\0&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;User:   %s</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">buf</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="nf">pclose</span><span class="p">(</span><span class="n">fp</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">fp</span> <span class="o">=</span> <span class="nf">popen</span><span class="p">(</span><span class="s">&#34;uptime&#34;</span><span class="p">,</span> <span class="s">&#34;r&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">fp</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nf">fgets</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">buf</span><span class="p">),</span> <span class="n">fp</span><span class="p">)</span> <span class="o">!=</span> <span class="nb">NULL</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;Uptime: %s&#34;</span><span class="p">,</span> <span class="n">buf</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="nf">pclose</span><span class="p">(</span><span class="n">fp</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;</span><span class="se">\n</span><span class="s">All systems nominal.</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;=====================================</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
<p>Compile and run it to confirm your toolchain is functioning:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cc hello.c -o hello
</span></span><span class="line"><span class="cl">./hello
</span></span></code></pre></div><p>You should see system info, the current date, and your user details — proof your NeXT compiler and environment are alive and kicking.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="o">=====================================</span>
</span></span><span class="line"><span class="cl">   Hello from NeXT/OpenStep Unix!
</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">System information unavailable <span class="o">(</span>no uname support<span class="o">)</span>.
</span></span><span class="line"><span class="cl">Date:   Tue Nov  <span class="m">4</span> 19:32:19 <span class="m">2025</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Running <span class="s1">&#39;whoami&#39;</span> and <span class="s1">&#39;uptime&#39;</span>...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">User:   me
</span></span><span class="line"><span class="cl">Uptime:   7:32pm  up  9:31,  <span class="m">3</span> users,  load average: 2.99, 2.33, 1.73
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">All systems nominal.
</span></span><span class="line"><span class="cl"><span class="o">=====================================</span>
</span></span></code></pre></div>
  </div>
</details>

<br>
<h2 id="shells">Bash shell</h2>
<p>Install a better shell and set your account to use it.</p>
<p>You can download a working pkg to install bash2 from <a href="https://fsck.technology/software/NeXT/next.68k.org%20Archive/otto/html/pub/Unix/Shells/bash.2.0.NIHS.b.tar.gz"><code>bash.2.0.NIHS.b.tar.gz</code></a>. This contemporary version supports tab completion and many other niceties over csh.</p>
<p>You can also grab a copy of tcsh if you prefer from <a href="https://fsck.technology/software/NeXT/next.68k.org%20Archive/otto/html/pub/Unix/Shells/tcsh.6.06.NIHS.b.gz">tcsh.6.06.NIHS.b.gz </a></p>
<p><span class="tag orange">TIP</span> When browsing archives of NeXT software, you are looking for <strong>NIHS</strong> or <strong>NI</strong> or <strong>I</strong> versions where the I stands for Intel and b denotes that the tarball contains a binary or pkg installer or bs meaning it contains binaries and source.</p>
<p><strong>To Install</strong></p>
<ul>
<li>use FileZilla to ftp to the IP or hostname of the VM using your me credentials and transfer the shell tarballs to <code>/me/src/</code></li>
<li>on NeXT, login as root and run the <code>bash.pkg</code> in <code>/me/src</code></li>
</ul>
<p>This will install bash to <code>/usr/gnu/bin/bash</code></p>
<hr>
<h3 id="bash-startup-dotfiles">Bash Startup Dotfiles</h3>
<p>You&rsquo;ll also need a good <code>.bashrc</code> and <code>.bash_profile</code></p>
<p><span class="tag orange">TIP</span> before building vim and termcap, :set paste will not be available in vi and pasting into your editor from telnet will mess up the formatting. It might be easiest to create the following file and ftp it to /me. Just remember to use ASCII and do it on Linux or run dos2unix on it before ftp&rsquo;ing.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <p>If you don&rsquo;t have a working editor yet, you can create the <code>.bashrc</code> file directly from the shell using a heredoc:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat &gt; ~/.bashrc <span class="s">&lt;&lt; &#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s"># ~/.bashrc  OPENSTEP BASH2 QOL
</span></span></span><span class="line"><span class="cl"><span class="s">export HISTFILE=~/.bash_history
</span></span></span><span class="line"><span class="cl"><span class="s">export HISTSIZE=5000
</span></span></span><span class="line"><span class="cl"><span class="s">export EDITOR=vim
</span></span></span><span class="line"><span class="cl"><span class="s">export VISUAL=vim
</span></span></span><span class="line"><span class="cl"><span class="s">export PAGER=less
</span></span></span><span class="line"><span class="cl"><span class="s">export TERMCAP=/etc/termcap
</span></span></span><span class="line"><span class="cl"><span class="s">export TERM=openstep-safe
</span></span></span><span class="line"><span class="cl"><span class="s">export PATH=&#34;/usr/local/bin:/usr/etc:/usr/ucb:/usr/gnu/bin:/usr/local/games:/usr/bin:/bin:$PATH&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">export MANPATH=/usr/local/man:/usr/man
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s"># Prompt
</span></span></span><span class="line"><span class="cl"><span class="s">#PS1=&#39;\[\033[1;36m\]\u@\h:\w\$ \[\033[0m\]&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s"># ANSI-safe color codes
</span></span></span><span class="line"><span class="cl"><span class="s">USER_COLOR=&#39;\[\033[1;36m\]&#39;      # Cyan
</span></span></span><span class="line"><span class="cl"><span class="s">AT_COLOR=&#39;\[\033[1;34m\]&#39;        # Blue
</span></span></span><span class="line"><span class="cl"><span class="s">HOST_COLOR=&#39;\[\033[1;36m\]&#39;      # Cyan again
</span></span></span><span class="line"><span class="cl"><span class="s">FRAME_COLOR=&#39;\[\033[1;33m\]&#39;     # Yellow brackets
</span></span></span><span class="line"><span class="cl"><span class="s">PATH_COLOR=&#39;\[\033[0;37m\]&#39;      # Normal white path
</span></span></span><span class="line"><span class="cl"><span class="s">RESET=&#39;\[\033[0m\]&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s"># Final PS1
</span></span></span><span class="line"><span class="cl"><span class="s">#PS1=&#34;${FRAME_COLOR}[ ${USER_COLOR}\u${AT_COLOR}@${HOST_COLOR}\h${FRAME_COLOR} ]${PATH_COLOR}:\w ${RESET}\\$ &#34;
</span></span></span><span class="line"><span class="cl"><span class="s">PS1=&#34;${FRAME_COLOR}[ ${USER_COLOR}\u${AT_COLOR}@${HOST_COLOR}\h${FRAME_COLOR} ]${PATH_COLOR}:\w ${RESET}\\$ ${RESET} &#34;
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">stty erase &#39;^?&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">eval `dircolors`
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s"># Aliases
</span></span></span><span class="line"><span class="cl"><span class="s">alias ls=&#39;ls --color=auto -F&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">alias ll=&#39;ls -lagF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">alias la=&#39;ls -aF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">alias cls=&#39;clear&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">alias termcheck=&#39;od -c&#39;      # Inspect key sequences
</span></span></span><span class="line"><span class="cl"><span class="s">alias vtest=&#39;TERM=openstep-safe vim -u NONE -N&#39;   # Launch Vim with minimal config
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s"># Enable some shell options
</span></span></span><span class="line"><span class="cl"><span class="s">set -o emacs               # emacs-style command line (default)
</span></span></span><span class="line"><span class="cl"><span class="s">shopt -s histappend        # preserve history between sessions
</span></span></span><span class="line"><span class="cl"><span class="s">shopt -s checkwinsize      # auto resize terminal columns
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s"># Functions (yes, bash 2 has these!)
</span></span></span><span class="line"><span class="cl"><span class="s">extract() {
</span></span></span><span class="line"><span class="cl"><span class="s">  if [ -f &#34;$1&#34; ]; then
</span></span></span><span class="line"><span class="cl"><span class="s">    case &#34;$1&#34; in
</span></span></span><span class="line"><span class="cl"><span class="s">      *.tar.bz2)   bzip2 -dc &#34;$1&#34; | tar xf -    ;;
</span></span></span><span class="line"><span class="cl"><span class="s">      *.tar.gz)    gzip -dc &#34;$1&#34; | tar xf -     ;;
</span></span></span><span class="line"><span class="cl"><span class="s">      *.bz2)       bunzip2 &#34;$1&#34;                 ;;
</span></span></span><span class="line"><span class="cl"><span class="s">      *.gz)        gunzip &#34;$1&#34;                  ;;
</span></span></span><span class="line"><span class="cl"><span class="s">      *.tar)       tar xf &#34;$1&#34;                  ;;
</span></span></span><span class="line"><span class="cl"><span class="s">      *.zip)       unzip &#34;$1&#34;                   ;;
</span></span></span><span class="line"><span class="cl"><span class="s">      *.Z)         uncompress &#34;$1&#34;              ;;
</span></span></span><span class="line"><span class="cl"><span class="s">      *.tar.Z)     uncompress -c &#34;$1&#34; | tar xf - ;;
</span></span></span><span class="line"><span class="cl"><span class="s">      *)           echo &#34;Unknown archive format.&#34; ;;
</span></span></span><span class="line"><span class="cl"><span class="s">    esac
</span></span></span><span class="line"><span class="cl"><span class="s">  else
</span></span></span><span class="line"><span class="cl"><span class="s">    echo &#34;&#39;$1&#39; is not a valid file&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">  fi
</span></span></span><span class="line"><span class="cl"><span class="s">}
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">if [ -x /usr/local/games/fortune ]; then
</span></span></span><span class="line"><span class="cl"><span class="s">    echo
</span></span></span><span class="line"><span class="cl"><span class="s">    /usr/local/games/fortune
</span></span></span><span class="line"><span class="cl"><span class="s">    echo
</span></span></span><span class="line"><span class="cl"><span class="s">fi
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Also create .bash_profile</span>
</span></span><span class="line"><span class="cl">cat &gt; ~/.bash_profile <span class="s">&lt;&lt; &#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">[ -f &#34;$HOME/.bashrc&#34; ] &amp;&amp; source &#34;$HOME/.bashrc&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Source it to apply changes</span>
</span></span><span class="line"><span class="cl"><span class="nb">source</span> ~/.bashrc</span></span></code></pre></div>
  </div>
</details>

<p><strong><code>.bashrc</code></strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># ~/.bashrc  OPENSTEP BASH2 QOL</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">HISTFILE</span><span class="o">=</span>~/.bash_history
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">HISTSIZE</span><span class="o">=</span><span class="m">5000</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">EDITOR</span><span class="o">=</span>vim
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">VISUAL</span><span class="o">=</span>vim
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">PAGER</span><span class="o">=</span>less
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">TERMCAP</span><span class="o">=</span>/etc/termcap
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">TERM</span><span class="o">=</span>openstep-safe
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span><span class="s2">&#34;/usr/local/bin:/usr/etc:/usr/ucb:/usr/gnu/bin:/usr/local/games:/usr/bin:/bin:</span><span class="nv">$PATH</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">MANPATH</span><span class="o">=</span>/usr/local/man:/usr/man
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Prompt</span>
</span></span><span class="line"><span class="cl"><span class="c1">#PS1=&#39;\[\033[1;36m\]\u@\h:\w\$ \[\033[0m\]&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ANSI-safe color codes</span>
</span></span><span class="line"><span class="cl"><span class="nv">USER_COLOR</span><span class="o">=</span><span class="s1">&#39;\[\033[1;36m\]&#39;</span>      <span class="c1"># Cyan</span>
</span></span><span class="line"><span class="cl"><span class="nv">AT_COLOR</span><span class="o">=</span><span class="s1">&#39;\[\033[1;34m\]&#39;</span>        <span class="c1"># Blue</span>
</span></span><span class="line"><span class="cl"><span class="nv">HOST_COLOR</span><span class="o">=</span><span class="s1">&#39;\[\033[1;36m\]&#39;</span>      <span class="c1"># Cyan again</span>
</span></span><span class="line"><span class="cl"><span class="nv">FRAME_COLOR</span><span class="o">=</span><span class="s1">&#39;\[\033[1;33m\]&#39;</span>     <span class="c1"># Yellow brackets</span>
</span></span><span class="line"><span class="cl"><span class="nv">PATH_COLOR</span><span class="o">=</span><span class="s1">&#39;\[\033[0;37m\]&#39;</span>      <span class="c1"># Normal white path</span>
</span></span><span class="line"><span class="cl"><span class="nv">RESET</span><span class="o">=</span><span class="s1">&#39;\[\033[0m\]&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Final PS1</span>
</span></span><span class="line"><span class="cl"><span class="c1">#PS1=&#34;${FRAME_COLOR}[ ${USER_COLOR}\u${AT_COLOR}@${HOST_COLOR}\h${FRAME_COLOR} ]${PATH_COLOR}:\w ${RESET}\\$ &#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">PS1</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">FRAME_COLOR</span><span class="si">}</span><span class="s2">[ </span><span class="si">${</span><span class="nv">USER_COLOR</span><span class="si">}</span><span class="s2">\u</span><span class="si">${</span><span class="nv">AT_COLOR</span><span class="si">}</span><span class="s2">@</span><span class="si">${</span><span class="nv">HOST_COLOR</span><span class="si">}</span><span class="s2">\h</span><span class="si">${</span><span class="nv">FRAME_COLOR</span><span class="si">}</span><span class="s2"> ]</span><span class="si">${</span><span class="nv">PATH_COLOR</span><span class="si">}</span><span class="s2">:\w </span><span class="si">${</span><span class="nv">RESET</span><span class="si">}</span><span class="s2">\\</span>$<span class="s2"> </span><span class="si">${</span><span class="nv">RESET</span><span class="si">}</span><span class="s2"> &#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">stty erase <span class="s1">&#39;^?&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">eval</span> <span class="sb">`</span>dircolors<span class="sb">`</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Aliases</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">ls</span><span class="o">=</span><span class="s1">&#39;ls --color=auto -F&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">ll</span><span class="o">=</span><span class="s1">&#39;ls -lagF&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">la</span><span class="o">=</span><span class="s1">&#39;ls -aF&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">cls</span><span class="o">=</span><span class="s1">&#39;clear&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">termcheck</span><span class="o">=</span><span class="s1">&#39;od -c&#39;</span>      <span class="c1"># Inspect key sequences</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">vtest</span><span class="o">=</span><span class="s1">&#39;TERM=openstep-safe vim -u NONE -N&#39;</span>   <span class="c1"># Launch Vim with minimal config</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Enable some shell options</span>
</span></span><span class="line"><span class="cl"><span class="nb">set</span> -o emacs               <span class="c1"># emacs-style command line (default)</span>
</span></span><span class="line"><span class="cl"><span class="nb">shopt</span> -s histappend        <span class="c1"># preserve history between sessions</span>
</span></span><span class="line"><span class="cl"><span class="nb">shopt</span> -s checkwinsize      <span class="c1"># auto resize terminal columns</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Functions (yes, bash 2 has these!)</span>
</span></span><span class="line"><span class="cl">extract<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> -f <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> in
</span></span><span class="line"><span class="cl">      *.tar.bz2<span class="o">)</span>   bzip2 -dc <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="p">|</span> tar xf -    <span class="p">;;</span>
</span></span><span class="line"><span class="cl">      *.tar.gz<span class="o">)</span>    gzip -dc <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="p">|</span> tar xf -     <span class="p">;;</span>
</span></span><span class="line"><span class="cl">      *.bz2<span class="o">)</span>       bunzip2 <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>                 <span class="p">;;</span>
</span></span><span class="line"><span class="cl">      *.gz<span class="o">)</span>        gunzip <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>                  <span class="p">;;</span>
</span></span><span class="line"><span class="cl">      *.tar<span class="o">)</span>       tar xf <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>                  <span class="p">;;</span>
</span></span><span class="line"><span class="cl">      *.zip<span class="o">)</span>       unzip <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>                   <span class="p">;;</span>
</span></span><span class="line"><span class="cl">      *.Z<span class="o">)</span>         uncompress <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>              <span class="p">;;</span>
</span></span><span class="line"><span class="cl">      *.tar.Z<span class="o">)</span>     uncompress -c <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="p">|</span> tar xf - <span class="p">;;</span>
</span></span><span class="line"><span class="cl">      *<span class="o">)</span>           <span class="nb">echo</span> <span class="s2">&#34;Unknown archive format.&#34;</span> <span class="p">;;</span>
</span></span><span class="line"><span class="cl">    <span class="k">esac</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;&#39;</span><span class="nv">$1</span><span class="s2">&#39; is not a valid file&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</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="k">if</span> <span class="o">[</span> -x /usr/local/games/fortune <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span>
</span></span><span class="line"><span class="cl">        /usr/local/games/fortune
</span></span><span class="line"><span class="cl">        <span class="nb">echo</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span></code></pre></div><p><strong><code>.bash_profile</code></strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="o">[</span> -f <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.bashrc&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nb">source</span> <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.bashrc&#34;</span>
</span></span></code></pre></div><p>You should now have bash available which is much nicer to use than csh.</p>
<p><span class="tag blue">NOTE</span>If you get a message about with the prompt TERM= in your shell, just type vt100. We&rsquo;ll fix that in the next section with a new termcap and TERM setting.</p>
<hr>
<h3 id="set-shell">Setting Shell for Your User Account</h3>
<figure style="text-align:center; margin: 1em auto;">
  <img src="next-setting-shell.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    You can change your shell with the `/NextAdmin/UserManager.app` Long Form 
  </figcaption>
</figure>
<p>After installing Bash, you can set it as your login shell using <strong>UserManager.app</strong>.</p>
<ol>
<li>
<p>Open <strong>/NextAdmin/UserManager.app</strong> as <code>root</code>.</p>
</li>
<li>
<p>From the menu, choose<br>
<strong>User ▸ Open</strong>, then double-click your account (<code>me</code>).</p>
</li>
<li>
<p>In the <strong>User Information</strong> window, locate the <strong>Login shell</strong> field and replace the existing entry with <code>/usr/gnu/bin/bash</code></p>
</li>
<li>
<p>Choose <strong>User ▸ Save</strong>, confirm in the pop-up dialog, and click <strong>OK</strong>.</p>
</li>
<li>
<p>Repeat the process for the <code>root</code> account if desired.</p>
</li>
<li>
<p>Edit <code>/etc/shells</code> and append <code>/usr/gnu/bin/bash</code>
This ensures Bash is recognized as a valid shell.</p>
</li>
<li>
<p>Log out and log back in to start a new Bash session.</p>
</li>
</ol>
<figure style="text-align:center; margin: 1em auto;">
  <img src="next-bash-prompt.png" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
     You should now see the Bash prompt and our PS1 from the .bashrc above:
  </figcaption>
</figure>
<br>
<h2 id="editor">Editor – Vim 5.3 &amp; Termcap Fixes</h2>
<blockquote>
<p><em>“Real editors never die; they just :q!”</em><br>
— NeXT OpenStep Restoration Project, 2025</p></blockquote>
<hr>
<h3 id="overview">Overview</h3>
<p>This guide explains how to install a <strong>fully working Vim 5.3</strong> on <strong>NeXTSTEP / OpenStep 4.2 (Intel)</strong> using a prebuilt GNU Termcap bundle. It replaces NeXT’s broken <code>libtermlib</code> with a proper <code>libtermcap.a</code> and eliminates arrow-key and Delete/Backspace issues.</p>
<p>This was relatively difficult to port but it makes the system so much nicer to use that it was well worth it. If you want to see what I did you can download an unmodified copy of <code>vim</code> 5.3 and diff with mine. There are numerous edits and shims to get it to work. Running <code>vim --version</code> will show the compile options. I wouldn&rsquo;t recommend it as a good time.</p>
<p>The result: Vim is a useful editor and a huge upgrade over the built-in, barebones <code>vi</code></p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="next-vim.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    This will make your Y2K code review much easier 🤣
  </figcaption>
</figure>
<hr>
<h3 id="required-sources">Required Sources</h3>
<p>Before beginning, grab the two prepared packages from the restoration project:</p>
<h4 id="-gnu-termcap-131-for-openstep">📦 GNU Termcap 1.3.1 (for OpenStep)</h4>
<p><a href="https://archive.org/details/termcap131_openstep_bundle"><strong>termcap131_openstep_bundle</strong> — Archive.org</a><br>
Includes <code>libtermcap.a</code>, headers, and man pages rebuilt for OpenStep 4.2 (Intel).<br>
This replaces NeXT’s broken <code>libtermlib.a</code> and restores full key-sequence support.</p>
<h4 id="-vim-53-native-openstep-build">🧰 Vim 5.3 (native OpenStep build)</h4>
<p><a href="https://archive.org/details/vim-5.3-openstep-intel"><strong>vim-5.3-openstep-intel</strong> — Archive.org</a><br>
Contains a fully compiled Vim 5.3 binary, runtime tree, and manual page — ready to install.</p>
<p>These contain the standard source file plus my modifications to get them to build and binaries ready to copy into place.</p>
<hr>
<h3 id="install-the-gnu-termcap-bundle">Install the GNU Termcap Bundle</h3>
<p>Download from Archive.org, ftp to <code>/me/src</code> on the VM and extract:</p>
<pre tabindex="0"><code>su
cd /me/src
gunzip termcap_openstep_bundle.tar.gz
tar -xvf termcap_openstep_bundle.tar

mkdir /usr/local
mkdir /usr/local/lib
mkdir /usr/local/include

cp /me/src/termcap_openstep_bundle/termcap-1.3.1/libtermcap.a /usr/local/lib/
cp /me/src/termcap_openstep_bundle/termcap-1.3.1/termcap.h     /usr/local/include/

cat /me/src/termcap_openstep_bundle/etc_termcap.openstep-safe &gt;&gt; /etc/termcap
</code></pre><h4 id="setting-the-environment-variables">Setting the Environment Variables</h4>
<pre tabindex="0"><code>echo &#39;export TERMCAP=/etc/termcap&#39;   &gt;&gt; ~/.bashrc
echo &#39;export TERM=openstep-safe&#39;     &gt;&gt; ~/.bashrc
. ~/.bashrc
</code></pre><p><em>(If <code>mkdir</code> reports “File exists,” you can safely ignore it.)</em></p>
<hr>
<h3 id="install-vim-53">Install Vim 5.3</h3>
<p>Download from archive.org, ftp the tarball to /me/src on the VM and extract:</p>
<pre tabindex="0"><code>su
cd /me/src

# Extract 
gunzip vim5.3-OpenStep-Intel.tar.gz
tar -xvf vim5.3-OpenStep-Intel.tar
cd vim5.3-OpenStep/


# Create destination paths (no -p on NeXT)
mkdir /usr/local
mkdir /usr/local/bin
mkdir /usr/local/share
mkdir /usr/local/share/vim
mkdir /usr/local/man
mkdir /usr/local/man/man1

# Copy the compiled Vim binary into place
cp src/vim /usr/local/bin/vim
chmod 755 /usr/local/bin/vim

# Copy runtime and support files
cp menu.vim bugreport.vim vimrc_example gvimrc_example /usr/local/share/vim/
cp -R macros syntax termcap tutor /usr/local/share/vim/

# Copy documentation and manpage
cp -R doc /usr/local/share/vim/
cp doc/vim.1 /usr/local/man/man1/
chmod 644 /usr/local/man/man1/vim.1

# Verify the installation
/usr/local/bin/vim --version
</code></pre><p>Ensure <code>/etc/termcap</code> includes the <code>openstep-safe</code> entry<br>
(from the <strong>termcap_openstep_bundle.tar.gz</strong> package).</p>
<p><span class="tag yellow">NOTE</span> Make sure to set your ftp transfer mode correctly during these steps. If you set ASCII and then transfer binary content it will be corrupt. If you set binary when tranferring things like .bashrc or .vimrc, you will see artifacts like ^M and oddly rendered invalid characters. check that if a file doesn&rsquo;t work at first and try again</p>
<h3 id="configure-the-terminal-environment">Configure the Terminal Environment</h3>
<h4 id="update"><strong>Update <code>.bashrc</code></strong></h4>
<p>Your <code>.bashrc</code> should already exist from earlier steps <a href="#shells">Part 1</a>, but confirm that it includes the following lines:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">TERMCAP</span><span class="o">=</span>/etc/termcap
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">TERM</span><span class="o">=</span>openstep-safe
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span><span class="s2">&#34;/usr/local/bin:/usr/etc:/usr/ucb:/usr/gnu/bin:/usr/local/games:/usr/local/bin:/usr/bin:/bin:</span><span class="nv">$PATH</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">stty erase <span class="s1">&#39;^?&#39;</span>
</span></span></code></pre></div><p>If these are missing, add them manually with <code>vi</code> or <code>cat &gt;&gt; ~/.bashrc</code>.</p>
<h4 id="add-man-page-path"><strong>Add man page path</strong></h4>
<p>You should already have this in your .bashrc but if you didn&rsquo;t use the one in Part 1 then make sure to add the following to yours</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;export MANPATH=/usr/local/man:/usr/man&#39;</span> &gt;&gt; ~/.bashrc
</span></span></code></pre></div><p>Reload your configuration:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">. ~/.bashrc
</span></span></code></pre></div><p><em>(Remember that <code>stty erase '^?'</code> ensures the Delete key behaves properly when editing.)</em></p>
<hr>
<h3 id="configure-vim-key-behavior">Configure Vim Key Behavior</h3>
<figure style="text-align:center; margin: 1em auto;">
  <img src="next-acs-bug.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    One of several issues we are working around with these steps involves these ACS artifacts next to the line numbers
  </figcaption>
</figure>
<p>Now that we have a better termcap and the openstep-safe profile to use as our $TERM, we can add a .vimrc to establish some decent settings and most importantly to fix some problem behaviors involving how the cursor, backspace, and delete keys are handled and to suppress some ugly ACS artifacts when displaying line numbers with :set numbers</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <p>If you don’t yet have Vim running or prefer to create the configuration non-interactively, you can generate your <code>.vimrc</code> using a heredoc:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat &gt; ~/.vimrc <span class="s">&lt;&lt; &#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">&#34; ======================================================================
</span></span></span><span class="line"><span class="cl"><span class="s">&#34; ~/.vimrc --  Vim 5.3 on OPENSTEP 4.2
</span></span></span><span class="line"><span class="cl"><span class="s">&#34; forfaxx@adminjitsu.com
</span></span></span><span class="line"><span class="cl"><span class="s">&#34; ======================================================================
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">&#34; ----- Core behavior -----
</span></span></span><span class="line"><span class="cl"><span class="s">set nocompatible
</span></span></span><span class="line"><span class="cl"><span class="s">set autoindent
</span></span></span><span class="line"><span class="cl"><span class="s">set smartindent
</span></span></span><span class="line"><span class="cl"><span class="s">set showmode
</span></span></span><span class="line"><span class="cl"><span class="s">set ruler
</span></span></span><span class="line"><span class="cl"><span class="s">set number
</span></span></span><span class="line"><span class="cl"><span class="s">set nowrap
</span></span></span><span class="line"><span class="cl"><span class="s">set noerrorbells
</span></span></span><span class="line"><span class="cl"><span class="s">set visualbell
</span></span></span><span class="line"><span class="cl"><span class="s">set laststatus=2
</span></span></span><span class="line"><span class="cl"><span class="s">set incsearch
</span></span></span><span class="line"><span class="cl"><span class="s">set ignorecase
</span></span></span><span class="line"><span class="cl"><span class="s">set smartcase
</span></span></span><span class="line"><span class="cl"><span class="s">set hlsearch
</span></span></span><span class="line"><span class="cl"><span class="s">set tabstop=4
</span></span></span><span class="line"><span class="cl"><span class="s">set shiftwidth=4
</span></span></span><span class="line"><span class="cl"><span class="s">set expandtab
</span></span></span><span class="line"><span class="cl"><span class="s">set modeline
</span></span></span><span class="line"><span class="cl"><span class="s">set showcmd
</span></span></span><span class="line"><span class="cl"><span class="s">set backspace=2
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">&#34; ----- Terminal &amp; key behavior -----
</span></span></span><span class="line"><span class="cl"><span class="s">&#34; Backspace/Delete fix
</span></span></span><span class="line"><span class="cl"><span class="s">execute &#34;map! &#34; . nr2char(127) . &#34; &lt;BS&gt;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">execute &#34;nmap &#34; . nr2char(127) . &#34; X&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">inoremap &lt;BS&gt; &lt;Del&gt;
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">&#34; ----- Paste mode toggle -----
</span></span></span><span class="line"><span class="cl"><span class="s">&#34; Vim 5.3 lacks &#39;pastetoggle&#39;, emulate manually
</span></span></span><span class="line"><span class="cl"><span class="s">nmap &lt;C-P&gt; :set invpaste paste?&lt;CR&gt;
</span></span></span><span class="line"><span class="cl"><span class="s">imap &lt;C-P&gt; &lt;C-O&gt;:set invpaste paste?&lt;CR&gt;
</span></span></span><span class="line"><span class="cl"><span class="s">set paste
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">&#34; ----- Visual tweaks -----
</span></span></span><span class="line"><span class="cl"><span class="s">if has(&#34;syntax&#34;)
</span></span></span><span class="line"><span class="cl"><span class="s">  syntax on
</span></span></span><span class="line"><span class="cl"><span class="s">endif
</span></span></span><span class="line"><span class="cl"><span class="s">set background=dark
</span></span></span><span class="line"><span class="cl"><span class="s">set t_Co=8
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">&#34; Disable ACS line-drawing artifacts (no fillchars in 5.3)
</span></span></span><span class="line"><span class="cl"><span class="s">&#34;if &amp;term == &#34;openstep-safe&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">&#34;  set vt100
</span></span></span><span class="line"><span class="cl"><span class="s">&#34;endif
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">&#34; ----- File handling -----
</span></span></span><span class="line"><span class="cl"><span class="s">set nobackup
</span></span></span><span class="line"><span class="cl"><span class="s">set nowritebackup
</span></span></span><span class="line"><span class="cl"><span class="s">set noswapfile
</span></span></span><span class="line"><span class="cl"><span class="s">set hidden
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">&#34; ----- Convenience mappings -----
</span></span></span><span class="line"><span class="cl"><span class="s">nmap &lt;F2&gt; :w&lt;CR&gt;
</span></span></span><span class="line"><span class="cl"><span class="s">nmap &lt;F3&gt; :q&lt;CR&gt;
</span></span></span><span class="line"><span class="cl"><span class="s">nmap &lt;F4&gt; :wq&lt;CR&gt;
</span></span></span><span class="line"><span class="cl"><span class="s">nmap &lt;F12&gt; :e $HOME/.vimrc&lt;CR&gt;
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span></span></span></code></pre></div>
  </div>
</details>

<p><code>.vimrc</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-vimrc" data-lang="vimrc"><span class="line"><span class="cl"><span class="c">&#34; ======================================================================</span>
</span></span><span class="line"><span class="cl"><span class="c">&#34; ~/.vimrc --  Vim 5.3 on OPENSTEP 4.2</span>
</span></span><span class="line"><span class="cl"><span class="c">&#34; forfaxx@adminjitsu.com</span>
</span></span><span class="line"><span class="cl"><span class="c">&#34; ======================================================================</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; ----- Core behavior -----</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">nocompatible</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">autoindent</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">smartindent</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">showmode</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">ruler</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">number</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">nowrap</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">noerrorbells</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">visualbell</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">laststatus</span><span class="p">=</span><span class="m">2</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">incsearch</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">ignorecase</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">smartcase</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">hlsearch</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">tabstop</span><span class="p">=</span><span class="m">4</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">shiftwidth</span><span class="p">=</span><span class="m">4</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">expandtab</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">modeline</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">showcmd</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">backspace</span><span class="p">=</span><span class="m">2</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; ----- Terminal &amp; key behavior -----</span>
</span></span><span class="line"><span class="cl"><span class="c">&#34; Backspace/Delete fix</span>
</span></span><span class="line"><span class="cl"><span class="c">&#34;map! ^? &lt;BS&gt;           &#34; make DEL behave like Backspace</span>
</span></span><span class="line"><span class="cl"><span class="c">&#34;map  ^? X              &#34; delete left in normal mode</span>
</span></span><span class="line"><span class="cl"><span class="c">&#34;map! &lt;BS&gt; &lt;Del&gt;        &#34; optional: swap behavior if needed</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">execute</span> <span class="s2">&#34;map! &#34;</span> . <span class="nx">nr2char</span><span class="p">(</span><span class="m">127</span><span class="p">)</span> . <span class="s2">&#34; &lt;BS&gt;&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">execute</span> <span class="s2">&#34;nmap &#34;</span> . <span class="nx">nr2char</span><span class="p">(</span><span class="m">127</span><span class="p">)</span> . <span class="s2">&#34; X&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">inoremap</span> <span class="p">&lt;</span><span class="nx">BS</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nx">Del</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; ----- Paste mode toggle -----</span>
</span></span><span class="line"><span class="cl"><span class="c">&#34; Vim 5.3 lacks &#39;pastetoggle&#39;, emulate it manually</span>
</span></span><span class="line"><span class="cl"><span class="nx">nmap</span> <span class="p">&lt;</span><span class="nx">C</span><span class="p">-</span><span class="nx">P</span><span class="p">&gt;</span> :<span class="k">set</span> <span class="nx">invpaste</span> <span class="nx">paste</span>?<span class="p">&lt;</span><span class="nx">CR</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nx">imap</span> <span class="p">&lt;</span><span class="nx">C</span><span class="p">-</span><span class="nx">P</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nx">C</span><span class="p">-</span><span class="nx">O</span><span class="p">&gt;</span>:<span class="k">set</span> <span class="nx">invpaste</span> <span class="nx">paste</span>?<span class="p">&lt;</span><span class="nx">CR</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">paste</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; ----- Visual tweaks -----</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">has</span><span class="p">(</span><span class="s2">&#34;syntax&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">syntax</span> <span class="nx">on</span>
</span></span><span class="line"><span class="cl"><span class="k">endif</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">background</span><span class="p">=</span><span class="nb">dark</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">t_Co</span><span class="p">=</span><span class="m">8</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; Disable ACS line-drawing artifacts (no fillchars in 5.3)</span>
</span></span><span class="line"><span class="cl"><span class="c">&#34;if &amp;term == &#34;openstep-safe&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c">&#34;  set vt100</span>
</span></span><span class="line"><span class="cl"><span class="c">&#34;endif</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; ----- File handling -----</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">nobackup</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">nowritebackup</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">noswapfile</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">hidden</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; ----- Convenience mappings -----</span>
</span></span><span class="line"><span class="cl"><span class="nx">nmap</span> <span class="p">&lt;</span><span class="nx">F2</span><span class="p">&gt;</span> :<span class="nx">w</span><span class="p">&lt;</span><span class="nx">CR</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nx">nmap</span> <span class="p">&lt;</span><span class="nx">F3</span><span class="p">&gt;</span> :<span class="nx">q</span><span class="p">&lt;</span><span class="nx">CR</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nx">nmap</span> <span class="p">&lt;</span><span class="nx">F4</span><span class="p">&gt;</span> :<span class="nx">wq</span><span class="p">&lt;</span><span class="nx">CR</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nx">nmap</span> <span class="p">&lt;</span><span class="nx">F12</span><span class="p">&gt;</span> :<span class="nx">e</span> $<span class="nx">HOME</span>/.<span class="nx">vimrc</span><span class="p">&lt;</span><span class="nx">CR</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>At this point you should have a working vim with good termcap behaviors just like in the VirtualBox ova appliance. One thing that really helps when trying to copy and paste through vim over telnet is to hit escape to enter command mode then the colon and the command :set paste. You can then paste in long text passages like the .vimrc and .bashrc without vim mangling the indentation. When you&rsquo;re done you can save with :wq or :w and enter :set nopaste to disable.</p>
<p>In the .vimrc above, I have mapped Ctrl-P to toggle between paste modes (which disables auto-indent and line wrapping and other insert mode features that get in the way)</p>
<br>
<p><span class="tag green">TIP</span>
Before experimenting further, make a snapshot of your VM. OpenStep’s filesystems can be delicate — you’ll thank yourself later.</p>
<br>
<h2 id="adding-fileutils-and-textutils">Adding Fileutils and Textutils</h2>
<p>To complete the core UNIX userland on OPENSTEP 4.2, you’ll want modern implementations of the GNU <strong>Fileutils</strong> and <strong>Textutils</strong> packages. These provide many of the essential commands expected on a contemporary UNIX system — including <code>ls</code>, <code>cp</code>, <code>mv</code>, <code>sort</code>, <code>uniq</code>, and more — all built natively for the NeXT environment.</p>
<h3 id="required-sources-1">Required Sources</h3>
<ul>
<li><a href="https://ftp.gnu.org/old-gnu/fileutils/fileutils-3.16.tar.gz">fileutils-3.16.tar.gz</a></li>
<li><a href="https://ftp.gnu.org/old-gnu/textutils/textutils-1.19.tar.gz">textutils-1.19.tar.gz</a></li>
</ul>
<p>Both archives are available from GNU mirrors or preservation repositories (such as <a href="https://ftp.gnu.org">ftp.gnu.org</a> mirrors or Archive.org).</p>
<h3 id="building-and-installing">Building and Installing</h3>
<p><strong>fileutils</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">su
</span></span><span class="line"><span class="cl">gunzip fileutils-3.16.tar.gz
</span></span><span class="line"><span class="cl">tar xvf fileutils-3.16.tar
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> fileutils-3.16
</span></span><span class="line"><span class="cl">sh ./configure --prefix<span class="o">=</span>/usr/local
</span></span><span class="line"><span class="cl">make
</span></span><span class="line"><span class="cl">make install
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ..
</span></span></code></pre></div><p><strong>textutils</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">gunzip textutils-1.19.tar.gz
</span></span><span class="line"><span class="cl">tar xvf textutils-1.19.tar
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> textutils-1.19
</span></span><span class="line"><span class="cl">sh ./configure --prefix<span class="o">=</span>/usr/local
</span></span><span class="line"><span class="cl">make
</span></span><span class="line"><span class="cl">make install
</span></span><span class="line"><span class="cl"><span class="nb">exit</span>
</span></span></code></pre></div><h3 id="installed-tools">Installed Tools</h3>
<p>After installation, <code>/usr/local/bin</code> should contain a full suite of GNU utilities, including:</p>
<pre tabindex="0"><code>cat        comm       df         fmt        ln         mknod      pr         strfile    touch      vdir
chgrp      cp         dir        fold       ls         mv         rm         sum        tr         vim
chmod      csplit     dircolors  head       md5sum     nl         rmdir      sync       unexpand   wc
chown      cut        du         install    mkdir      od         sort       tac        uniq
cksum      dd         expand     join       mkfifo     paste      split      tail       unstr
</code></pre><p>With these installed, OPENSTEP behaves much more like a late-90s GNU environment, allowing most shell scripts and build tools to run without modification.</p>
<p><span class="tag green">TIP</span>
If you&rsquo;ve followed this tutorial in order, <code>/usr/local/bin</code> should appear at the <strong>beginning</strong> of your <code>$PATH</code>.<br>
On NeXT, the <code>which</code> command uses hard-coded search paths — instead, run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="nv">$PATH</span>
</span></span></code></pre></div><br>
<h2 id="dos2unix-and-unix2dos">dos2unix and unix2dos</h2>
<p>Before beginning, grab the prepared package from the restoration project:</p>
<p>🧩 dos2unix 5.2 (OPENSTEP Port)
<a href="https://archive.org/details/dos2unix-5.2-openstep.tar"><strong>dos2unix-5.2-openstep</strong> — Archive.org</a><br>
Includes fully working <code>dos2unix</code> and <code>unix2dos</code> binaries, source code, and corrected Makefile for OPENSTEP 4.2 (Intel).<br>
This port fixes missing <code>libgen.h</code> and <code>mode_t</code> definitions, adds compatibility stubs for <code>querycp</code>, and ensures clean builds using NeXT’s native <code>cc</code> and <code>/usr/bin/install</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Clean any previous object files or binaries</span>
</span></span><span class="line"><span class="cl">make clean
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Build both dos2unix and unix2dos</span>
</span></span><span class="line"><span class="cl">make
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Install the binaries and man pages as root</span>
</span></span><span class="line"><span class="cl">su
</span></span><span class="line"><span class="cl">make install
</span></span><span class="line"><span class="cl"><span class="nb">exit</span>
</span></span></code></pre></div><p>The updated Makefile for OPENSTEP automatically handles:</p>
<ul>
<li>Ensuring <code>/usr/local/bin</code> and <code>/usr/local/man/man1</code> exist before installation</li>
<li>Installing the binaries:
<ul>
<li><code>/usr/local/bin/dos2unix</code></li>
<li><code>/usr/local/bin/unix2dos</code></li>
</ul>
</li>
<li>Installing the manpage (if present):
<ul>
<li><code>/usr/local/man/man1/dos2unix.1</code></li>
<li>(Unix2dos uses the same manpage; it’s symlinked internally)</li>
</ul>
</li>
</ul>
<p>You can verify both tools work:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Create a DOS-style test file</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> -e <span class="s2">&#34;Hello world.\r\nThis is a DOS line.\r\nAnd another.\r\n&#34;</span> &gt; test_dos.txt
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Convert it to Unix format</span>
</span></span><span class="line"><span class="cl">/usr/local/bin/dos2unix test_dos.txt
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Inspect it</span>
</span></span><span class="line"><span class="cl">od -c test_dos.txt
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Convert it back to DOS</span>
</span></span><span class="line"><span class="cl">/usr/local/bin/unix2dos test_dos.txt
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Inspect it again</span>
</span></span><span class="line"><span class="cl">od -c test_dos.txt
</span></span></code></pre></div><p>✅ <strong>Notes</strong></p>
<ul>
<li>Both programs are fully self-contained and compile cleanly under the OPENSTEP developer tools.</li>
<li>The Makefile uses <code>/usr/bin/install</code> for compatibility with NeXT’s environment (no <code>-p</code> flag).</li>
<li>NeXT lacks pod2man so I bundled the resulting manpage from linux.</li>
<li>This version replaces the missing <code>libgen.h</code>, <code>mode_t</code>, and <code>querycp</code> functions with portable local definitions.</li>
<li>You can safely re-run <code>make install</code>; it will overwrite binaries but not break the environment.</li>
</ul>
<br>
<h2 id="grep">Replacing the Broken grep with GNU grep 2.2</h2>
<p>The stock <code>grep</code> shipped with OpenStep 4.2 is badly broken — <code>-q</code>, <code>-v</code>, and long options fail outright.<br>
We can replace it with a working <strong>GNU grep 2.2</strong> built natively using the NeXT toolchain.</p>
<p>Download the verified working build from Archive.org:</p>
<p class="github-btn">
  <a href="https://archive.org/details/grep-2.2-openstep-intel" target="_blank">
    📦 Download grep-2.2-openstep-intel
  </a>
</p>
<p>Upload the tarball to <code>/me/src</code> on your VM using <strong>binary</strong> mode FTP</p>
<p><strong>Steps</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> ~/src
</span></span><span class="line"><span class="cl">gunzip grep-2.2.tar.gz
</span></span><span class="line"><span class="cl">tar -xvf grep-2.2.tar
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> grep-2.2
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># fix timestamps and permissions</span>
</span></span><span class="line"><span class="cl">find . -exec touch <span class="o">{}</span> <span class="se">\;</span>
</span></span><span class="line"><span class="cl">chmod u+x configure
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># configure and build</span>
</span></span><span class="line"><span class="cl">./configure --prefix<span class="o">=</span>/usr/local --disable-nls
</span></span><span class="line"><span class="cl">make <span class="nv">CC</span><span class="o">=</span><span class="s2">&#34;/bin/cc&#34;</span> <span class="nv">LD</span><span class="o">=</span><span class="s2">&#34;/bin/ld&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>     <span class="nv">CFLAGS</span><span class="o">=</span><span class="s2">&#34;-O2 -arch i486 -traditional-cpp&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>     <span class="nv">LDFLAGS</span><span class="o">=</span><span class="s2">&#34;-lSystem -lc&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># test the result</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> src
</span></span><span class="line"><span class="cl">./grep --version
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> foo <span class="p">|</span> ./grep -q foo <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">&#34;grep works&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># install</span>
</span></span><span class="line"><span class="cl">su
</span></span><span class="line"><span class="cl">make install
</span></span><span class="line"><span class="cl"><span class="nb">exit</span>
</span></span></code></pre></div><p>This installs the new binaries and manpages to:</p>
<pre tabindex="0"><code>/usr/local/bin/grep
/usr/local/bin/egrep
/usr/local/bin/fgrep
/usr/local/man/man1/grep.1
</code></pre><p><span class="tag blue">NOTE</span><br>
No need to replace the system binary — as long as <code>/usr/local/bin</code> appears <strong>before</strong> <code>/bin</code> in your <code>$PATH</code>, your shell will automatically prefer the new version. If you’ve followed the earlier bash configuration, you’re already covered.</p>
<p><strong>Verification</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">grep --version
</span></span><span class="line"><span class="cl">grep -q foo <span class="o">&lt;&lt;&lt;</span> foo <span class="o">&amp;&amp;</span> <span class="nb">echo</span> OK
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> -e <span class="s2">&#34;foo\nbar&#34;</span> <span class="p">|</span> grep -v foo
</span></span></code></pre></div><p>Expected output:</p>
<pre tabindex="0"><code>grep (GNU grep) 2.2
OK
bar
</code></pre><p>✅ Your OpenStep environment now has a fully functional <code>grep</code> with standard GNU options, completing the core text-processing toolchain alongside <code>textutils</code> and <code>fileutils</code>.</p>
<br>
<h2 id="cmd-top">top</h2>
<p>Now we&rsquo;ll add <strong>top</strong> to make it easier to keep an eye on resource usage and processes.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="top-on-next.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    top is exactly the kind of thing a 90s NeXT admin would have downloaded and run
  </figcaption>
</figure>
<p>You can grab a prebuilt binary from 🔗 <a href="https://fsck.technology/software/NeXT/next.68k.org%20Archive/otto/html/pub/Unix/Utils4admin/top.0.5.NI.b.tar.gz">fsck.technology</a></p>
<p>Transfer it to <code>/me/src</code> on your VM and unpack:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">su
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> /me/src
</span></span><span class="line"><span class="cl">gunzip top.0.5.NI.b.tar.gz
</span></span><span class="line"><span class="cl">tar -xvf top.0.5.NI.b.tar
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> top-v0.5
</span></span></code></pre></div><p>Copy the binary and set the proper ownership and permissions so it can read kernel memory:</p>
<p><span class="tag yellow">NOTE</span> You don’t see this much on modern systems — with <code>sudo</code>, privilege separation, and ACLs doing the heavy lifting — but <strong>setuid binaries</strong> were once a common (and slightly terrifying) way to let normal users perform privileged actions safely.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cp top /usr/local/bin/top
</span></span><span class="line"><span class="cl">chmod <span class="m">4755</span> /usr/local/bin/top
</span></span><span class="line"><span class="cl">chown root.kmem /usr/local/bin/top
</span></span><span class="line"><span class="cl">ls -al /usr/local/bin/top
</span></span></code></pre></div><p>There’s also a man page included — install it to <code>/usr/local/man/man1</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir /usr/local/man/man1 2&gt;/dev/null
</span></span><span class="line"><span class="cl">cp top.1 /usr/local/man/man1/
</span></span><span class="line"><span class="cl">chmod <span class="m">644</span> /usr/local/man/man1/top.1
</span></span></code></pre></div><p>Test it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">man top
</span></span><span class="line"><span class="cl">top
</span></span></code></pre></div><p>If everything’s right, you’ll see a live process list with CPU and memory stats.<br>
Press <strong>q</strong> to exit.</p>
<br>
<h2 id="x11r6">CUBX X11R6</h2>
<figure style="text-align:center; margin: 1em auto;">
  <img src="next-x11r6.png" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    X11R6 on NeXT is a nice retro-modern addition to the system. 
  </figcaption>
</figure>
<p>This was easy to set up. Just grab the iso and serial number from archive.org or from the NeXT archive <a href="https://fsck.technology/software/NeXT/OpenStep%20Applications/CUBX%20Windows%205.0%20for%20OpenStep/">here</a></p>
<p><strong>Steps</strong></p>
<ul>
<li>login as root</li>
<li>attach the iso to Virtualbox</li>
<li>when it appears in the file viewer, click on the cd and double-click on <code>ISInstaller.app</code>
This will prompt for the serial number</li>
<li>You probably want to choose Client when prompted.</li>
<li>The installer will then install and configure the system</li>
</ul>
<blockquote>
<p><span class="tag yellow">NOTE</span> When installing <strong>CUB&rsquo;X Windows 5</strong>, you can choose <strong>Server</strong> (to <em>receive</em> remote X11 apps drawn on your OpenStep desktop) or <strong>Client</strong> (to <em>send</em> X apps from OpenStep to another display).<br>
Use the server mode if you want to integrate modern Unix apps into your NeXT workspace; use the client mode if you prefer to open OpenStep X programs on another host’s screen.</p>
<p>See <a href="https://wiki.archlinux.org/title/X_display#Displaying_remote_X11_applications">X11 display forwarding explained</a> for a solid overview of how exported displays work.</p></blockquote>
<p>This gives you xterm which is much nicer to use than the built in Terminal as well as a host of standard x11 apps and the ability to build more (you&rsquo;ll want to look for releases from 1994-1999 typically that would have been contemporaneous )</p>
<p>Try running <code>xeyes &amp;</code> and enjoy having xterm available.</p>
<p><span class="tag green">TIP</span>
Before experimenting further, make a snapshot of your VM. This is a good place to save.</p>
<br>
<h2 id="w3c-httpd">w3c-httpd</h2>
<p>This is version 3.0A of the original <strong>CERN httpd</strong> web server — the first web server developed by Tim Berners-Lee on NeXTSTEP.<br>
This port has been rebuilt and tested natively under <strong>OPENSTEP 4.2 Intel</strong>, using the original toolchain (<code>cc</code> = gcc 2.7.2.1) with no GNU dependencies.</p>
<hr>
<h3 id="required-sources-2">Required Sources</h3>
<p>Download the verified working build from Archive.org:</p>
<p class="github-btn">
  <a href="https://archive.org/details/w3c-httpd-3.0A.next.i486" target="_blank">
    📦 Download w3c-httpd-3.0A.next.i486.tar.gz
  </a>
</p>
<p><a href="https://archive.org/download/w3c-httpd-3.0A.next.i486/w3c-httpd-3.0A.next.i486.tar.gz">https://archive.org/download/w3c-httpd-3.0A.next.i486/w3c-httpd-3.0A.next.i486.tar.gz</a></p>
<p>Upload the tarball to <code>/me/src</code> on your VM using <strong>binary</strong> mode (FTP or NFS).</p>
<hr>
<h3 id="installing-cern-httpd-30a">Installing CERN httpd 3.0A</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Navigate to your source directory</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> /me/src
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Extract the source archive</span>
</span></span><span class="line"><span class="cl">gunzip cern-httpd-3.0A-openstep-bin.tar.gz
</span></span><span class="line"><span class="cl">tar -xvf cern-httpd-3.0A-openstep-bin.tar
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> w3c-httpd-3.0A
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Become root</span>
</span></span><span class="line"><span class="cl">su
</span></span></code></pre></div><p>Create the target install layout:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p /usr/local/httpd/bin
</span></span><span class="line"><span class="cl">mkdir -p /usr/local/httpd/lib
</span></span><span class="line"><span class="cl">mkdir -p /usr/local/httpd/conf
</span></span><span class="line"><span class="cl">mkdir -p /usr/local/httpd/logs
</span></span><span class="line"><span class="cl">mkdir -p /usr/local/httpd/html
</span></span></code></pre></div><p><span class="tag gray">NOTE</span> <em>If you haven&rsquo;t followed the previous steps in this tutorial and installed fileutils, you will need to run mkdir without <code>-p</code> and create both <code>/usr</code>, <code>/usr/local</code> and <code>/usr/local/httpd</code> manually before creating the subdirs in the steps above.</em></p>
<p>Install the compiled binaries and library:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">install -m <span class="m">0755</span> Daemon/next/httpd /usr/local/httpd/bin/httpd
</span></span><span class="line"><span class="cl">install -m <span class="m">0755</span> Daemon/next/htadm /usr/local/httpd/bin/htadm
</span></span><span class="line"><span class="cl">install -m <span class="m">0755</span> Daemon/next/htimage /usr/local/httpd/bin/htimage
</span></span><span class="line"><span class="cl">install -m <span class="m">0755</span> Daemon/next/cgiparse /usr/local/httpd/bin/cgiparse
</span></span><span class="line"><span class="cl">install -m <span class="m">0755</span> Daemon/next/cgiutils /usr/local/httpd/bin/cgiutils
</span></span><span class="line"><span class="cl">install -m <span class="m">0644</span> Library/next/libwww.a /usr/local/httpd/lib/libwww.a
</span></span></code></pre></div><p>Create a working configuration file for port 80 and user pages:</p>
<p><span class="tag red">WARNING</span> <strong>About Port 80 and the nobody user:</strong><br>
Binding to port 80 requires root privileges, but the daemon drops to the <code>nobody</code> user after startup for security. The <code>HostName</code> directive should match your VM&rsquo;s actual IP address — adjust <code>192.168.1.50</code> to your network configuration. If you prefer to avoid running as root, change <code>Port 80</code> to <code>Port 8080</code> (or any port above 1024) and start the daemon as a regular user.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat &gt;/usr/local/httpd/conf/httpd.conf <span class="s">&lt;&lt;&#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">ServerRoot   /usr/local/httpd
</span></span></span><span class="line"><span class="cl"><span class="s">Port         80
</span></span></span><span class="line"><span class="cl"><span class="s">User         nobody
</span></span></span><span class="line"><span class="cl"><span class="s">Group        nobody
</span></span></span><span class="line"><span class="cl"><span class="s">HostName     192.168.1.50
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">DocumentRoot /usr/local/httpd/html
</span></span></span><span class="line"><span class="cl"><span class="s">LogFile      /usr/local/httpd/logs/access_log
</span></span></span><span class="line"><span class="cl"><span class="s">ErrorLog     /usr/local/httpd/logs/error_log
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">Protect      / none
</span></span></span><span class="line"><span class="cl"><span class="s">Pass         /      /usr/local/httpd/html/*
</span></span></span><span class="line"><span class="cl"><span class="s">Pass         /~me*  /me/public_html/*
</span></span></span><span class="line"><span class="cl"><span class="s">Protect      /~me*  none
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span></code></pre></div><p>And a test HTML document:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p /me/public_html
</span></span><span class="line"><span class="cl">chmod <span class="m">755</span> /me /me/public_html
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">cat &gt;/usr/local/httpd/html/index.html <span class="s">&lt;&lt;&#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">&lt;!DOCTYPE html&gt;
</span></span></span><span class="line"><span class="cl"><span class="s">&lt;html&gt;&lt;body&gt;&lt;h1&gt;Welcome to CERN httpd 3.0A on OpenStep!&lt;/h1&gt;&lt;/body&gt;&lt;/html&gt;
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">cat &gt;/me/public_html/index.html <span class="s">&lt;&lt;&#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">&lt;!DOCTYPE html&gt;
</span></span></span><span class="line"><span class="cl"><span class="s">&lt;html&gt;&lt;body&gt;&lt;h1&gt;Hello from ~me on OpenStep!&lt;/h1&gt;&lt;/body&gt;&lt;/html&gt;
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span></code></pre></div><hr>
<h3 id="running-the-server">Running the Server</h3>
<p>Start the daemon manually:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">/usr/local/httpd/bin/httpd <span class="p">&amp;</span>
</span></span></code></pre></div><p>You should see output similar to:</p>
<pre tabindex="0"><code>............ This is CERN-HTTPD, version 3.0A, using libwww version 2.17
ServerType.. standalone
Reading..... /usr/local/httpd/conf/httpd.conf
</code></pre><p>Verify the process:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ps aux <span class="p">|</span> grep httpd
</span></span></code></pre></div><p>Check logs:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">tail -n <span class="m">5</span> /usr/local/httpd/logs/error_log
</span></span></code></pre></div><hr>
<h3 id="testing">Testing</h3>
<p>From your host or another VM:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl http://192.168.1.50/
</span></span><span class="line"><span class="cl">curl http://192.168.1.50/~me/
</span></span></code></pre></div><p>Expected result:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>Welcome to CERN httpd 3.0A on OpenStep!<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>Hello from ~me on OpenStep!<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>If you’re using OmniWeb, Mosaic, or an early Netscape build, both pages should render correctly.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="next-webserver.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    You too can have a 90s website again! Just need some flashing under construction gifs
  </figcaption>
</figure>
<hr>
<h3 id="auto-start-at-boot">Auto-start at Boot</h3>
<p>Add CERN httpd to <code>/etc/rc.local</code> so it launches automatically after networking:</p>
<p><span class="tag red">WARNING</span> <strong>Security consideration:</strong><br>
This configuration runs httpd as root initially to bind port 80, then drops to <code>nobody</code>. On a private network VM this is acceptable, but <strong>never expose this server to the public internet</strong> — it lacks modern security features and uses unencrypted HTTP only. Consider using port 8080 or adding this VM to an isolated network segment.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat &gt;&gt;/etc/rc.local <span class="s">&lt;&lt;&#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s"># Start CERN httpd 3.0A
</span></span></span><span class="line"><span class="cl"><span class="s">if [ -x /usr/local/httpd/bin/httpd ]; then
</span></span></span><span class="line"><span class="cl"><span class="s">    echo &#34;Starting CERN httpd 3.0A...&#34; &gt;/dev/console
</span></span></span><span class="line"><span class="cl"><span class="s">    /usr/local/httpd/bin/httpd &amp;
</span></span></span><span class="line"><span class="cl"><span class="s">fi
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">chmod +x /etc/rc.local
</span></span></code></pre></div><p>You can test immediately without rebooting:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">/etc/rc.local
</span></span></code></pre></div><p>Confirm it’s running:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ps aux <span class="p">|</span> grep httpd
</span></span></code></pre></div><p>At the next reboot, the daemon will start automatically and serve your pages from<br>
<code>http://192.168.1.50/</code> and <code>http://192.168.1.50/~me/</code>.</p>
<hr>
<h3 id="notes">Notes</h3>
<ul>
<li>This build uses native OpenStep tools only — no GNU Make, no bash, no BSD ports.</li>
<li>Configuration format matches CERN httpd 3.x syntax exactly.</li>
<li>Runs safely in standalone mode on port 80.</li>
<li>Logs and HTML documents live under <code>/usr/local/httpd/</code> for simplicity.</li>
</ul>
<p>You now have a fully installed and operational <strong>CERN httpd 3.0A</strong> daemon, serving web content from OpenStep just like the first webserver.</p>
<p><span class="tag green">TIP</span>
This is another friendly reminder to save a snapshot in VirtualBox!</p>
<br>
<h2 id="fortune">fortune-mod 9708 &amp; Custom Sets</h2>
<p>A UNIX system without <code>fortune</code> is a sterile, joyless place. I even wrote an entire post celebrating its shenanigans —<br>
<a href="/posts/fortune-favors-the-sysadmin/"><strong>Fortune Favors the Sysadmin</strong></a>.</p>
<p>So naturally, we’re going to need a working copy on <strong>OPENSTEP</strong>.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="next-fortune-gui-app.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    There is a fortune for NeXT pkg out there but it installs a gui app with some hard-coded fortunes.<br> Neat but we'll be installing fortune-mod with everything included
  </figcaption>
</figure>
<hr>
<h3 id="required-sources-3">Required Sources</h3>
<p>Start by grabbing my fortune-mod-9708 port from archive.org</p>
<p class="github-btn">
  <a href="https://archive.org/details/fortune-mod-9708.OPENSTEP4.2.i486" target="_blank">
    📦 Download fortune-mod-9708 for OPENSTEP 4.2
  </a>
</p>
<br>
<hr>
<h3 id="building-and-installing-fortune-mod-9708-port">Building and Installing <code>fortune-mod-9708</code> port</h3>
<p>Set FTP transfer mode to <strong>binary</strong> and upload <code>fortune-mod-9708-openstep.tar.gz</code> to <code>/me/src</code> on the VM.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Navigate to your source directory</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> /me/src
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Extract the source archive</span>
</span></span><span class="line"><span class="cl">gunzip fortune-mod-9708-openstep.tar.gz
</span></span><span class="line"><span class="cl">tar -xvf fortune-mod-9708-openstep.tar
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> fortune-mod-9708-openstep
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Clean any previous builds</span>
</span></span><span class="line"><span class="cl">make clean
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Build the binaries and cookie databases</span>
</span></span><span class="line"><span class="cl">make
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Install everything as root (fortune, utilities, manpages, and data)</span>
</span></span><span class="line"><span class="cl">su
</span></span><span class="line"><span class="cl">make install
</span></span><span class="line"><span class="cl"><span class="nb">exit</span>
</span></span></code></pre></div><p>The <code>install</code> target now handles everything automatically:</p>
<ul>
<li>
<p>Creates required directories if they don’t exist:</p>
<ul>
<li><code>/usr/local/games</code></li>
<li><code>/usr/local/share/games/fortunes</code></li>
<li><code>/usr/local/bin</code></li>
<li><code>/usr/local/man/man6</code></li>
<li><code>/usr/local/man/man1</code></li>
</ul>
</li>
<li>
<p>Installs the compiled binaries:</p>
<ul>
<li><code>/usr/local/games/fortune</code></li>
<li><code>/usr/local/bin/strfile</code></li>
<li><code>/usr/local/bin/unstr</code></li>
</ul>
</li>
<li>
<p>Generates and installs the manpages:</p>
<ul>
<li><code>/usr/local/man/man6/fortune.6</code></li>
<li><code>/usr/local/man/man1/strfile.1</code> (with <code>unstr.1</code> symlink)</li>
</ul>
</li>
<li>
<p>Copies all fortune cookie databases (<code>*.dat</code> and subdirs) into
<code>/usr/local/share/games/fortunes/</code>.</p>
</li>
</ul>
<hr>
<h3 id="verify-it-works-as-expected">Verify it works as expected</h3>
<p>You can verify it works:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">/usr/local/games/fortune
</span></span><span class="line"><span class="cl">/usr/local/games/fortune startrek
</span></span><span class="line"><span class="cl">/usr/local/games/fortune zippy
</span></span></code></pre></div><p>If you want to add your own fortune sets, place them in
<code>datfiles/</code>, then rebuild the indexes:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> datfiles
</span></span><span class="line"><span class="cl">/usr/local/bin/strfile myfortunes
</span></span></code></pre></div><p>Your new set appears automatically under
<code>/usr/local/share/games/fortunes/myfortunes</code>.</p>
<hr>
<p>✅ <strong>Notes</strong></p>
<ul>
<li>This Makefile is OPENSTEP-compatible and idempotent: re-running <code>make install</code> won’t break existing installs.</li>
<li>You no longer need to manually copy files or run <code>strfile</code> — the build handles everything.</li>
<li>Manpages and directories are generated cleanly with the correct permissions.</li>
</ul>
<br>
<blockquote>
<p>&ldquo;Absolutum obsoletum.  (If it works, it&rsquo;s out of date.)&rdquo;
&ndash; Stafford Beer</p></blockquote>
<br>
<h2 id="gui-apps">Installing GUI Apps &amp; Packages</h2>
<p>There are a ton of downloads available on archive.org and fsck.technology. They tend to come in a few flavors:</p>
<ul>
<li>
<p>tarballs (file.tar.gz)</p>
<ul>
<li>you want ones with NIHS, NI and/or b or bs in the title</li>
<li>These will contain a README or INSTALL with instructions</li>
<li>typically you&rsquo;ll use something like make install as directed by the documentation in the archive.</li>
</ul>
</li>
<li>
<p>tarballs containing source only, not ported to NeXT. You&rsquo;ll have to figure these out. Some are easy, some aren&rsquo;t. Try to find releases from around 1995-1998</p>
</li>
<li>
<p><code>.pkg</code> files.
Login as root and double-click the .pkg file to run the installer
alternatively you can launch with /NextAdmin/Installer.app/Installer packagename.pkg</p>
</li>
<li>
<p><code>.app</code> bundles
As long as it contains an Intel Mach binary, these can be run from anywhere.
You probably want to copy these to either /me/Apps or /LocalApps or even /NextApps</p>
</li>
</ul>
<br>
<h2 id="nmap">Running <code>nmap</code> Against nextcube</h2>
<p>When you scan nextcube from a modern host:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">nmap -A -p- -T4 -v nextcube
</span></span></code></pre></div><p>It lights up like a slot machine of open ports — Telnet, FTP, Finger, rlogin, exec, SMTP, and friends.<br>
A living exhibit of 90s networking.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">┌──[ grumble@shinobi ]:~/codelab/adminjitsu  (main)
</span></span><span class="line"><span class="cl">└─$ nmap -A -p- -T4 -v nextcube.darkstar.home
</span></span><span class="line"><span class="cl">Starting Nmap 7.80 ( https://nmap.org ) at 2025-11-03 17:14 CST
</span></span><span class="line"><span class="cl">NSE: Loaded 151 scripts for scanning.
</span></span><span class="line"><span class="cl">NSE: Script Pre-scanning.
</span></span><span class="line"><span class="cl">Initiating NSE at 17:14
</span></span><span class="line"><span class="cl">Completed NSE at 17:14, 0.00s elapsed
</span></span><span class="line"><span class="cl">Initiating Ping Scan at 17:14
</span></span><span class="line"><span class="cl">Scanning nextcube.darkstar.home (192.168.1.50) [2 ports]
</span></span><span class="line"><span class="cl">Completed Ping Scan at 17:14, 0.20s elapsed (1 total hosts)
</span></span><span class="line"><span class="cl">Initiating Parallel DNS resolution of 1 host. at 17:14
</span></span><span class="line"><span class="cl">Completed Parallel DNS resolution of 1 host. at 17:14, 0.01s elapsed
</span></span><span class="line"><span class="cl">Initiating Connect Scan at 17:14
</span></span><span class="line"><span class="cl">Scanning nextcube.darkstar.home (192.168.1.50) [65535 ports]
</span></span><span class="line"><span class="cl">Discovered open port 23/tcp on 192.168.1.50
</span></span><span class="line"><span class="cl">Discovered open port 111/tcp on 192.168.1.50
</span></span><span class="line"><span class="cl">Discovered open port 25/tcp on 192.168.1.50
</span></span><span class="line"><span class="cl">Discovered open port 21/tcp on 192.168.1.50
</span></span><span class="line"><span class="cl">Discovered open port 19/tcp on 192.168.1.50
</span></span><span class="line"><span class="cl">Discovered open port 7/tcp on 192.168.1.50
</span></span><span class="line"><span class="cl">Discovered open port 79/tcp on 192.168.1.50
</span></span><span class="line"><span class="cl">Discovered open port 2453/tcp on 192.168.1.50
</span></span><span class="line"><span class="cl">Discovered open port 706/tcp on 192.168.1.50
</span></span><span class="line"><span class="cl">Discovered open port 13/tcp on 192.168.1.50
</span></span><span class="line"><span class="cl">Discovered open port 513/tcp on 192.168.1.50
</span></span><span class="line"><span class="cl">Discovered open port 515/tcp on 192.168.1.50
</span></span><span class="line"><span class="cl">Discovered open port 514/tcp on 192.168.1.50
</span></span><span class="line"><span class="cl">Discovered open port 37/tcp on 192.168.1.50
</span></span><span class="line"><span class="cl">Discovered open port 512/tcp on 192.168.1.50
</span></span><span class="line"><span class="cl">Discovered open port 9/tcp on 192.168.1.50
</span></span><span class="line"><span class="cl">Discovered open port 709/tcp on 192.168.1.50
</span></span><span class="line"><span class="cl">Discovered open port 178/tcp on 192.168.1.50
</span></span><span class="line"><span class="cl">Completed Connect Scan at 17:25, 677.52s elapsed (65535 total ports)
</span></span><span class="line"><span class="cl">Initiating Service scan at 17:25
</span></span><span class="line"><span class="cl">Scanning 18 services on nextcube.darkstar.home (192.168.1.50)
</span></span><span class="line"><span class="cl">Completed Service scan at 17:28, 163.85s elapsed (18 services on 1 host)
</span></span><span class="line"><span class="cl">NSE: Script scanning 192.168.1.50.
</span></span><span class="line"><span class="cl">Initiating NSE at 17:28
</span></span><span class="line"><span class="cl">Completed NSE at 17:28, 29.77s elapsed
</span></span><span class="line"><span class="cl">Nmap scan report for nextcube.darkstar.home (192.168.1.50)
</span></span><span class="line"><span class="cl">Host is up (0.0054s latency).
</span></span><span class="line"><span class="cl">Not shown: 65497 closed ports
</span></span><span class="line"><span class="cl">PORT      STATE    SERVICE    VERSION
</span></span><span class="line"><span class="cl">7/tcp     open     echo
</span></span><span class="line"><span class="cl">9/tcp     open     discard?
</span></span><span class="line"><span class="cl">13/tcp    open     daytime    Sun Solaris daytime
</span></span><span class="line"><span class="cl">19/tcp    open     chargen
</span></span><span class="line"><span class="cl">21/tcp    open     ftp
</span></span><span class="line"><span class="cl">| fingerprint-strings:
</span></span><span class="line"><span class="cl">|   GenericLines:
</span></span><span class="line"><span class="cl">|     220 nextcube FTP server (Version 5.1 (NeXT 1.0) Tue Jan 26, 1999) ready.
</span></span><span class="line"><span class="cl">|     command not understood.
</span></span><span class="line"><span class="cl">|     command not understood.
</span></span><span class="line"><span class="cl">|   NULL, SMBProgNeg:
</span></span><span class="line"><span class="cl">|     220 nextcube FTP server (Version 5.1 (NeXT 1.0) Tue Jan 26, 1999) ready.
</span></span><span class="line"><span class="cl">|   SSLSessionReq:
</span></span><span class="line"><span class="cl">|     220 nextcube FTP server (Version 5.1 (NeXT 1.0) Tue Jan 26, 1999) ready.
</span></span><span class="line"><span class="cl">|_    command not understood.
</span></span><span class="line"><span class="cl">| ftp-syst:
</span></span><span class="line"><span class="cl">|   SYST: Version: BSD-43
</span></span><span class="line"><span class="cl">|   STAT:
</span></span><span class="line"><span class="cl">|  nextcube FTP server status:
</span></span><span class="line"><span class="cl">|      Version 5.1 (NeXT 1.0) Tue Jan 26, 1999
</span></span><span class="line"><span class="cl">|      Connected to 192.168.1.51 (192.168.1.51)
</span></span><span class="line"><span class="cl">|      Waiting for user name
</span></span><span class="line"><span class="cl">|      TYPE: ASCII, FORM: Nonprint; STRUcture: File; transfer MODE: Stream
</span></span><span class="line"><span class="cl">|      No data connection
</span></span><span class="line"><span class="cl">|_End of status
</span></span><span class="line"><span class="cl">23/tcp    open     telnet     IRIX telnetd 6.X
</span></span><span class="line"><span class="cl">25/tcp    open     smtp       Sendmail NX5.67g/NX3.0S
</span></span><span class="line"><span class="cl">|_smtp-commands: SMTP: EHLO 500 Command unrecognized\x0D
</span></span><span class="line"><span class="cl">37/tcp    open     time       (32 bits)
</span></span><span class="line"><span class="cl">|_rfc868-time: 2025-11-03T17:28:07
</span></span><span class="line"><span class="cl">79/tcp    open     finger     SGI IRIX or NeXTSTEP fingerd
</span></span><span class="line"><span class="cl">| finger: Login       Name              TTY Idle    When            Office\x0D
</span></span><span class="line"><span class="cl">|_me       My Account            co   1d Mon 00:48 \x0D
</span></span><span class="line"><span class="cl">111/tcp   open     rpcbind    2 (RPC #100000)
</span></span><span class="line"><span class="cl">178/tcp   open     nextstep?
</span></span><span class="line"><span class="cl">512/tcp   open     exec?
</span></span><span class="line"><span class="cl">| fingerprint-strings:
</span></span><span class="line"><span class="cl">|   Kerberos, SMBProgNeg, afp, oracle-tns:
</span></span><span class="line"><span class="cl">|_    Login incorrect.
</span></span><span class="line"><span class="cl">513/tcp   open     login      OpenBSD or Solaris rlogind
</span></span><span class="line"><span class="cl">514/tcp   open     tcpwrapped
</span></span><span class="line"><span class="cl">515/tcp   open     printer    lpd (path: /usr/lib/lpd; error: ....: Malformed from address)
</span></span><span class="line"><span class="cl">706/tcp   open     rpcbind
</span></span><span class="line"><span class="cl">709/tcp   open     rpcbind
</span></span><span class="line"><span class="cl">2453/tcp  open     madge-ltd?
</span></span><span class="line"><span class="cl">2968/tcp  filtered enpp
</span></span><span class="line"><span class="cl">8649/tcp  filtered unknown
</span></span><span class="line"><span class="cl">10553/tcp filtered unknown
</span></span><span class="line"><span class="cl">12263/tcp filtered unknown
</span></span><span class="line"><span class="cl">12651/tcp filtered unknown
</span></span><span class="line"><span class="cl">13523/tcp filtered unknown
</span></span><span class="line"><span class="cl">27087/tcp filtered unknown
</span></span><span class="line"><span class="cl">27860/tcp filtered unknown
</span></span><span class="line"><span class="cl">30923/tcp filtered unknown
</span></span><span class="line"><span class="cl">32979/tcp filtered unknown
</span></span><span class="line"><span class="cl">34521/tcp filtered unknown
</span></span><span class="line"><span class="cl">35920/tcp filtered unknown
</span></span><span class="line"><span class="cl">46067/tcp filtered unknown
</span></span><span class="line"><span class="cl">46934/tcp filtered unknown
</span></span><span class="line"><span class="cl">47204/tcp filtered unknown
</span></span><span class="line"><span class="cl">52006/tcp filtered unknown
</span></span><span class="line"><span class="cl">56233/tcp filtered unknown
</span></span><span class="line"><span class="cl">62708/tcp filtered unknown
</span></span><span class="line"><span class="cl">64492/tcp filtered unknown
</span></span><span class="line"><span class="cl">64498/tcp filtered unknown
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">2 services unrecognized despite returning data. If you know the service/version, please submit the following fingerprints at https://nmap.org/cgi-bin/submit.cgi?new-service :
</span></span><span class="line"><span class="cl">==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
</span></span><span class="line"><span class="cl">SF-Port21-TCP:V=7.80%I=7%D=11/3%Time=690939E9%P=x86_64-pc-linux-gnu%r(NULL
</span></span><span class="line"><span class="cl">SF:,4A,&#34;220\x20nextcube\x20FTP\x20server\x20\(Version\x205\.1\x20\(NeXT\x2
</span></span><span class="line"><span class="cl">SF:01\.0\)\x20Tue\x20Jan\x2026,\x201999\)\x20ready\.\r\n&#34;)%r(GenericLines,
</span></span><span class="line"><span class="cl">SF:8C,&#34;220\x20nextcube\x20FTP\x20server\x20\(Version\x205\.1\x20\(NeXT\x20
</span></span><span class="line"><span class="cl">SF:1\.0\)\x20Tue\x20Jan\x2026,\x201999\)\x20ready\.\r\n500\x20&#39;&#39;:\x20comma
</span></span><span class="line"><span class="cl">SF:nd\x20not\x20understood\.\r\n500\x20&#39;&#39;:\x20command\x20not\x20understood
</span></span><span class="line"><span class="cl">SF:\.\r\n&#34;)%r(SSLSessionReq,6D,&#34;220\x20nextcube\x20FTP\x20server\x20\(Vers
</span></span><span class="line"><span class="cl">SF:ion\x205\.1\x20\(NeXT\x201\.0\)\x20Tue\x20Jan\x2026,\x201999\)\x20ready
</span></span><span class="line"><span class="cl">SF:\.\r\n500\x20&#39;\x16\x03&#39;:\x20command\x20not\x20understood\.\r\n&#34;)%r(SMBP
</span></span><span class="line"><span class="cl">SF:rogNeg,4A,&#34;220\x20nextcube\x20FTP\x20server\x20\(Version\x205\.1\x20\(N
</span></span><span class="line"><span class="cl">SF:eXT\x201\.0\)\x20Tue\x20Jan\x2026,\x201999\)\x20ready\.\r\n&#34;);
</span></span><span class="line"><span class="cl">==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
</span></span><span class="line"><span class="cl">SF-Port512-TCP:V=7.80%I=7%D=11/3%Time=69093A18%P=x86_64-pc-linux-gnu%r(Ker
</span></span><span class="line"><span class="cl">SF:beros,12,&#34;\x01Login\x20incorrect\.\n&#34;)%r(SMBProgNeg,12,&#34;\x01Login\x20in
</span></span><span class="line"><span class="cl">SF:correct\.\n&#34;)%r(oracle-tns,12,&#34;\x01Login\x20incorrect\.\n&#34;)%r(afp,12,&#34;\
</span></span><span class="line"><span class="cl">SF:x01Login\x20incorrect\.\n&#34;);
</span></span><span class="line"><span class="cl">Service Info: OSs: Solaris, IRIX, Unix; CPE: cpe:/o:sun:sunos, cpe:/o:sgi:irix
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Host script results:
</span></span><span class="line"><span class="cl">|_clock-skew: -6h00m02s
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">NSE: Script Post-scanning.
</span></span><span class="line"><span class="cl">Read data files from: /usr/bin/../share/nmap
</span></span><span class="line"><span class="cl">Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
</span></span><span class="line"><span class="cl">Nmap done: 1 IP address (1 host up) scanned in 876.65 seconds</span></span></code></pre></div>

  </div>
</details>

<blockquote>
<p><span class="tag red">WARNING</span><br>
Keep nextcube off the public internet. It’s historically authentic and hilariously insecure by design.</p></blockquote>
<br>
<h2 id="links-and-stuff">Links and Stuff</h2>
<p>These install correctly without modification:</p>
<ul>
<li><a href="https://ftp.gnu.org/old-gnu/fileutils/">GNU Fileutils (ftp.gnu.org)</a></li>
<li><a href="https://ftp.gnu.org/old-gnu/textutils/">GNU Textutils (ftp.gnu.org)</a></li>
</ul>
<p>Prepared packages and binaries with required modifications:</p>
<ul>
<li><a href="https://archive.org/details/grep-2.2-openstep-intel">grep-2.2-openstep-intel (Archive.org)</a></li>
<li><a href="https://archive.org/details/termcap131_openstep_bundle">termcap131_openstep_bundle (Archive.org)</a></li>
<li><a href="https://archive.org/details/vim-5.3-openstep-intel">vim-5.3-openstep-intel (Archive.org)</a></li>
<li><a href="https://archive.org/details/fortune-mod-9708.OPENSTEP4.2.i486">fortune-mod-9708.OPENSTEP4.2.i486 (Archive.org)</a></li>
<li><a href="https://archive.org/details/w3c-httpd-3.0A.next.i486">w3c-httpd-3.0A.next.i486 (Archive.org)</a></li>
<li><a href="https://archive.org/details/dos2unix-5.2-openstep.tar">dos2unix-5.2-openstep</a></li>
</ul>
<p>Software highlights in the collection:</p>
<ul>
<li><a href="https://fsck.technology/software/NeXT/OpenStep%20Applications/CUBX%20Windows%205.0%20for%20OpenStep/">CUBX Windows 5.0 for OpenStep (fsck.technology)</a></li>
</ul>
<br>
<h2 id="outro">Conclusion</h2>
<p>At this point, you have a pretty complete OpenStep 4.2 workstation — developer tools, X11, shells, GNU utilities, and the full retro Unix experience.</p>
<p>In the next installment, we’ll publish a short <strong>User Guide</strong> for the Appliance:<br>
filesystem map, compile examples, file sharing tips, and snapshot management.</p>
<p>It&rsquo;s been nice having the nextcube VM now that I&rsquo;ve gotten it to this point. I&rsquo;d love to hear any feedback if this guide helped you or if I missed something <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>nextcube, Part 1 - an OpenStep 4.2 VM in 2025</title>
      <link>https://adminjitsu.com/posts/virtual-nextcube/</link>
      <pubDate>Sun, 02 Nov 2025 10:00:00 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/virtual-nextcube/</guid>
      <description>My reproducible recipe for OpenStep 4.2 Patch 4 running great on Ubuntu 22.04 with VirtualBox 7.0.16, including video, networking, developer tools, and first-boot fixes.</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<p>For a long time I’ve wanted to build a NeXTSTEP virtual machine—but it’s always been a hassle to get it working fully.<br>
Over the years I’ve tried just about everything: <strong>QEMU</strong>, <strong>86Box</strong>, <strong>VMware Workstation</strong>, <strong>Parallels</strong>, and multiple versions of <strong>VirtualBox</strong>. Every attempt ended the same way: partial success, bad drivers and no usable network stack.</p>
<figure style="float:left; margin:0 1rem 1rem 0; width:clamp(180px, 45%, 200px);">
  <img src="NeXT_logo.svg.png" 
       alt="" 
       style="display:block; width:100%; height:auto;">
  <figcaption style="font-size:85%; color:#666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
<p>Recently I stumbled across some solid public file collections and decided to give it another shot.<br>
This time, after the usual ritual of false starts and head scratching, I eventually managed to assemble a <strong>clean, stable VirtualBox 7 for Linux</strong> build of <strong>OpenStep 4.2 Patch 4</strong>.<br>
It runs beautifully on my <a href="/posts/vintage-macs/">MacBook Pro running Ubuntu</a>, and honestly—it’s a great little pet VM for any Mac admin or developer who wants to better understand the NeXT Step DNA and foundations present in the modern macOS world.</p>
<p><span class="tag green">Background</span> <a href="https://en.wikipedia.org/wiki/NeXT">https://en.wikipedia.org/wiki/NeXT</a></p>
<p>You see, OpenStep is the most recent version of the NeXT operating system. It was a shiny but niche workstation OS that Apple later used as the foundation for <strong>Mac OS X</strong>, and by extension <strong>macOS</strong> and <strong>iOS</strong>. It established a lot of what we still take for granted today: the <strong>CMU Mach kernel</strong>, the <strong>BSD subsystem</strong>, the <code>.app</code> bundle format, an <strong>object-oriented OS</strong>, <strong>Display PostScript</strong>, the <strong>Services</strong> menu, the <strong>Dock</strong>, and familiar utilities like <strong>Fonts</strong>, <strong>TextEdit</strong>, and <strong>Terminal</strong>.</p>
<p>If you know macOS—or the Darwin command-line environment—you’ll feel strangely at home here.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="First_Web_Server.jpg"
       alt="The first web server running on a NeXT Cube"
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size:85%; font-weight:normal; color:#666; line-height:1.4; margin-top:0.4em;">
    The first web server running on a NeXT Cube.<br>
    Coolcaesar, CC BY-SA 3.0 (<a href="http://creativecommons.org/licenses/by-sa/3.0/">link</a>), via Wikimedia Commons.
  </figcaption>
</figure>
<p>So yes — it’s a time capsule, but also a surprisingly fun little <strong>Unix obstacle course</strong>. You’ll end up debugging, porting, and re-learning core Unix skills — just without the modern safety nets. Working inside NeXT feels like stepping back into the late-90s: <strong>SunOS</strong>, <strong>Solaris</strong>, <strong>HP-UX</strong> — but cleaner, tighter, and more consistent. And its development environment was nothing short of revolutionary. With <strong>Objective-C</strong> and <strong>Interface Builder</strong>, developers could craft powerful, polished apps in record time — long before “rapid application development” became a buzzword.</p>
<br>
<h2 id="getting-started">Getting Started</h2>
<p>If you want to skip the setup and get straight to exploring, I’ve published a clean, patched, and fully functional <strong>OpenStep 4.2 VirtualBox OVA</strong> on <strong>Archive.org</strong>.</p>
<p>Most images floating around the web have missing patches, broken networking, or minimal configurations.<br>
This one includes everything I wished those did — a stable, period-accurate system ready to explore, customize, and build on.</p>
<hr>
<p>📦 <strong>Download:</strong> <a href="https://archive.org/details/openstep_ova">OpenStep 4.2 VirtualBox OVA (Archive.org)</a></p>
<p>You can login as either user (<code>me</code> or <code>root</code>) with the password, &lsquo;password&rsquo;. To change that, see <a href="#set-password">Set password</a> later in the Post-Install steps.</p>
<hr>
<h3 id="readme">README</h3>
<span class="hl yellow">I don’t recommend running this on VirtualBox for Windows.</span>
<br>
<blockquote>
<p><em>I tested extensively on Windows 10 and 11 with VirtualBox 6.1.32 (the last version with the old NDIS bridge code) and 7.2.4 (the current release as of writing). I even went as far as dedicating a physical Ethernet adapter to VirtualBox, stripped it of every binding except IPv4, and disabled all offload features—no joy. Performance was sluggish, and networking would drop or stall without reason.</em></p></blockquote>
<p>On <strong>Linux</strong>, though, it’s a dream. The VM is fast, stable, and the bridged adapter seems to work well.<br>
I run mine on <strong>Ubuntu 22.04 LTS</strong> with <strong>VirtualBox 7.0.16</strong>, and it feels nearly native.</p>
<p>Unfortunately, <strong>VirtualBox on Apple Silicon (M1/M2/M3)</strong> cannot virtualize <strong>x86 or x86-64</strong> guest operating systems — it only supports <strong>ARM-based</strong> VMs. So while OpenStep runs beautifully on Intel-based Macs with older versions of VirtualBox, it isn’t currently possible to run it on Apple Silicon hardware. Yes that sucks.</p>
<p><span class="tag blue">NOTE</span> My deprecated Intel Macs are all running Linux. You can check out my article
<a href="/posts/vintage-macs/">Breathing New Life Into Vintage Macs</a> for the details.</p>
<br>
<h3 id="whats-in-the-ova-appliance">What&rsquo;s in the OVA appliance</h3>
<p>On first boot, you&rsquo;ll find the following features. The only steps you should have to do is to change the password and review the network settings (<code>/etc/hostconfig</code> and <code>/etc/resolv.conf</code>) for your environment. I built around bridge mode so NAT and Host-Only will be a little different. See <a href="#post-install-steps">Post-Install Steps</a> for more info.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="omniweb-google.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Web browsing in extreme danger mode! Check out <a href="https://theoldnet.com" target="_blank" rel="noopener">theoldnet.com</a>
  </figcaption>
</figure>
<figure style="text-align:center; margin: 1em auto;">
  <img src="next-vim.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    A particularly nice quality-of-life port is vim
  </figcaption>
</figure>
<p>The OVA appliance contains the following goodies:</p>
<ul>
<li>Developer Tools</li>
<li>OS42MachUserPatch4</li>
<li>Doom, OmniWeb, WordPerfect, Concurrence, Diagram, ParaSheet, WetPaint, numerous small games and apps.</li>
<li>CUBX Windows 5.0 for OpenStep (X11R6 with Xterm and all the usual x11 apps)</li>
<li>fortune mod 9708 with all available fortune dat files and a few custom ones of my own (grumble, deep-thoughts, rules, oblique, rickmorty)</li>
<li>replacement for broken native bsd grep</li>
<li>fileutils and textutils installed to /usr/local/bin</li>
<li>w3c-httpd. latest version of the original web server ready to serve ~/public_html</li>
<li>replacement termcap and sane /etc/termcap to fix display issues</li>
<li>vim 5.3 installed with nice .vimrc that fixes issues and provides a very capable editor</li>
<li>various fixes and polish and about 700mb of free disk space to experiment with</li>
<li>tcsh and bash shells with bash set as shell for <code>root</code> and <code>me</code> users. decent .bashrc and .bash_profile to make it nice to use</li>
</ul>
<p>All in all I have found it to be a fun, old school Unix sandbox and pet VM.</p>
<br>
<h2 id="building-your-own">Building Your Own</h2>
<p>If you’d rather build from scratch, I found this to be a clear tutorial that walks through the basic process:
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/XAF0xdIiI20?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

here is the <a href="https://www.youtube.com/watch?v=XAF0xdIiI20&amp;t=680s">Youtube link</a></p>
<p>The two big rules are:</p>
<ol>
<li><strong>Use EIDE/ATAPI for your disks</strong> (don’t use SATA or SCSI).</li>
<li><strong>Install Patch 4</strong> immediately after first boot—it adds the modern drivers and fixes (Y2K) that make the system stable.</li>
</ol>
<p>Choosing the <strong>VESA VBE</strong> display driver during setup gives you a working color desktop up to <strong>1600 × 1200</strong> and smooth window redraws.</p>
<p><em>(next section will cover the detailed VirtualBox configuration and installation walkthrough, with screenshots of each key step)</em></p>
<br>
<h3 id="virtualbox-settings">VirtualBox settings</h3>
<p>The following settings are proven to work although 16mb for video is unused. I believe the VESA VBE modes max out at 8mb but the VM doesn&rsquo;t complain if you give it a little extra.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="virtualbox-overview.png" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    The overview of settings for the nextcube VM
  </figcaption>
</figure>
<figure style="text-align:center; margin: 1em auto;">
  <img src="virtualbox-1.png" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    128-256mb RAM, 1 cpu only, PIIX3 chipset, Ps/2 Mouse and I/O APIC. <br>
    You should remove the floppy and network from boot order after install to speed up boot.
  </figcaption>
</figure>
<figure style="text-align:center; margin: 1em auto;">
  <img src="virtualbox-2.png" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Make sure to use VBoxVGA and no 3d acceleration. Scaling helps on high res host screens
  </figcaption>
</figure>
<figure style="text-align:center; margin: 1em auto;">
  <img src="virtualbox-3.png" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    PIIX3 and Use Host I/O Cache for the hard drive. Keep vdi hdd files to 2gb or less for compatibility. <br> 
    Do not use ssd
  </figcaption>
</figure>
<figure style="text-align:center; margin: 1em auto;">
  <img src="virtualbox-4.png" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Critical. For Bridged Adapter, make sure to choose PCnet-PCII (Am79C970A) and Promiscuous mode
  </figcaption>
</figure>
<figure style="text-align:center; margin: 1em auto;">
  <img src="virtualbox-5.png" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Finally make sure to disable USB or you will get Virtual Box errors about the mouse on boot. 
  </figcaption>
</figure>
<hr>
<span class="tag orange">On Linux</span>
<p>VirtualBox and the Linux KVM modules (<code>kvm</code> and <code>kvm_intel</code>) both require exclusive access to hardware virtualization features (VT-x/AMD-V). When KVM is loaded—often automatically on boot—it takes control of these extensions, which prevents VirtualBox from launching VMs and results in errors like <code>VERR_VMX_IN_VMX_ROOT_MODE</code>. This is especially problematic with older OSes like OpenStep 4.2, which require hardware virtualization in VirtualBox due to the removal of legacy software emulation in recent versions.</p>
<p>To resolve this, unloading KVM modules (<code>modprobe -r kvm_intel kvm</code>) before launching VirtualBox frees up VT-x so the VM can start. You can make this change persistent by blacklisting the modules (via <code>/etc/modprobe.d/blacklist-kvm.conf</code>) or adding <code>kvm.enable_virt_at_load=0</code> to your kernel command line. The trade-off is that you lose access to KVM-based virtualization (e.g., for QEMU or Android emulators) while VirtualBox has control.</p>
<br>
<h3 id="-install-process">🧩 Install Process</h3>
<p>Once your VM is configured, attach the <strong>Install floppy</strong> and the <strong>User CD</strong>, then boot the system; follow the on-screen prompts until you’re asked for the <strong>driver floppy</strong> — insert it when prompted.</p>
<p>When choosing devices for the <strong>hard disk</strong> and <strong>CD-ROM</strong>, press <code>7</code> twice to reveal additional options.<br>
Select <strong>Option 5: EIDE/ATAPI</strong>, which is the correct choice for modern emulated hardware.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="eide-atapi.png" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    You want to choose option 5  <b>Primary/Secondary(Dual) EIDE and ATAPI Device Controllers (v4.01)</b> <br>
    for both the hard drive and cdrom. It will prompt you twice to do this. 
  </figcaption>
</figure>
<p>Continue through the installer — there are few non-default choices, and everything is shown clearly in the tutorial video linked above.</p>
<p>After the base install completes and the system reboots, you’ll be prompted once more to insert the <strong>driver disk</strong> before the GUI portion of setup continues.</p>
<p>From there, NeXTSTEP will copy files to the hard drive — this step takes a while, so be patient.<br>
When it finishes, you’ll be asked to select your <strong>keyboard layout</strong> and <strong>language</strong> to complete setup.</p>
<hr>
<p>I’ve gone through this process many times while preparing this guide — it’s straightforward once you know the key steps.<br>
The video walkthrough covers everything visually, but if you prefer a written version with screenshots, check out the excellent 🔗 <a href="https://cdn-learn.adafruit.com/downloads/pdf/build-your-own-next-with-a-virtual-machine.pdf"><strong>Build Your Own NeXT with a Virtual Machine (PDF)</strong></a>.</p>
<br>
<figure style="text-align:center; margin: 1em auto;">
  <img src="next-doom.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Don't worry, you'll be playing Doom in no time! Still a blast. 
  </figcaption>
</figure>
<br>
<h2 id="post-install-steps">Post-Install steps</h2>
<p>At this point, you should have a basic install of OpenStep on your virtual hard drive. To finish what I would consider to be required steps for usability, you&rsquo;ll want to do the following. I would suggest making liberal use of snapshots in VirtualBox as you progress through this process. It&rsquo;s easy to mess up if you aren&rsquo;t paying attention.</p>
<h3 id="install-openstep-mach-patch-4">Install OpenStep Mach Patch 4</h3>
<p>The first thing you should do after installation and initial setup is to install <strong>OS42MachUserPatch4.pkg</strong>.</p>
<p>🔗 DOWNLOAD <a href="https://fsck.technology/software/NeXT/next.68k.org%20Archive/otto/html/openstep.se/resources/files/patches/openstep/OS42MachUserPatch4.tar">fsck.technology</a><br>
📝 RELEASE NOTES <a href="https://fsck.technology/software/NeXT/next.68k.org%20Archive/otto/html/openstep.se/resources/files/patches/openstep/OS42Patch4ReleaseNotes.pdf">OS42Patch4ReleaseNotes.pdf</a>.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="mach-patch.png" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Already installed this one. Good! 
  </figcaption>
</figure>
<p>This patch includes:</p>
<ul>
<li>Updated kernel and device drivers</li>
<li>Support for VESA VBE video (which allows color and higher resolutions on modern emulators and hardware)</li>
<li>Enhanced EIDE/SATA disk support</li>
<li>Fixes for network and TCP/IP stack issues</li>
<li>Updated system libraries and runtime components</li>
<li>Y2K handling fixes</li>
</ul>
<hr>
<h3 id="set-display-driver-vesa-vbe">Set Display Driver (VESA VBE)</h3>
<p>After installing OS42MachUserPatch4, you can launch /NextAdmin/Configure.app, navigate to Display and choose the VESA VBE driver. This will allow you to set a variety of higher resolutions up to 1600x1200/32 in color! Definitely do this</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="vesa-vbe.png" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Configure.app -> Display -> Vesa VBE driver configured.
  </figcaption>
</figure>
<hr>
<h3 id="set-password">Set password for <code>me</code> and <code>root</code> to enable LoginWindow</h3>
<p>OpenStep uses <strong>NetInfo</strong> to manage user accounts, not the traditional <code>/etc/passwd</code> files found on most Unix systems.<br>
To set passwords safely and ensure LoginWindow works correctly:</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="change-password.png" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Make sure to change password here and not with passwd
  </figcaption>
</figure>
<ol>
<li>
<p><strong>Log in as <code>root</code>.</strong><br>
(If the system logs in automatically as <code>me</code>, open a terminal and run <code>su -</code> to switch to root.)</p>
</li>
<li>
<p><strong>Launch User Manager:</strong><br>
Open <code>/NextAdmin/UserManager.app</code>.</p>
</li>
<li>
<p><strong>Open the user record:</strong></p>
<ul>
<li>From the <strong>User</strong> menu, choose <strong>Open</strong>.</li>
<li>In the dialog, navigate to <code>/users/me</code>.</li>
<li>To set the root password, open <code>/users/root</code> instead.</li>
</ul>
</li>
<li>
<p><strong>Set the password:</strong></p>
<ul>
<li>Click in the <strong>Password</strong> field and enter your new password.</li>
<li>Choose <strong>User → Save</strong> from the menu.</li>
<li>You’ll be prompted to re-enter it for confirmation.</li>
</ul>
</li>
<li>
<p><strong>Repeat</strong> for both <code>me</code> and <code>root</code> users.</p>
</li>
</ol>
<p>After saving both accounts, reboot or log out — the <strong>LoginWindow</strong> should now appear at startup.</p>
<blockquote>
<p>💡 <strong>Tip:</strong> Avoid using the <code>passwd</code> command.<br>
It updates <code>/etc/passwd</code> but doesn’t synchronize with <strong>NetInfo</strong>, which can break authentication.<br>
Always use <strong>UserManager.app</strong> or the following command instead:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">su
</span></span><span class="line"><span class="cl">niutil -createprop . /users/username passwd <span class="s2">&#34;newpassword&#34;</span>
</span></span></code></pre></div></blockquote>
<figure style="text-align:center; margin: 1em auto;">
  <img src="openstep-login.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    You should now see a login window instead of being logged in automatically as `me`
  </figcaption>
</figure>
<hr>
<h3 id="configure-networking">Configure Networking</h3>
<p>You&rsquo;ll need to edit <code>/etc/hostconfig</code>. Here is the one I use in nextcube, the VirtualBox OVA Appliance.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="o">[</span> me@nextcube <span class="o">]</span>:~ $  cat /etc/hostconfig
</span></span><span class="line"><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="cl"><span class="c1"># /etc/hostconfig</span>
</span></span><span class="line"><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="cl"><span class="c1"># This file sets up shell variables used by the various rc scripts to</span>
</span></span><span class="line"><span class="cl"><span class="c1"># configure the host.  Edit this file instead of rc.boot.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Warning:  This is sourced by /bin/sh.  Make sure there are no spaces</span>
</span></span><span class="line"><span class="cl"><span class="c1">#           on either side of the &#34;=&#34;.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="cl"><span class="c1"># There are some special keywords used by rc.boot and the programs it</span>
</span></span><span class="line"><span class="cl"><span class="c1"># calls:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="cl"><span class="c1">#       -AUTOMATIC-     Configure automatically</span>
</span></span><span class="line"><span class="cl"><span class="c1">#       -YES-           Turn a feature on</span>
</span></span><span class="line"><span class="cl"><span class="c1">#       -NO-            Leave a feature off or do not configure</span>
</span></span><span class="line"><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">HOSTNAME</span><span class="o">=</span>nextcube
</span></span><span class="line"><span class="cl"><span class="nv">INETADDR</span><span class="o">=</span>192.168.1.50
</span></span><span class="line"><span class="cl"><span class="nv">ROUTER</span><span class="o">=</span>192.168.1.1
</span></span><span class="line"><span class="cl"><span class="nv">IPNETMASK</span><span class="o">=</span>255.255.255.0
</span></span><span class="line"><span class="cl"><span class="nv">IPBROADCAST</span><span class="o">=</span>-AUTOMATIC-
</span></span><span class="line"><span class="cl"><span class="nv">NETMASTER</span><span class="o">=</span>-YES-
</span></span><span class="line"><span class="cl"><span class="nv">YPDOMAIN</span><span class="o">=</span>-NO-
</span></span><span class="line"><span class="cl"><span class="nv">TIME</span><span class="o">=</span>-AUTOMATIC-
</span></span></code></pre></div><p>You will then need to edit <code>/etc/resolv.conf</code>. Mine looks like this:</p>
<p>OpenStep needs <code>/etc/resolv.conf</code> to resolve domain names to IP addresses. This file tells the system which DNS servers to query and what domain to use for hostname lookups.</p>
<p>Create or edit <code>/etc/resolv.conf</code> as root:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">domain darkstar.home
</span></span><span class="line"><span class="cl">nameserver 192.168.1.1
</span></span></code></pre></div><ul>
<li><strong><code>domain</code></strong>: Your local network domain (often your home network name or LAN domain)</li>
<li><strong><code>nameserver</code></strong>: The IP address of your DNS server (typically your router/gateway)</li>
</ul>
<p>You can add multiple <code>nameserver</code> lines if you have backup DNS servers. The system will try them in order if the first one doesn&rsquo;t respond.</p>
<p>Without this file configured, you won&rsquo;t be able to browse websites by name in OmniWeb or use any network tools that require DNS—everything would need to be accessed by IP address directly.</p>
<hr>
<h3 id="annoying-floppy-issue">Annoying Floppy Issue</h3>
<p>Out of the box, due to a quirk of virtualization, you will see a popup every boot like this</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="next-floppy-issue.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Rogue floppy bug
  </figcaption>
</figure>
<p>Once I had finished installing floppies I removed the floppy controller from VirtualBox which effectively suppresses the message. It is extremely easy to add it back if you need a floppy by shutting down the VM, adding a Floppy controller and booting again. I recommend this step for quality-of-life.</p>
<p>To remove, go to the VM Settings and under storage highlight the floppy controller and click the little remove icon. Adding it back is easy as well just do it when the VM is powered down</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="remove-floppy.png" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Easy peasy
  </figcaption>
</figure>
<hr>
<h3 id="netinfo-parent-issue">NetInfo Parent issue</h3>
<p>One annoying configuration issue I encountered without clear documentation was with netinfod searching for a parent on boot. As configured during install, netinfod will broadcast for a NetInfo parent regardless of whether NETMASTER=-YES- is set in <code>/etc/hostconfig</code> or not.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="netinfo-parent.jpg" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    This causes long timeouts and requires user intervention to press c to continue booting
  </figcaption>
</figure>
<p>The fix is:</p>
<ul>
<li>Make sure that <code>root</code> (and<code>me</code>) have a password set</li>
<li>Log in as root</li>
<li>Open /NextAdmin/NetinfoManager.app</li>
<li>Navigate to /machines/broadcasthost and double-click on broadcasthost to edit</li>
<li>Highlight the serves property then from the menu select Edit and Delete</li>
<li>Save by selecting Directory from the menu and Save</li>
<li>Reboot and the message should no longer appear.</li>
</ul>
<br>
<h2 id="upcoming-articles-to-finish-building-the-ova-appliance">Upcoming articles to finish building the OVA appliance.</h2>
<p>We still need to install the developer tools, get a better shell, get a better editor and replace the broken awk with a gnu version, install fileutils and textutils, vim, X11R6, and build some sensible dotfiles. Stay tuned! Coming shortly.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="next-x11r6.png" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    The appliance includes CUBX Windows, an X11R6 client system with xterm and familiar x11 apps
  </figcaption>
</figure>
<h2 id="links-and-stuff">Links and Stuff</h2>
<h3 id="-video--download">🎥 Video &amp; Download</h3>
<p>This was the tutorial I followed that worked the best. Make sure to grab the zip file. Otherwise you can find the install images and patches from the links in the next section.</p>
<ul>
<li><a href="https://www.youtube.com/watch?v=XAF0xdIiI20&amp;t=680s">OpenStep 4.2 VirtualBox Tutorial (YouTube)</a></li>
<li><a href="https://mega.nz/file/xNVjADgR#417wddOATnAOf7ATvGfJU5qm7-0mU3aQ6lZKXzlDrVw">Mega.nz download (zip file from the tutorial)</a></li>
<li><a href="http://www.shawcomputing.net/resources/next/software/install/ns_install.html">NEXTSTEP &amp; OPENSTEP Install Guide – ShawComputing</a> This is a really clear walkthrough if you&rsquo;re looking for more.</li>
</ul>
<h3 id="-archives--file-resources">💾 Archives &amp; File Resources</h3>
<ul>
<li>
<p><a href="https://fsck.technology/software/NeXT/">fsck.technology – NeXT / OpenSTEP Archive</a></p>
</li>
<li>
<p><a href="https://bitsavers.org/pdf/next/">Bitsavers – NeXT Documentation PDFs</a></p>
</li>
<li>
<p><a href="https://www.nextcomputers.org/NeXTfiles/">NextComputers.org – NeXTfiles collection</a></p>
</li>
<li>
<p><a href="https://archive.org/search?query=NeXT+">Archive.org search – NeXT</a></p>
</li>
<li>
<p><a href="https://archive.org/search?query=nextworld">Archive.org search – Nextworld Magazine</a></p>
</li>
</ul>
<h3 id="-additional-resources">🔍 Additional Resources</h3>
<ul>
<li><a href="https://theoldnet.com/">The Old Net</a></li>
<li><a href="https://www.nextcomputers.org/forums/">NextComputers.org Forums</a></li>
<li><a href="https://en.wikipedia.org/wiki/OpenStep">Wikipedia - OpenStep</a></li>
<li><a href="https://en.wikipedia.org/wiki/NeXT">Wikipedia – NeXT</a></li>
<li><a href="https://en.wikipedia.org/wiki/Serial_Experiments_Lain">Wikipedia - Serial Experiments Lain connection</a> lots of great references</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>If you’ve followed along, you now have a complete OpenStep 4.2 environment running on modern hardware—networking, color video, and development tools included. It’s a fully functional UNIX system from another era, and yet, it still integrates cleanly into a modern lab network.</p>
<p>Make sure to check out <a href="/posts/next-config/">Part 2</a> where we&rsquo;ll complete the build by adding developer tools, a modern shell environment, improved editors, GNU utilities, and X11 - transforming this into a genuinely usable Unix workstation.</p>
<p>I&rsquo;d love to hear feedback. Email me at <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="kill-icon.png" 
       alt="" 
       style="display:block; margin:0 auto; width:min(100%, 100px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
]]></content:encoded>
    </item>
    <item>
      <title>SchemaSpy with LocalDB (SQL Server)</title>
      <link>https://adminjitsu.com/posts/schemaspy-mssql/</link>
      <pubDate>Thu, 02 Oct 2025 00:00:00 +0000</pubDate>
      <guid>https://adminjitsu.com/posts/schemaspy-mssql/</guid>
      <description>Unlike MySQL or PostgreSQL, SQL Server LocalDB only speaks named pipes. Here’s the exact stack and command that makes SchemaSpy 6.1.0 work reliably with LocalDB on Windows.</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<p>SchemaSpy is my preferred tool for generating <strong>browsable HTML docs and ER diagrams</strong> from SQL databases. I’ve already shown how to configure it with SQLite, but SQL Server’s <strong>LocalDB</strong> edition introduces a completely different set of quirks.</p>
<p>LocalDB is a lightweight SQL Server runtime used by tools like Lansweeper and Visual Studio. Unlike full SQL Server or Express, which run as services and listen on TCP, <strong>LocalDB doesn’t accept TCP connections at all</strong>. It only exposes a <strong>named pipe endpoint</strong>, and it only works with <strong>Windows integrated authentication (NTLM/SSPI)</strong>.</p>
<p>That’s why most SchemaSpy guides skip LocalDB entirely — Microsoft’s official JDBC driver expects TCP, and Java has no built-in NTLM support. The workaround is to use the older <strong>jTDS driver</strong>, which still supports named pipes, plus its companion <code>ntlmauth.dll</code> for NTLM authentication.</p>
<p>After a lot of trial and error, I found a reliable combination: <strong>SchemaSpy 6.1.0 + jTDS 1.3.3 + ntlmauth.dll + Graphviz 2.38</strong>. It’s fiddly, but once you have the pieces in place, you can generate a complete HTML schema doc site for any LocalDB-backed application.</p>
<br>
<h2 id="the-pieces-you-need">The Pieces You Need</h2>
<table>
  <thead>
      <tr>
          <th>Component</th>
          <th>Why It Matters</th>
          <th>Version / Link</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>SchemaSpy (fat JAR)</strong></td>
          <td>The doc/diagram generator</td>
          <td><strong>6.1.0</strong> (newer versions break jTDS)</td>
      </tr>
      <tr>
          <td><strong>jTDS JDBC Driver</strong></td>
          <td>Supports named pipes + NTLM/SSO</td>
          <td><strong>1.3.3</strong></td>
      </tr>
      <tr>
          <td><strong>Java Runtime</strong></td>
          <td>Runs SchemaSpy</td>
          <td>Java 8+ (64-bit recommended)</td>
      </tr>
      <tr>
          <td><strong>Graphviz</strong></td>
          <td>Renders static diagrams</td>
          <td><strong>2.38</strong> (stable with SchemaSpy 6.x)</td>
      </tr>
      <tr>
          <td><strong><code>ntlmauth.dll</code></strong></td>
          <td>Bridges NTLM/SSPI into Java for SSO</td>
          <td>Comes with jTDS zip (x86/x64/ia64)</td>
      </tr>
  </tbody>
</table>
<blockquote>
<p><span class="tag orange">Heads-up</span> Microsoft’s official JDBC driver <strong>cannot connect to LocalDB</strong> — it only works over TCP. That’s why jTDS is required.</p></blockquote>
<br>
<h2 id="suggested-setup-windows">Suggested Setup (Windows)</h2>
<p>Keep everything SchemaSpy needs in one folder so paths are consistent:</p>
<pre tabindex="0"><code>C:\bin\SchemaSpy\
  schemaspy-6.1.0.jar     # SchemaSpy itself
  jtds-1.3.3.jar          # JDBC driver
  ntlmauth.dll            # From jTDS zip, for Windows SSO
</code></pre><p>Install Graphviz to its default location:</p>
<pre tabindex="0"><code>C:\Program Files (x86)\Graphviz2.38\
</code></pre><hr>
<p><span class="tag red">Important</span> <strong>About <code>ntlmauth.dll</code></strong><br>
The jTDS 1.3.3 zip includes three versions:</p>
<ul>
<li><code>x86\ntlmauth.dll</code> → use if you run 32-bit Java</li>
<li><code>x64\ntlmauth.dll</code> → use if you run 64-bit Java (most common today)</li>
<li><code>ia64\ntlmauth.dll</code> → legacy Itanium, almost never needed</li>
</ul>
<p>Copy the correct DLL into your <code>C:\bin\SchemaSpy\</code> folder. When running SchemaSpy, add:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">-Djava</span><span class="p">.</span><span class="py">library</span><span class="p">.</span><span class="n">path</span><span class="p">=</span><span class="s2">&#34;C:\bin\SchemaSpy&#34;</span>
</span></span></code></pre></div><p>If the bitness doesn’t match, you’ll see <code>UnsatisfiedLinkError</code> or “Login failed” errors.</p>
<br>
<h2 id="step-1-find-the-localdb-instance-pipe">Step 1: Find the LocalDB Instance Pipe</h2>
<p>LocalDB doesn’t have a fixed hostname like <code>localhost,1433</code>. Instead, every time it starts, Windows assigns it a <strong>unique named pipe</strong>. That pipe is what jTDS connects to.</p>
<p>To check the pipe, open <strong>PowerShell or Command Prompt on Windows</strong> (on the same machine that’s running LocalDB) and run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">SqlLocalDB</span> <span class="nb">start </span><span class="n">LSInstance</span>
</span></span><span class="line"><span class="cl"><span class="n">SqlLocalDB</span> <span class="n">info</span>  <span class="n">LSInstance</span>
</span></span></code></pre></div><p>This will print details about the LocalDB instance, including the <strong>Instance pipe name</strong>. For example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">np:\\.\pipe\LOCALDB#SH964109\tsql\query
</span></span></code></pre></div><p>The important part is the <strong>instance token</strong> (<code>LOCALDB#SH964109</code>). You’ll pass that value into SchemaSpy with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">-instance &#34;LOCALDB#SH964109&#34;
</span></span></code></pre></div><p><span class="tag orange">Heads-up</span>: The token changes <strong>every time LocalDB restarts</strong>. If SchemaSpy suddenly stops connecting, rerun <code>SqlLocalDB info</code> to grab the updated string.</p>
<br>
<h2 id="step-2-authentication-options">Step 2: Authentication Options</h2>
<p>SQL Server supports two authentication modes, but only one works with LocalDB:</p>
<ul>
<li>
<p><strong>Windows Integrated (SSO)</strong><br>
LocalDB uses Windows credentials instead of SQL usernames. The account that runs SchemaSpy must be a Windows user who has been granted access to the database.</p>
<p>By default, this is the same account that installed or created the LocalDB instance (for example, the Windows user who installed Lansweeper). If you run SchemaSpy as a different user — even another local admin — you may get <code>Login failed for user</code> until that account is explicitly added as a login in SQL Server Management Studio.</p>
<p>To make NTLM authentication work in Java, you also need <code>ntlmauth.dll</code> from the jTDS package. Without it, SchemaSpy will fail to authenticate even if the Windows account has access.</p>
</li>
<li>
<p><strong>SQL Login (username/password)</strong><br>
This works on full SQL Server or Express, where TCP/IP is enabled and you can create dedicated logins. LocalDB does <strong>not</strong> allow this mode out of the box, so you can’t rely on <code>-u/-p</code> against a stock LocalDB instance.</p>
</li>
</ul>
<br>
<h2 id="step-3-the-working-command">Step 3: The Working Command</h2>
<p>With SchemaSpy, jTDS, and Graphviz in place, you’re ready to run the actual command. This is where everything comes together.</p>
<blockquote>
<p><span class="tag red">Important</span> SchemaSpy appends <code>\bin\dot</code> to whatever you provide via <code>-gv</code>. Always pass the <strong>Graphviz install root folder</strong>, not the path to <code>dot.exe</code>.</p></blockquote>
<br>
<h3 id="option-1-windows-integrated-authentication-sso">Option 1: Windows Integrated Authentication (SSO)</h3>
<p>This is the <strong>normal way</strong> to connect to LocalDB. SchemaSpy will log in as the Windows account you’re running under (make sure that account has database access). You also need <code>ntlmauth.dll</code> and <code>-Djava.library.path</code>.</p>
<p><strong>If you’re running from <code>cmd.exe</code>:</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cmd" data-lang="cmd"><span class="line"><span class="cl">java -Djava.library.path=<span class="s2">&#34;C:\bin\SchemaSpy&#34;</span> -jar <span class="s2">&#34;C:\bin\SchemaSpy\schemaspy-6.1.0.jar&#34;</span> <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -t mssql-jtds-instance <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -dp <span class="s2">&#34;C:\bin\SchemaSpy\jtds-1.3.3.jar&#34;</span> <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -host . <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -instance <span class="s2">&#34;LOCALDB#SH964109&#34;</span> <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -db lansweeperdb <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -s dbo <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -sso <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -connprops <span class="s2">&#34;namedPipe\=true&#34;</span> <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -o <span class="s2">&#34;C:\bin\SchemaSpy\output&#34;</span> <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -gv <span class="s2">&#34;C:\Program Files (x86)\Graphviz2.38&#34;</span>
</span></span></code></pre></div><p><strong>If you’re running from PowerShell:</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">java</span> <span class="n">-Djava</span><span class="p">.</span><span class="py">library</span><span class="p">.</span><span class="n">path</span><span class="p">=</span><span class="s2">&#34;C:\bin\SchemaSpy&#34;</span> <span class="n">-jar</span> <span class="s2">&#34;C:\bin\SchemaSpy\schemaspy-6.1.0.jar&#34;</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">  <span class="n">-t</span> <span class="nb">mssql-jtds</span><span class="n">-instance</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">  <span class="n">-dp</span> <span class="s2">&#34;C:\bin\SchemaSpy\jtds-1.3.3.jar&#34;</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">  <span class="n">-host</span> <span class="p">.</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">  <span class="n">-instance</span> <span class="s2">&#34;LOCALDB#SH964109&#34;</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">  <span class="n">-db</span> <span class="n">lansweeperdb</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">  <span class="n">-s</span> <span class="n">dbo</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">  <span class="n">-sso</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">  <span class="n">-connprops</span> <span class="s2">&#34;namedPipe\=true&#34;</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">  <span class="n">-o</span> <span class="s2">&#34;C:\bin\SchemaSpy\output&#34;</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">  <span class="n">-gv</span> <span class="s2">&#34;C:\Program Files (x86)\Graphviz2.38&#34;</span>
</span></span></code></pre></div><p><span class="tag orange">Heads-up</span> Use <code>^</code> in Command Prompt and <code>`</code> (backtick) in PowerShell. Mixing them up will cause errors like <code>'-t' is not recognized as an internal or external command</code>.</p>
<br>
<h3 id="option-2-sql-login-if-one-exists-in-localdb">Option 2: SQL Login (if one exists in LocalDB)</h3>
<p>Some applications (like Lansweeper) create a SQL login inside LocalDB (<code>lansweeperuser</code>). If such a login exists, you can use it with <code>-u</code>/<code>-p</code> instead of SSO. The continuation rules are the same as above.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cmd" data-lang="cmd"><span class="line"><span class="cl">java -jar <span class="s2">&#34;C:\bin\SchemaSpy\schemaspy-6.1.0.jar&#34;</span> <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -t mssql-jtds-instance <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -dp <span class="s2">&#34;C:\bin\SchemaSpy\jtds-1.3.3.jar&#34;</span> <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -host . <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -instance <span class="s2">&#34;LOCALDB#SH964109&#34;</span> <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -db lansweeperdb <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -s dbo <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -u lansweeperuser -p <span class="s2">&#34;SuperSecret&#34;</span> <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -connprops <span class="s2">&#34;namedPipe\=true&#34;</span> <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -o <span class="s2">&#34;C:\bin\SchemaSpy\output&#34;</span> <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -gv <span class="s2">&#34;C:\Program Files (x86)\Graphviz2.38&#34;</span>
</span></span></code></pre></div><p><span class="tag blue">Note</span> If no SQL login exists in your LocalDB instance, this will fail with <em>Login failed for user</em>. In that case, you must use <strong>SSO</strong>.</p>
<br>
<h3 id="flag-breakdown">Flag Breakdown</h3>
<ul>
<li><code>-t mssql-jtds-instance</code> → use the jTDS SQL Server driver profile.</li>
<li><code>-dp</code> → path to the jTDS JAR file.</li>
<li><code>-host .</code> + <code>-instance &quot;LOCALDB#...&quot;</code> → connect via the LocalDB named pipe.</li>
<li><code>-db lansweeperdb</code> → database name to document.</li>
<li><code>-s dbo</code> → schema to include (typically <code>dbo</code>).</li>
<li><code>-sso</code> → use Windows integrated authentication.</li>
<li><code>-u/-p</code> → alternative, if a SQL login exists.</li>
<li><code>-connprops &quot;namedPipe=true&quot;</code> → <strong>critical</strong>: forces jTDS to use named pipes instead of TCP.</li>
<li><code>-gv &quot;&lt;GraphvizRoot&gt;&quot;</code> → Graphviz install root (SchemaSpy appends <code>\bin\dot</code>).</li>
<li><code>-o</code> → output directory for the generated site.</li>
</ul>
<br>
<h3 id="what-normal-output-looks-like">What Normal Output Looks Like</h3>
<p>SchemaSpy is <em>chatty</em>. A successful run doesn’t just say “Connected” — you’ll see:</p>
<ul>
<li>ASCII-art SchemaSpy banner and license text.</li>
<li>Warnings about restricted methods (expected with jTDS + modern Java).</li>
<li>A line like:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">INFO  - Connected to Microsoft SQL Server - 16.00.xxxx
</span></span></code></pre></div></li>
<li>Long “Gathering schema details…” progress lines with dots.</li>
<li>Occasional warnings about <code>sysproperties</code> (safe to ignore — SQL Server 2016+ removed it).</li>
<li>Graphviz messages about “graph is too large for cairo-renderer” with scaling (also safe).</li>
<li>A final summary like:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Wrote relationship details of 1274 tables/views to directory &#39;C:\bin\SchemaSpy\output&#39;
</span></span><span class="line"><span class="cl">View the results by opening C:\bin\SchemaSpy\output\index.html
</span></span></code></pre></div></li>
</ul>
<p>The key thing to look for is that <strong>output files get written</strong> and you see a final “View the results…” line. Everything else (warnings, scaling errors, missing comments) is normal noise.</p>
<p>The output directory will contain a full HTML site with:</p>
<ul>
<li><code>index.html</code> → overview page</li>
<li><code>tables/</code> → per-table pages</li>
<li><code>columns/</code> → per-column detail pages</li>
<li><code>relationships/</code> → ER diagrams (static images + clickable maps)</li>
</ul>
<p>Open <code>index.html</code> in your browser to start exploring.</p>
<br>
<h2 id="why-this-works">Why This Works</h2>
<ul>
<li><strong>SchemaSpy 6.1.0</strong> preserves working integration with <code>mssql-jtds-instance</code>.</li>
<li><strong>jTDS 1.3.3</strong> supports <strong>named pipes</strong> and <strong>Windows SSO</strong> (critical for LocalDB).</li>
<li><code>-connprops &quot;namedPipe=true&quot;</code> routes through the LocalDB pipe.</li>
<li><strong>Graphviz 2.38</strong> is the last hassle-free version for SchemaSpy 6.x.</li>
<li><strong><code>ntlmauth.dll</code></strong> provides NTLM/SSPI so SSO works from Java.</li>
</ul>
<br>
<h2 id="troubleshooting-matrix">Troubleshooting Matrix</h2>
<p>Even with the right jars and flags, LocalDB is fiddly. Here’s a quick reference for the most common errors and how to resolve them:</p>
<table>
  <thead>
      <tr>
          <th>Symptom / Error</th>
          <th>Likely Cause</th>
          <th>Fix</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>Login failed for user</code> (SSO)</td>
          <td><code>ntlmauth.dll</code> missing or wrong bitness</td>
          <td>Put <code>ntlmauth.dll</code> alongside the JARs; match your Java runtime (x64 vs x86); include <code>-Djava.library.path=&quot;C:\bin\SchemaSpy&quot;</code></td>
      </tr>
      <tr>
          <td><code>The TCP/IP connection to the host…</code></td>
          <td>Using Microsoft’s JDBC driver, or forgot <code>namedPipe=true</code></td>
          <td>Use <strong>jTDS 1.3.3</strong>; add <code>-connprops &quot;namedPipe\=true&quot;</code></td>
      </tr>
      <tr>
          <td><code>Cannot open database &quot;&lt;dbname&gt;&quot; requested by the login</code></td>
          <td>Wrong DB name or missing permissions</td>
          <td>Verify the database name (e.g. <code>lansweeperdb</code> for Lansweeper, or your own app’s DB); check that your Windows user or SQL login has access</td>
      </tr>
      <tr>
          <td><code>No suitable driver</code></td>
          <td>Driver not found</td>
          <td>Ensure <code>-dp &quot;C:\bin\SchemaSpy\jtds-1.3.3.jar&quot;</code> is correct</td>
      </tr>
      <tr>
          <td>Diagrams not generated</td>
          <td>Wrong <code>-gv</code> path or Graphviz missing</td>
          <td>Point <code>-gv</code> to the <strong>Graphviz root folder</strong> (e.g., <code>...\Graphviz2.38</code>), not <code>dot.exe</code>; reinstall 2.38 if necessary</td>
      </tr>
      <tr>
          <td>Works once, fails after reboot</td>
          <td>LocalDB pipe name changed</td>
          <td>Re-run <code>SqlLocalDB info &lt;InstanceName&gt;</code> to get the new pipe, update <code>-instance &quot;LOCALDB#...&quot;</code></td>
      </tr>
      <tr>
          <td><code>The network path was not found</code></td>
          <td>Instance not started or pipe unavailable</td>
          <td>Run <code>SqlLocalDB start &lt;InstanceName&gt;</code>; confirm the pipe string; if using SSO, run SchemaSpy as the same Windows user that owns the DB</td>
      </tr>
      <tr>
          <td>Non-ASCII path weirdness</td>
          <td>Path quoting/escaping on Windows</td>
          <td>Wrap all paths in quotes; avoid special characters in folder names if possible</td>
      </tr>
  </tbody>
</table>
<br>
<h2 id="links--stuff">Links &amp; Stuff</h2>
<ul>
<li>
<p><strong>SchemaSpy</strong><br>
📥 <a href="https://github.com/schemaspy/schemaspy/releases/download/v6.1.0/schemaspy-6.1.0.jar">SchemaSpy v6.1.0 JAR (GitHub)</a><br>
<a href="https://schemaspy.readthedocs.io/">SchemaSpy Documentation</a></p>
</li>
<li>
<p><strong>jTDS JDBC Driver (1.3.3)</strong><br>
📥 <a href="https://sourceforge.net/projects/jtds/files/jtds/1.3.3/">Download jTDS 1.3.3 (SourceForge)</a><br>
<a href="https://web.archive.org/web/20160331133731/http://jtds.sourceforge.net/">Archived jTDS project site</a></p>
</li>
<li>
<p><strong>Graphviz (2.38 for Windows)</strong><br>
📥 <a href="https://graphviz.gitlab.io/_pages/Download/windows/graphviz-2.38.msi">Graphviz 2.38 Windows installer (archive)</a><br>
<a href="https://graphviz.org/">Graphviz site &amp; docs</a></p>
</li>
<li>
<p><strong>SQL Server LocalDB</strong><br>
<a href="https://learn.microsoft.com/en-us/sql/database-engine/configure-windows/sql-server-express-localdb">Microsoft Docs – SQL Server Express LocalDB</a></p>
</li>
<li>
<p><strong>SQL Server Management Studio (SSMS)</strong><br>
📥 <a href="https://aka.ms/ssmsfullsetup">Download SSMS (Microsoft)</a><br>
Handy for browsing your LocalDB, verifying database names (e.g. <code>lansweeperdb</code>), and checking which logins exist.</p>
</li>
</ul>
<p><em>(For long-term use, consider mirroring the required JARs and DLLs internally so your team isn’t chasing archive sites years later.)</em></p>
<br>
<h2 id="conclusion">Conclusion</h2>
<p>LocalDB’s <strong>named-pipe-only</strong> quirk trips up the usual JDBC approach. The dependable combo is:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">SchemaSpy 6.1.0  +  jTDS 1.3.3  +  -connprops &#34;namedPipe=true&#34;  +  Graphviz 2.38
</span></span></code></pre></div><p>With that in place — and the LocalDB <strong>instance pipe</strong> refreshed as needed — you’ll get clean, searchable HTML docs and ERDs for any LocalDB-backed app (e.g. Lansweeper, Visual Studio projects).</p>
<p>If you run into an edge case I didn’t cover, send it my way <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a> and I’ll expand the troubleshooting matrix.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Tcpdump Survival Guide</title>
      <link>https://adminjitsu.com/posts/tcpdump-survival-guide/</link>
      <pubDate>Wed, 24 Sep 2025 05:50:39 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/tcpdump-survival-guide/</guid>
      <description>Tcpdump isn’t flashy, but it’s indispensable. This guide covers the essentials of packet capture: where to listen, how to plan, saving full packets to PCAP, filtering for relevance, and even capturing iOS and Android traffic. Perfect for troubleshooting, automation, and handing off to Wireshark for analysis.</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<p>Tcpdump is the <strong>Swiss Army knife of packet capture</strong>. It’s lean, scriptable, and ships with almost every Unix-like system. That makes it ideal for automation, troubleshooting, or handing to a customer when you need a reproducible capture without walking them through a GUI.</p>
<p>Most people analyze captures in <strong>Wireshark</strong>, the premiere open-source packet analyzer. But tcpdump is often the better tool for collecting the data in the first place: it can be scripted, shared as a one-liner, and run with minimal instructions. That workflow — capture with tcpdump, analyze with Wireshark — is a staple in real-world troubleshooting.</p>
<p>Packet captures are rarely trivial: they’re big, messy, and not something you want to redo needlessly. The goal is to get it right <strong>the first time</strong> — a clean, useful trace containing the data you need that you can hand off to Wireshark or another analyst. This survival guide shows the commands you need to do just that.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="saknife.jpg" 
       alt="Swiss Army Knife Climber model" 
       style="display:block; margin:0 auto; width:min(100%, 500px); height:auto;">
<figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
  Tcpdump is the Swiss Army knife of packet capture.<br>
  Photo: Ave Maria / Jonas, via <a href="https://commons.wikimedia.org/wiki/File:Victorinox_Swiss_Army_Knife_-_Climber_(15554551505).jpg" target="_blank">Wikimedia Commons</a>, 
  <a href="https://creativecommons.org/licenses/by/2.0/" target="_blank">CC BY 2.0</a>
</figcaption>
</figure>
<br>
<h2 id="where-to-capture">Where to Capture?</h2>
<p>Before worrying about command flags, it helps to think about <strong>point of view</strong>. Packet captures are always relative — what you see depends on where you’re listening from.</p>
<p>If you’re debugging a client/server exchange, you could capture on the client to confirm whether requests are leaving, or on the server to see if they arrive. Sometimes you need both perspectives: one to prove the client sent the packet, the other to prove the server received (or didn’t).</p>
<p>The network itself matters too. On a flat LAN or broadcast domain, a capture may show you traffic between many hosts. But on a switched or routed network, you’ll only see packets destined to or from the machine you’re capturing on. That’s why capturing on the endpoint itself is often the simplest, most reliable approach.</p>
<p>For complex problems, it’s common to gather multiple captures — one at the client, one at the server, maybe another at a key router or firewall. When you line them up by timestamp, you can follow the packet’s journey across the path and pinpoint where things break down.</p>
<br>
<h2 id="capturing-with-purpose">Capturing With Purpose</h2>
<p>Before you even start tcpdump, think about the flow: <strong>set up the capture, reproduce the issue, then stop and analyze</strong>. The most useful captures are the ones where you already know what you were testing and when.</p>
<p>When possible, note the time that certain events happen (“clicked Connect at 14:03:12”, “login failed around 14:05”). Even better, jot down exactly what you did while the capture was running. Those notes become anchors when you later scroll through packets in Wireshark.</p>
<p>It’s tempting to just run tcpdump for hours and fish around afterwards, but that usually leaves you with a mountain of noise. A focused capture is faster to analyze and less likely to miss the moment you care about. Sometimes you know the exact traffic you want (say, HTTPS to a specific host). Other times you don’t — and that’s fine. Even then, try to reproduce the problem cleanly and capture just long enough to get the exchange you’re after.</p>
<p>The key idea: <strong>minimum noise, maximum relevance</strong>. The cleaner your capture, the easier your analysis.</p>
<br>
<h2 id="gathering-a-solid-capture">Gathering a Solid Capture</h2>
<p>Now that you’ve thought about where to capture and how to focus your test, the next step is setting up tcpdump to record everything you need without losing detail. The essentials boil down to three things: <strong>interface, scope, and output</strong>. We’ll get deeper into interface choices in the next section — for now, let’s focus on how to record a solid trace.</p>
<h3 id="save-to-a-file">Save to a File</h3>
<p>For later analysis in Wireshark or another tool, always write to a <code>.pcap</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo tcpdump -i eth0 -s <span class="m">0</span> -w capture.pcap
</span></span></code></pre></div><ul>
<li><code>-s 0</code> → capture the <strong>entire packet</strong>, not just the default snap length.</li>
<li><code>-w</code>   → write raw packets to a file (not human-readable).</li>
<li>Use <code>Ctrl+C</code> to stop when you’ve got enough.</li>
</ul>
<h3 id="quick-checks">Quick Checks</h3>
<p>Sometimes you only need a small slice:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo tcpdump -i eth0 -s <span class="m">0</span> -c <span class="m">500</span> -w sample.pcap
</span></span></code></pre></div><ul>
<li><code>-c</code> stops after N packets.</li>
<li>Great for reproducible test runs.</li>
</ul>
<h3 id="reading-it-back">Reading It Back</h3>
<p>To verify or skim without Wireshark:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">tcpdump -r capture.pcap
</span></span></code></pre></div><h3 id="pro-tips">Pro Tips</h3>
<ul>
<li>Always run with <code>sudo</code> (or root).</li>
<li>Watch disk space — captures grow quickly.</li>
<li>Sync system clocks if possible (NTP helps line up multi-host captures).</li>
<li>Time matters: make sure your system clock is accurate for useful logs.</li>
</ul>
<p>Now that you know the essential flags for capturing, you need to point tcpdump at the right place to listen. Modern systems have multiple network interfaces, and choosing the wrong one means capturing nothing useful.</p>
<br>
<h2 id="choosing-the-right-interface">Choosing the Right Interface</h2>
<p>Once you know where you want to capture, the next step is picking the right network interface. Modern systems almost always have more than one: a wired port, one or more wireless adapters, VPN tunnels, virtual interfaces created by containers, or diagnostic interfaces used for special cases like mobile devices. Tcpdump needs to know exactly which one to listen on.</p>
<p>List available interfaces with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">tcpdump -D
</span></span></code></pre></div><p>You’ll see numbered entries such as:</p>
<ul>
<li><strong>Linux:</strong> <code>eth0</code>, <code>ens33</code>, <code>wlan0</code>, <code>lo</code> (loopback), <code>docker0</code>, <code>tun0</code> (VPN), <code>vethXYZ</code> (containers)</li>
<li><strong>macOS:</strong> <code>en0</code> (Ethernet or Wi-Fi), <code>en1</code>, <code>lo0</code> (loopback), <code>utunX</code> (VPN), <code>bridge0</code>, <code>rvi0</code> (iOS Remote Virtual Interface)</li>
<li><strong>Windows (via WSL or WinDump):</strong> <code>Ethernet</code>, <code>Wi-Fi</code>, <code>Local Area Connection</code>, plus GUID-style names for virtual adapters</li>
</ul>
<h3 id="which-ones-matter">Which Ones Matter?</h3>
<ul>
<li><strong>Ethernet / Wi-Fi (<code>eth0</code>, <code>ens33</code>, <code>en0</code>, <code>wlan0</code>)</strong> → Most common capture points for client or server traffic.</li>
<li><strong>Loopback (<code>lo</code>, <code>lo0</code>)</strong> → Useful if you’re testing services that talk to themselves on <code>localhost</code>.</li>
<li><strong>VPN (<code>tun0</code>, <code>utunX</code>)</strong> → Captures encrypted tunnel traffic; sometimes useful for debugging, but you won’t see the inner payloads unless you capture on the endpoint before encryption.</li>
<li><strong>Docker / Container Bridges (<code>docker0</code>, <code>vethXYZ</code>, <code>cniXYZ</code>)</strong> → Capture container-to-container traffic. Handy in Kubernetes/Docker debugging.</li>
<li><strong>Mobile Debugging (<code>rvi0</code> for iOS, <code>any</code> for Android)</strong> → Special cases we’ll cover in detail below.</li>
<li><strong>Others (bridges, virtual adapters, hypervisor links)</strong> → Usually less relevant unless you’re debugging virtualization or complex network topologies.</li>
</ul>
<p>The key point: capture on the interface where the traffic of interest actually flows. Choose wrong, and you’ll end up with an empty file or irrelevant chatter.</p>
<br>
<h2 id="capturing-mobile-traffic">Capturing Mobile Traffic</h2>
<p>One of tcpdump’s neat tricks is that you can capture traffic from mobile devices without exotic hardware taps. Both iOS and Android expose interfaces that tcpdump can hook into from your computer. In both cases you’ll need the device <strong>plugged in with a USB cable</strong>.</p>
<hr>
<h3 id="ios-macos-host-required">iOS (macOS host required)</h3>
<p>On macOS, Apple provides a <strong>Remote Virtual Interface (RVI)</strong> that exposes iPhone or iPad network traffic over USB. The device must be <strong>unlocked</strong> and you’ll need to tap <em>Trust this computer</em> the first time you connect. Once enabled, the phone appears as a virtual interface (<code>rvi0</code>) that you can capture from with tcpdump.</p>
<p>The only catch: you need the device’s <strong>UDID</strong> (Unique Device Identifier). You can get it a few ways:</p>
<ul>
<li>
<p><strong>System Profiler (built-in):</strong><br>
Plug in your device, then run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">system_profiler SPUSBDataType <span class="p">|</span> grep -w <span class="s2">&#34;Serial Number&#34;</span>
</span></span></code></pre></div><p>This prints the serial numbers of connected USB devices. For iPhones and iPads, that string is the UDID.</p>
</li>
<li>
<p><strong>libimobiledevice (third-party tools):</strong><br>
If you have <a href="https://libimobiledevice.org/">libimobiledevice</a> installed, you can run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">idevice_id -l
</span></span></code></pre></div><p>which lists all connected iOS devices by UDID.</p>
</li>
</ul>
<p><span class="tag green">Pro-Tip</span> If you’re handing instructions to someone else, you can make it a one-liner:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">rvictl -s <span class="k">$(</span>system_profiler SPUSBDataType <span class="p">|</span> awk <span class="s1">&#39;/Serial Number/{print $3; exit}&#39;</span><span class="k">)</span>
</span></span></code></pre></div><p>That command automatically grabs the first iOS device’s UDID and passes it to <code>rvictl</code>.</p>
<p>Once the RVI is active, check with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">rvictl -l
</span></span></code></pre></div><p>When finished, tear it down with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">rvictl -x &lt;device-udid&gt;
</span></span></code></pre></div><p>At that point, <code>rvi0</code> behaves like any other interface. You can capture traffic from it just like you would on Ethernet or Wi-Fi:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo tcpdump -i rvi0 -s <span class="m">0</span> -w iphone.pcap
</span></span></code></pre></div><p>This gives you a complete packet trace of the phone’s traffic — Wi-Fi, cellular, or both — without needing a special router or access point. It’s one of the most useful tricks for debugging mobile apps in the field.</p>
<hr>
<h3 id="android-usb-required">Android (USB required)</h3>
<p>Android doesn’t expose an RVI, but you can capture traffic via the <strong>Android Debug Bridge (adb)</strong>. Your phone must be <strong>plugged in over USB with USB debugging enabled</strong>. On many devices this works out of the box, but some builds require a tcpdump binary installed on the phone and <strong>root privileges</strong> to run it.</p>
<ul>
<li><strong>macOS:</strong>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">brew install android-platform-tools
</span></span></code></pre></div></li>
<li><strong>Debian/Ubuntu:</strong>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt install android-tools-adb
</span></span></code></pre></div></li>
<li><strong>Fedora:</strong>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo dnf install android-tools
</span></span></code></pre></div></li>
<li><strong>Windows:</strong><br>
Download <a href="https://developer.android.com/tools/releases/platform-tools">Android Platform Tools</a> from Google.</li>
</ul>
<p>With adb installed and your phone connected by USB (with USB debugging enabled), verify the device is detected:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">adb devices
</span></span></code></pre></div><p>You may need to accept a prompt on the phone.</p>
<p>From there, you have two approaches:</p>
<ul>
<li>
<p><strong>Capture directly on the device:</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">adb shell tcpdump -i any -s <span class="m">0</span> -w /sdcard/capture.pcap
</span></span><span class="line"><span class="cl">adb pull /sdcard/capture.pcap
</span></span></code></pre></div><p>This writes a <code>.pcap</code> file to the device and then pulls it back to your computer.</p>
</li>
<li>
<p><strong>Stream packets live to your desktop:</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">adb exec-out tcpdump -i any -s <span class="m">0</span> -w - &gt; android.pcap
</span></span></code></pre></div><p>This writes the capture directly to your computer, which is often simpler for real-time analysis in Wireshark.</p>
</li>
</ul>
<p>Whichever method you use, the result is the same: a <code>.pcap</code> file containing app traffic over Wi-Fi and cellular, ready to open in Wireshark.</p>
<br>
<h2 id="filtering-for-relevance">Filtering for Relevance</h2>
<p>Captures get big quickly. A focused filter saves disk space, cuts noise, and makes analysis much easier. Tcpdump uses the <strong>Berkeley Packet Filter (BPF)</strong> syntax, which is simple once you know a few basics.</p>
<p>In practice, you’ll usually want to capture <strong>full packets</strong> and write them to a file for later analysis in Wireshark. For example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo tcpdump -i eth0 -s <span class="m">0</span> -w capture.pcap <span class="s1">&#39;tcp port 443 and host example.com&#39;</span>
</span></span></code></pre></div><ul>
<li><code>-s 0</code> → capture the <strong>entire packet</strong></li>
<li><code>-w capture.pcap</code> → write to a file instead of printing to the screen</li>
<li>Filter expression in quotes → what traffic to capture</li>
</ul>
<p>For the examples below, we’ll omit the <code>-s 0 -w ...</code> parts to keep things short, but remember to include them when you’re running a real capture.</p>
<h3 id="common-filters">Common Filters</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Host-based</span>
</span></span><span class="line"><span class="cl">tcpdump host 192.168.1.50
</span></span><span class="line"><span class="cl">tcpdump src host 10.0.0.5
</span></span><span class="line"><span class="cl">tcpdump dst host example.com
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Port-based</span>
</span></span><span class="line"><span class="cl">tcpdump port <span class="m">80</span>
</span></span><span class="line"><span class="cl">tcpdump tcp port <span class="m">443</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Protocol</span>
</span></span><span class="line"><span class="cl">tcpdump icmp
</span></span><span class="line"><span class="cl">tcpdump arp
</span></span><span class="line"><span class="cl">tcpdump udp
</span></span></code></pre></div><h3 id="practical-filtering-recipes">Practical Filtering Recipes</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># DNS traffic only</span>
</span></span><span class="line"><span class="cl">tcpdump -i eth0 port <span class="m">53</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># HTTP traffic to a specific host</span>
</span></span><span class="line"><span class="cl">tcpdump -i eth0 tcp port <span class="m">80</span> and host example.com
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># HTTPS traffic to a specific host</span>
</span></span><span class="line"><span class="cl">tcpdump -i eth0 tcp port <span class="m">443</span> and host 203.0.113.42
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># All web traffic (HTTP + HTTPS) from one client</span>
</span></span><span class="line"><span class="cl">tcpdump -i eth0 host 192.168.1.25 and <span class="se">\(</span> port <span class="m">80</span> or port <span class="m">443</span> <span class="se">\)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Capture traffic in a subnet</span>
</span></span><span class="line"><span class="cl">tcpdump net 192.168.1.0/24
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Capture all but one noisy host</span>
</span></span><span class="line"><span class="cl">tcpdump not host 192.168.1.10
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># SSH traffic to or from a specific server</span>
</span></span><span class="line"><span class="cl">tcpdump tcp port <span class="m">22</span> and host bastion.example.com
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ICMP (pings) except from a certain host</span>
</span></span><span class="line"><span class="cl">tcpdump icmp and not src host 10.0.0.5
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Multiple conditions: HTTPS to a subnet, but ignore one host</span>
</span></span><span class="line"><span class="cl">tcpdump tcp port <span class="m">443</span> and net 10.0.0.0/24 and not host 10.0.0.50
</span></span></code></pre></div><h3 id="tips">Tips</h3>
<ul>
<li>Use parentheses <code>()</code> when combining multiple expressions (escape them in shells with <code>\(</code> <code>\)</code>).</li>
<li>Add <code>-n</code> to prevent tcpdump from resolving hostnames (faster, less noise).</li>
<li>Increase detail with <code>-v</code>, <code>-vv</code>, or <code>-vvv</code> when reviewing packets live.</li>
<li>Filters in tcpdump (BPF) are not the same as <strong>Wireshark display filters</strong> — capture filters decide what packets are saved, while display filters decide what you see later.</li>
</ul>
<br>
<h2 id="links--stuff">Links &amp; Stuff</h2>
<p><strong>Essential Documentation:</strong></p>
<ul>
<li><a href="https://www.tcpdump.org/manpages/tcpdump.1.html">Tcpdump man page</a></li>
<li><a href="https://www.tcpdump.org/">Tcpdump &amp; Libpcap homepage</a></li>
<li><a href="https://www.wireshark.org/docs/dfref/">Wireshark Display Filters</a></li>
</ul>
<p><strong>Further Reading:</strong></p>
<ul>
<li><a href="https://wizardzines.com/zines/tcpdump/">Tcpdump Zine by Julia Evans</a> — a fun, visual guide that complements this article.</li>
<li><a href="https://packetlife.net/library/cheat-sheets/">Packet Life Cheat Sheets</a> — quick reference sheets for tcpdump, Wireshark, and more.</li>
</ul>
<br>
<h2 id="conclusion">Conclusion</h2>
<p>Tcpdump isn’t flashy, but it’s indispensable. With just a few commands you can capture clean, useful packet traces that save hours of guesswork. Think about <em>where</em> to capture, plan your test, write to a file, and use filters to cut noise. Do it right once, and you won’t have to ask your customer to redo it.</p>
<p>This guide covered the essentials — enough to get you started, hand off captures, and debug issues with confidence. In a follow-up, we’ll dive into analysis and advanced tricks: <code>tshark</code>, Wireshark filtering, ring buffer captures, and boot-time monitoring.</p>
<p>As always, if you found this post useful or if you have questions, I’d love to hear from you: <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
<p>Happy packet hunting ✨</p>
]]></content:encoded>
    </item>
    <item>
      <title>Unix Users Handbook</title>
      <link>https://adminjitsu.com/posts/unix-users-handbook/</link>
      <pubDate>Sun, 21 Sep 2025 14:12:00 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/unix-users-handbook/</guid>
      <description>Unix has always been multiuser. This guide explains why, shows you how identities are stored and enforced, and gives you a cross-platform, task-based cheatsheet of every user-related command you need.</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<p>Unix isn’t a lonely single-player game.</p>
<p>Even when adventuring on your personal laptop, you’re <strong>never alone</strong>. A whole party of user accounts travels with you: <code>root</code> with absolute power, <code>daemon</code> running background jobs, <code>nobody</code> handling scraps, and dozens of service identities (<code>mysql</code>, <code>www-data</code>, <code>postfix</code>). They own files, run processes, and enforce boundaries between users.</p>
<p>That’s the point: Unix was built for multiuser life. In the 1970s, a single minicomputer would act as a hub for dozens of dumb terminals — green-screen VT100s, Teletypes, or glass TTYs — all wired in over RS-232 serial lines. Later, SLIP and PPP links carried those sessions across early networks. Everyone logged in concurrently, sharing the same CPU, disks, and memory. To survive in that environment, the OS enforced strict user separation: each login session mapped to a unique UID, file ownership was enforced at the kernel level, and processes were isolated by identity.</p>
<p>That DNA remains at the heart of Unix. Even today, the cast of characters — root, daemon, nobody, end-user accounts, and service identities — exists to enforce least privilege and prevent one misbehaving process from trampling another.</p>
<p>This guide explains:</p>
<ul>
<li><strong>History and concepts</strong> of Unix users</li>
<li><strong>Where identities live</strong> (<code>/etc/passwd</code>, <code>/etc/shadow</code>, <code>/etc/group</code>, <code>/etc/sudoers</code>)</li>
<li>A <strong>cross-platform, task-based cheatsheet</strong> covering everything from inspecting to creating, modifying, and deleting users, plus group management and sudo.</li>
<li>Historical tidbits, gotchas, and modern tricks</li>
<li>All the pertinent documentation and links</li>
</ul>
<p><span class="tag orange">NOTE</span>
By the end you’ll have your own <strong>player&rsquo;s screen</strong> — every user-related command condensed into one place and neatly organized by task.</p>
<br>
<h2 id="so-what-is-a-user">So, What is a User?</h2>
<p>In Unix, a “user” isn’t a human being — it’s an <strong>identity</strong> (a user principal) that the kernel uses to decide what’s allowed. Every process runs <em>as someone</em>, every file is <em>owned by someone</em>, and the system enforces permissions based on those identities.</p>
<p>At the machine level, a user boils down to three things:</p>
<ul>
<li><strong>UID (User ID):</strong> a unique integer (UID). This is what the kernel actually checks. UID <code>0</code> is hard-coded as <code>root</code>, the all-powerful superuser.</li>
<li><strong>GID (Group ID):</strong> groups are collections of users, identified by their own numeric IDs (GID). Group permissions are checked alongside individual user permissions.</li>
<li><strong>Username:</strong> a human-friendly alias for a UID. When you type <code>alice</code> at a login prompt, the system maps that to UID <code>1000</code> (or whatever number was assigned).</li>
</ul>
<p>The kernel never cares about names — only numbers. When you run <code>ls -l</code>, it looks up UIDs and GIDs in <code>/etc/passwd</code> and <code>/etc/group</code> just to display friendlier, human-readable names.</p>
<p><span class="tag orange">NOTE</span> If you delete a user but keep their files, ownership doesn’t vanish. The UID stays behind, and you’ll see files owned by <code>1001</code> or <code>2000</code>, etc. That’s the <strong>ghost of a user</strong>: the identity is gone, but the number persists in the filesystem.</p>
<p>This split between human-friendly names and machine-level numbers is deliberate. It means accounts can be automated, identities can be isolated, and multiuser systems don’t collapse into chaos.</p>
<br>
<h2 id="history--tidbits">History &amp; Tidbits</h2>
<p>Unix grew up in a world where multiuser wasn’t optional — it was the baseline.</p>
<ul>
<li><strong>1970s multiuser terminals:</strong> A single PDP-11 or VAX might have dozens of dumb terminals (Teletypes, VT100s, Wyse displays) connected over serial lines. Each terminal was just a keyboard and screen; all the real computing happened on the host. Later, SLIP and PPP carried terminal sessions over early TCP/IP links, making multiuser logins possible from remote sites.</li>
<li><strong>Strict separation of users:</strong> Because many people were logged in simultaneously, the kernel had to enforce sharp boundaries. Each session mapped to a unique UID; processes couldn’t touch each other’s files; and permissions were checked on every system call.</li>
<li><strong>UID 0:</strong> Always <code>root</code>. The kernel literally has “if (uid == 0)” checks hard-coded in privileged operations. This convention has survived intact for 50+ years.</li>
<li><strong>System accounts:</strong> Services needed their own identities to run safely. Instead of everything being <code>root</code>, daemons like <code>mail</code>, <code>www-data</code>, and <code>mysql</code> got UIDs of their own. That way, a web server compromise didn’t instantly mean total system takeover.</li>
<li><strong>Different OS ranges:</strong>
<ul>
<li><strong>Linux:</strong> human users typically start at UID <strong>1000</strong> today (but <strong>500</strong> on older RHEL/CentOS). Below that are system accounts.</li>
<li><strong>macOS:</strong> human users start at <strong>501</strong>; everything below is reserved. Apple prefixes many system accounts with underscores (<code>_spotlight</code>, <code>_windowserver</code>).</li>
<li><strong>BSD:</strong> similar to Linux, but ranges vary; users should start at UID <strong>1001</strong>; service accounts and reserved IDs are well documented in the BSD handbooks.</li>
</ul>
</li>
<li><strong>“Nobody” user:</strong> UID <code>65534</code> (or sometimes <code>-2</code>) is a special identity with the least privilege possible. It exists to run processes with <em>almost no rights at all</em>.</li>
</ul>
<p>The takeaway: Unix users are a product of real hardware and real constraints — not just an abstract security idea. The multiuser DNA of the 1970s still shapes how your laptop and servers work today.</p>
<br>
<h2 id="the-user-database">The User Database</h2>
<p>So where do these identities actually live? In classic Unix, they’re just flat text files:</p>
<ul>
<li><strong><code>/etc/passwd</code></strong> — the account roster. Username, UID, GID, home directory, shell, and a password placeholder. World-readable.</li>
<li><strong><code>/etc/shadow</code></strong> — password hashes and aging rules. Only root can read it. If this file leaks, the system is blown.</li>
<li><strong><code>/etc/group</code></strong> — group definitions and memberships. Controls shared access.</li>
<li><strong><code>/etc/sudoers</code></strong> — the privilege ledger. Defines who can become root (and how).</li>
</ul>
<figure style="float:left; margin:0 1rem 1rem 0; width:clamp(260px, 45%, 450px);">
  <img src="spellbook.jpg" 
       alt="a pixel art depiction of an open book with mystical symbols" 
       style="display:block; width:100%; height:auto;">
  <figcaption style="font-size:85%; color:#666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
<p>On modern enterprise systems, these files often act as a <strong>front-end to NSS</strong> (Name Service Switch), which may pull identities from LDAP, NIS, Kerberos, or Active Directory. That’s why tools like <code>getent</code> are preferred over <code>cat /etc/passwd</code> — they return the whole picture, not just the local slice.</p>
<p>Think of these files as the <strong>bones</strong> of identity. The commands you run — <code>passwd</code>, <code>useradd</code>, <code>dscl</code>, <code>visudo</code> — are the muscles that safely move those bones around.</p>
<p>And above it all sits the kernel, acting as the <strong>nervous system</strong>.<br>
It doesn’t care whether an identity came from a flat file or a directory<br>
service; all it sees are UIDs and GIDs. That abstraction is the secret<br>
that lets Unix scale from a single laptop to a campus full of machines<br>
while keeping the rules of identity consistent.</p>
<div style="clear:both"></div>
<br>
<h2 id="permissions--ownership-recap">Permissions &amp; Ownership Recap</h2>
<p>Users only matter because the kernel enforces <strong>who owns what</strong> and <strong>who can do what</strong>. That enforcement happens at two levels: <strong>files</strong> and <strong>processes</strong>.</p>
<h3 id="file-permissions">File Permissions</h3>
<p>Every file has two owners:</p>
<ul>
<li>a <strong>user owner</strong> (UID)</li>
<li>a <strong>group owner</strong> (GID)</li>
</ul>
<p>And three sets of permissions: <strong>user (u)</strong>, <strong>group (g)</strong>, and <strong>other (o)</strong>.</p>
<p>Example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">-rwxr-sr-- <span class="m">1</span> alice devs <span class="m">532</span> Sep <span class="m">21</span> 13:01 script.sh
</span></span></code></pre></div><p>Breakdown:</p>
<ul>
<li><strong><code>-</code></strong> → type of file. (<code>-</code> = regular file, <code>d</code> = directory, <code>l</code> = symlink, <code>c</code>/<code>b</code> = device, <code>s</code> = socket, <code>p</code> = named pipe)</li>
<li><strong><code>rwx</code></strong> → permissions for the <strong>user/owner</strong> (<code>alice</code>): read, write, execute.</li>
<li><strong><code>r-s</code></strong> → permissions for the <strong>group</strong> (<code>devs</code>): read + execute, plus <code>s</code> meaning <strong>setgid</strong> is set.</li>
<li><strong><code>r--</code></strong> → permissions for <strong>others</strong>: read only.</li>
<li><strong><code>1</code></strong> → hard link count. Directories show how many subdirs + self + parent.</li>
<li><strong><code>alice</code></strong> → the owner (mapped from UID).</li>
<li><strong><code>devs</code></strong> → the group (mapped from GID).</li>
<li><strong><code>532</code></strong> → file size in bytes.</li>
<li><strong><code>Sep 21 13:01</code></strong> → last modification time.</li>
<li><strong><code>script.sh</code></strong> → filename.</li>
</ul>
<p>Other permission bits:</p>
<ul>
<li><strong>setuid (<code>s</code> on user perms):</strong> program runs with file owner’s UID. Example: <code>/usr/bin/passwd</code> runs as root.</li>
<li><strong>setgid (<code>s</code> on group perms):</strong> files created in this directory inherit the group; executables run with group’s GID.</li>
<li><strong>sticky bit (<code>t</code> on others perms):</strong> on directories, only file owners can delete their files. <code>/tmp</code> uses this.</li>
</ul>
<p>💡 <em>Gotcha:</em> Permissions are checked <strong>on every system call</strong> (<code>open()</code>, <code>execve()</code>, etc). There’s no “once at login” caching — enforcement is continuous.</p>
<hr>
<h3 id="process-ownership">Process Ownership</h3>
<p>Every process runs <strong>as a user</strong> and carries both a <strong>real UID</strong> (who started it) and an <strong>effective UID</strong> (who it’s acting as).</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ps -u alice
</span></span><span class="line"><span class="cl">UID   PID  CMD
</span></span><span class="line"><span class="cl"><span class="m">1000</span>  <span class="m">2345</span> bash
</span></span><span class="line"><span class="cl"><span class="m">1000</span>  <span class="m">2371</span> vim
</span></span><span class="line"><span class="cl"><span class="m">0</span>     <span class="m">2402</span> sudo
</span></span><span class="line"><span class="cl"><span class="m">0</span>     <span class="m">2403</span> systemctl
</span></span></code></pre></div><p>Here:</p>
<ul>
<li>Alice owns her <code>bash</code> and <code>vim</code> processes.</li>
<li>When she runs <code>sudo systemctl</code>, the new process has UID 0 (root).</li>
</ul>
<p>Key fields:</p>
<ul>
<li><strong>Real UID/GID</strong> → the account that launched the process.</li>
<li><strong>Effective UID/GID</strong> → what the kernel uses for permission checks. (Setuid/setgid binaries modify this.)</li>
<li><strong>Saved UID</strong> → allows a process to drop and later regain privileges (common in daemons).</li>
</ul>
<p>💡 <em>Example:</em> Apache (<code>httpd</code>) starts as <code>root</code> to bind to port 80, then immediately drops privileges to <code>www-data</code>. If the web server is compromised, the attacker only gains <code>www-data</code> rights, not root.</p>
<hr>
<h3 id="why-it-matters">Why It Matters</h3>
<ul>
<li><strong>Every file belongs to someone.</strong> Delete the account, and the UID lingers.</li>
<li><strong>Every process runs as someone.</strong> If that process is compromised, its UID defines the blast radius.</li>
<li><strong>Least privilege works only if users and groups are defined properly.</strong> Services should almost never run as root.</li>
</ul>
<p>This is the backbone of Unix security. Once you grasp how file permissions and process ownership interact, the user management commands in the cheatsheet make sense: you’re really just moving numbers and labels around to control who owns what and who can act as whom.</p>
<br>
<figure style="text-align:center; margin: 1em auto;">
  <img src="dice.jpg" 
       alt="a pixel art image of a pair of 20-sided dice with a 20 and a 1 showing" 
       style="display:block; margin:0 auto; width:min(100%, 300px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Let's roll up some users!
  </figcaption>
</figure>
<h2 id="cheatsheet">Cheatsheet</h2>
<p>Task-based, cross-platform, and complete. Each section expands with commands from basic to advanced.</p>
<hr>
<h3 id="inspect-users--groups"><span style="color:#CC0000;">Inspect Users &amp; Groups</span></h3>
<p>Check who you are, who exists on the system, and what groups users belong to.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Current identity ===</span>
</span></span><span class="line"><span class="cl">whoami                    <span class="c1"># show effective username</span>
</span></span><span class="line"><span class="cl">id                        <span class="c1"># UID, GID, groups</span>
</span></span><span class="line"><span class="cl">id -u                     <span class="c1"># numeric UID only</span>
</span></span><span class="line"><span class="cl">id -g                     <span class="c1"># numeric GID only</span>
</span></span><span class="line"><span class="cl">id -nG                    <span class="c1"># group names only</span>
</span></span><span class="line"><span class="cl">id -G                     <span class="c1"># numeric GIDs only</span>
</span></span><span class="line"><span class="cl">groups                    <span class="c1"># list groups (Linux, BSD)</span>
</span></span><span class="line"><span class="cl">groups alice              <span class="c1"># groups for another user</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === System-wide queries ===</span>
</span></span><span class="line"><span class="cl">getent passwd             <span class="c1"># all users (NSS-aware: local, LDAP, AD)</span>
</span></span><span class="line"><span class="cl">getent passwd alice       <span class="c1"># one user entry</span>
</span></span><span class="line"><span class="cl">getent group              <span class="c1"># all groups</span>
</span></span><span class="line"><span class="cl">getent group sudo         <span class="c1"># details of one group</span>
</span></span><span class="line"><span class="cl">getent group <span class="p">|</span> grep alice <span class="c1"># all groups containing alice</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Local file lookups (local-only, not NSS-aware) ===</span>
</span></span><span class="line"><span class="cl">cut -d: -f1 /etc/passwd   <span class="c1"># usernames only</span>
</span></span><span class="line"><span class="cl">awk -F: <span class="s1">&#39;$3 &lt; 1000 {print $1, $3}&#39;</span> /etc/passwd   <span class="c1"># system accounts</span>
</span></span><span class="line"><span class="cl">awk -F: <span class="s1">&#39;$3 &gt;= 1000 {print $1, $3}&#39;</span> /etc/passwd  <span class="c1"># human accounts</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === macOS Directory Services ===</span>
</span></span><span class="line"><span class="cl">dscl . -list /Users             <span class="c1"># all users</span>
</span></span><span class="line"><span class="cl">dscl . -read /Users/alice       <span class="c1"># full record for &#39;alice&#39;</span>
</span></span><span class="line"><span class="cl">id alice                        <span class="c1"># UID/GID/groups still works</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === BSD variants ===</span>
</span></span><span class="line"><span class="cl">pw usershow alice               <span class="c1"># FreeBSD: show one user</span>
</span></span><span class="line"><span class="cl">pw groupshow wheel              <span class="c1"># FreeBSD: show one group</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>Prefer <code>id -nG</code> and <code>getent</code> in scripts (NSS-aware).</li>
<li><code>/etc/passwd</code> shows local users only, which may miss LDAP/AD accounts.</li>
<li>On macOS, <code>dscl</code> is the source of truth.</li>
</ul>
  </div>
</details>

<hr>
<h3 id="create-users"><span style="color:#CC0000;">Create Users</span></h3>
<p>Create human accounts or service accounts.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Linux (Debian/Ubuntu) ===</span>
</span></span><span class="line"><span class="cl">sudo adduser alice                  <span class="c1"># interactive, sets password, creates home, shell</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Linux (RHEL/Fedora) ===</span>
</span></span><span class="line"><span class="cl">sudo useradd -m -s /bin/bash alice  <span class="c1"># create with home and shell</span>
</span></span><span class="line"><span class="cl">sudo passwd alice                   <span class="c1"># set password</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Service/system accounts ===</span>
</span></span><span class="line"><span class="cl">sudo useradd -r -s /usr/sbin/nologin -d /var/www www-data
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === macOS 10.13+ ===</span>
</span></span><span class="line"><span class="cl">sudo sysadminctl -addUser alice -fullName <span class="s2">&#34;Alice Smith&#34;</span> -password -
</span></span><span class="line"><span class="cl"><span class="c1"># Legacy alternative:</span>
</span></span><span class="line"><span class="cl">sudo dscl . -create /Users/alice
</span></span><span class="line"><span class="cl">sudo dscl . -create /Users/alice UserShell /bin/zsh
</span></span><span class="line"><span class="cl">sudo dscl . -create /Users/alice RealName <span class="s2">&#34;Alice Smith&#34;</span>
</span></span><span class="line"><span class="cl">sudo dscl . -create /Users/alice UniqueID <span class="s2">&#34;501&#34;</span>
</span></span><span class="line"><span class="cl">sudo dscl . -create /Users/alice PrimaryGroupID <span class="m">20</span>
</span></span><span class="line"><span class="cl">sudo dscl . -create /Users/alice NFSHomeDirectory /Users/alice
</span></span><span class="line"><span class="cl">sudo dscl . -passwd /Users/alice password</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li><code>adduser</code> (Debian) is friendlier than <code>useradd</code> (RHEL).</li>
<li>Always use <code>-m</code> with <code>useradd</code> to ensure a home directory is created.</li>
<li>On macOS, <code>sysadminctl</code> is preferred, but <code>dscl</code> gives more fine-grained control.</li>
</ul>
  </div>
</details>

<hr>
<h3 id="modify-users"><span style="color:#CC0000;">Modify Users</span></h3>
<p>Change passwords, shells, groups, or lock accounts.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Passwords ===</span>
</span></span><span class="line"><span class="cl">passwd alice                <span class="c1"># change password</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Shell ===</span>
</span></span><span class="line"><span class="cl">chsh -s /bin/zsh alice      <span class="c1"># change login shell</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Groups ===</span>
</span></span><span class="line"><span class="cl">usermod -aG sudo alice      <span class="c1"># add alice to group (Linux)</span>
</span></span><span class="line"><span class="cl">gpasswd -a alice developers <span class="c1"># alternative on some distros</span>
</span></span><span class="line"><span class="cl">gpasswd -d alice developers <span class="c1"># remove from group</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Lock/Unlock ===</span>
</span></span><span class="line"><span class="cl">usermod -L alice            <span class="c1"># lock account (Linux)</span>
</span></span><span class="line"><span class="cl">usermod -U alice            <span class="c1"># unlock account (Linux)</span>
</span></span><span class="line"><span class="cl">passwd -l alice             <span class="c1"># lock via passwd tool</span>
</span></span><span class="line"><span class="cl">passwd -u alice             <span class="c1"># unlock</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === macOS ===</span>
</span></span><span class="line"><span class="cl">dscl . -change /Users/alice UserShell /bin/bash /bin/zsh
</span></span><span class="line"><span class="cl">dscl . -append /Groups/admin GroupMembership alice</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>⚠️ <code>usermod -G</code> <strong>without <code>-a</code></strong> replaces all groups.</li>
<li>Locking prepends <code>!</code> to the shadow password field.</li>
<li>macOS uses <code>dscl</code> to edit user attributes and groups.</li>
</ul>
  </div>
</details>

<hr>
<h3 id="delete-users"><span style="color:#CC0000;">Delete Users</span></h3>
<p>Remove accounts and clean up files.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Linux ===</span>
</span></span><span class="line"><span class="cl">sudo userdel alice               <span class="c1"># delete user, keep files</span>
</span></span><span class="line"><span class="cl">sudo userdel -r alice            <span class="c1"># delete user and home directory</span>
</span></span><span class="line"><span class="cl">sudo deluser alice               <span class="c1"># Debian helper</span>
</span></span><span class="line"><span class="cl">sudo deluser --remove-home alice <span class="c1"># remove home too</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Find orphaned files ===</span>
</span></span><span class="line"><span class="cl">find / -nouser -o -nogroup 2&gt;/dev/null
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === macOS ===</span>
</span></span><span class="line"><span class="cl">sudo sysadminctl -deleteUser alice
</span></span><span class="line"><span class="cl"><span class="c1"># Or, with dscl:</span>
</span></span><span class="line"><span class="cl">sudo dscl . -delete /Users/alice</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>Orphaned files will show as owned by a numeric UID.</li>
<li>Always search for <code>-nouser</code> files after deletion.</li>
<li>On macOS, <code>sysadminctl</code> handles home directory cleanup.</li>
</ul>
  </div>
</details>

<hr>
<h3 id="manage-groups"><span style="color:#CC0000;">Manage Groups</span></h3>
<p>Groups define shared access.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Linux ===</span>
</span></span><span class="line"><span class="cl">groupadd developers           <span class="c1"># create group</span>
</span></span><span class="line"><span class="cl">groupdel developers           <span class="c1"># delete group</span>
</span></span><span class="line"><span class="cl">usermod -aG developers alice  <span class="c1"># add to group</span>
</span></span><span class="line"><span class="cl">gpasswd -d alice developers   <span class="c1"># remove from group</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === BSD ===</span>
</span></span><span class="line"><span class="cl">pw groupadd developers
</span></span><span class="line"><span class="cl">pw groupmod developers -m alice
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === macOS ===</span>
</span></span><span class="line"><span class="cl">dscl . -create /Groups/devs
</span></span><span class="line"><span class="cl">dscl . -append /Groups/devs GroupMembership alice
</span></span><span class="line"><span class="cl">dscl . -delete /Groups/devs</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>Common admin groups: <code>sudo</code> (Ubuntu), <code>wheel</code> (RHEL/BSD), <code>admin</code> (macOS).</li>
<li>Setgid bit on directories (<code>chmod g+s</code>) makes files inherit group ownership.</li>
</ul>
  </div>
</details>

<hr>
<h3 id="classic-multiuser-tools"><span style="color:#CC0000;">Classic Multiuser Tools</span></h3>
<p>Old-school, but still around.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">who              <span class="c1"># list logged-in users</span>
</span></span><span class="line"><span class="cl">w                <span class="c1"># list logged-in users + what they’re doing</span>
</span></span><span class="line"><span class="cl">users            <span class="c1"># just usernames</span>
</span></span><span class="line"><span class="cl">last             <span class="c1"># login history</span>
</span></span><span class="line"><span class="cl">write bob        <span class="c1"># message another user</span>
</span></span><span class="line"><span class="cl">wall <span class="s2">&#34;msg&#34;</span>       <span class="c1"># broadcast to all users</span>
</span></span><span class="line"><span class="cl">talk bob         <span class="c1"># split-screen chat</span>
</span></span><span class="line"><span class="cl">finger bob       <span class="c1"># show user info (if finger service enabled)</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>These tools reflect the <strong>multiuser roots</strong> of Unix — a reminder that one<br>
machine often served an entire lab or office.</li>
<li>Still useful today for <strong>audits, troubleshooting, or curiosity</strong> (e.g.<br>
spotting a forgotten session or checking login history).</li>
<li>Messaging commands like <code>write</code>, <code>wall</code>, and <code>talk</code> are often disabled on<br>
modern systems, and <code>finger</code> is usually missing entirely due to security<br>
concerns.</li>
<li>The <code>finger</code> command would also display a user’s <code>~/.plan</code> file — a personal<br>
status note people used for anything from office hours to quirky quotes.<br>
In the early internet, <code>.plan</code> files became a proto–status update, years<br>
before blogs or Twitter.</li>
</ul>
  </div>
</details>

<hr>
<h3 id="sudo--visudo"><span style="color:#CC0000;">Sudo &amp; visudo</span></h3>
<p>The <code>/etc/sudoers</code> file decides who can act as root (or another user).<br>
Always use <code>visudo</code> to edit it — it locks the file and checks syntax so you don’t brick sudo.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Change default editor (defaults to vi) ===</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">EDITOR</span><span class="o">=</span>nano
</span></span><span class="line"><span class="cl">sudo visudo
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Rule format ===</span>
</span></span><span class="line"><span class="cl">user_or_%group   <span class="nv">host</span> <span class="o">=</span> <span class="o">(</span>run_as<span class="o">)</span> command_list
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Parts of a rule:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># - user_or_%group → single user (alice) or group (%wheel)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># - host           → usually ALL unless restricted to specific hosts</span>
</span></span><span class="line"><span class="cl"><span class="c1"># - run_as         → ALL (default root) or another user (postgres, deploy)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># - command_list   → full path(s) to allowed commands</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Open and validate sudoers ===</span>
</span></span><span class="line"><span class="cl">sudo visudo            <span class="c1"># edit main sudoers file safely</span>
</span></span><span class="line"><span class="cl">sudo visudo -c         <span class="c1"># check config syntax only</span>
</span></span><span class="line"><span class="cl">sudo visudo -f /etc/sudoers.d/webadmins   <span class="c1"># edit a drop-in file</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Give one user full root powers ===</span>
</span></span><span class="line"><span class="cl">alice <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> ALL
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Group-based full access ===</span>
</span></span><span class="line"><span class="cl">%wheel <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> ALL      <span class="c1"># common on RHEL/BSD</span>
</span></span><span class="line"><span class="cl">%sudo  <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> ALL      <span class="c1"># common on Debian/Ubuntu</span>
</span></span><span class="line"><span class="cl">%admin <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> ALL      <span class="c1"># common on macOS</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Limit bob to restarting nginx only ===</span>
</span></span><span class="line"><span class="cl">bob <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> /usr/bin/systemctl restart nginx
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Let webadmins group manage nginx + apache ===</span>
</span></span><span class="line"><span class="cl">%webadmins <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> /usr/bin/systemctl restart nginx, <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>                     /usr/bin/systemctl restart apache2
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Allow package updates only ===</span>
</span></span><span class="line"><span class="cl">dave <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> /usr/bin/apt update, /usr/bin/apt upgrade
</span></span><span class="line"><span class="cl">dave <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> /usr/bin/yum update
</span></span><span class="line"><span class="cl">dave <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> /usr/bin/dnf upgrade
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Run Docker commands without full root ===</span>
</span></span><span class="line"><span class="cl">carol <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> /usr/bin/docker ps, /usr/bin/docker restart *
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Run commands as a different user (deploy) ===</span>
</span></span><span class="line"><span class="cl">carol <span class="nv">ALL</span><span class="o">=(</span>deploy<span class="o">)</span> /usr/bin/git pull, /usr/bin/git checkout
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Force password every time (no caching) ===</span>
</span></span><span class="line"><span class="cl">Defaults <span class="nv">timestamp_timeout</span><span class="o">=</span><span class="m">0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Allow passwordless sudo (⚠️ dangerous) ===</span>
</span></span><span class="line"><span class="cl">alice <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> NOPASSWD: ALL</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>Prefer drop-in configs under <code>/etc/sudoers.d/</code> instead of cluttering <code>/etc/sudoers</code>.</li>
<li>Always use <strong>absolute paths</strong> to commands in rules (<code>which systemctl</code>).</li>
<li>Use groups (<code>%group</code>) to manage privileges cleanly for teams.</li>
<li>Check syntax anytime with <code>sudo visudo -c</code>.</li>
</ul>
<p>🔗 <strong>Docs &amp; References:</strong></p>
<ul>
<li><a href="https://www.sudo.ws/docs/man/sudoers.man/">man 5 sudoers</a></li>
<li><a href="https://www.sudo.ws/docs/man/sudoers.man/">Sudoers Manual</a></li>
<li><a href="https://wiki.archlinux.org/title/sudo">ArchWiki: Sudo</a></li>
</ul>

  </div>
</details>

<hr>
<h3 id="troubleshooting"><span style="color:#CC0000;">Troubleshooting</span></h3>
<p>When a user can’t log in, can’t write files, or <code>sudo</code> mysteriously fails, these checks will save you.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === File permission issues ===</span>
</span></span><span class="line"><span class="cl">ls -l file                  <span class="c1"># check ownership + rwx bits</span>
</span></span><span class="line"><span class="cl">id alice                    <span class="c1"># confirm UID + GIDs</span>
</span></span><span class="line"><span class="cl">groups alice                <span class="c1"># confirm group memberships</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Login problems ===</span>
</span></span><span class="line"><span class="cl">passwd -S alice             <span class="c1"># Linux: check password status (L=locked, P=usable)</span>
</span></span><span class="line"><span class="cl">chage -l alice              <span class="c1"># Linux: check password aging + expiry</span>
</span></span><span class="line"><span class="cl">grep ^alice: /etc/passwd    <span class="c1"># check home dir + shell field</span>
</span></span><span class="line"><span class="cl">dscl . -read /Users/alice   <span class="c1"># macOS: inspect account record</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Orphaned files or groups ===</span>
</span></span><span class="line"><span class="cl">find / -nouser -o -nogroup 2&gt;/dev/null   <span class="c1"># files with no matching UID/GID</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Process ownership ===</span>
</span></span><span class="line"><span class="cl">ps -u alice                 <span class="c1"># all processes owned by alice</span>
</span></span><span class="line"><span class="cl">pgrep -u alice              <span class="c1"># list PIDs only</span>
</span></span><span class="line"><span class="cl">pkill -u alice              <span class="c1"># kill all processes for alice (⚠️ destructive)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Sudo debugging ===</span>
</span></span><span class="line"><span class="cl">sudo -l                     <span class="c1"># list sudo rights for current user</span>
</span></span><span class="line"><span class="cl">sudo -v                     <span class="c1"># refresh credentials (prompts password)</span>
</span></span><span class="line"><span class="cl">sudo -k                     <span class="c1"># expire cached credentials</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>If <code>/etc/passwd</code> shows <code>/usr/sbin/nologin</code>, <code>/sbin/nologin</code>, or <code>/bin/false</code>, the user cannot log in interactively.</li>
<li>Account expiry or locks often explain mysterious login failures (<code>passwd -S</code>, <code>chage -l</code>).</li>
<li>Always check group membership (<code>id -nG</code>) when file access doesn’t make sense.</li>
<li>On macOS, many system accounts start with <code>_</code> and are not intended for login.</li>
</ul>
  </div>
</details>

<br>
<figure style="text-align:center; margin: 1em auto;">
  <img src="orcs.jpg" 
       alt="a pixel art image of 3 orcs armed with spears and swords in a dungeon setting" 
       style="display:block; margin:0 auto; width:min(100%, 400px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Onward to advanced topics — time to deal with pesky orcs.  
  </figcaption>
</figure>
<h2 id="advanced-topics">Advanced Topics</h2>
<p>The basics cover 90% of admin life, but sometimes you need sharper tools. These features extend the Unix user model into modern territory.</p>
<hr>
<h3 id="defaults--templates"><span style="color:#CC0000;">Defaults &amp; Templates</span></h3>
<p>When a new account is created, the system applies defaults: skeleton files, UID ranges, shells, and the <strong>default mask</strong> (<code>umask</code>). These define how a fresh user’s environment looks and how secure their files are.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Skeleton files ===</span>
</span></span><span class="line"><span class="cl">ls -A /etc/skel          <span class="c1"># files copied into new home dirs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># .bashrc  .profile  .bash_logout</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Add a custom file for all new users</span>
</span></span><span class="line"><span class="cl">sudo cp /etc/motd /etc/skel/welcome.txt
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Default umask ===</span>
</span></span><span class="line"><span class="cl"><span class="nb">umask</span>                    <span class="c1"># show current mask</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 0022 → new files 644 (-rw-r--r--) and dirs 755 (drwxr-xr-x)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Change umask for tighter privacy (per shell session)</span>
</span></span><span class="line"><span class="cl"><span class="nb">umask</span> <span class="m">0077</span>
</span></span><span class="line"><span class="cl">touch secret.txt <span class="o">&amp;&amp;</span> ls -l secret.txt
</span></span><span class="line"><span class="cl"><span class="c1"># -rw------- 1 alice users 0 Sep 21 16:10 secret.txt</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Global login defaults (Linux) ===</span>
</span></span><span class="line"><span class="cl">grep -E <span class="s1">&#39;UID_MIN|UID_MAX|GID_MIN|GID_MAX&#39;</span> /etc/login.defs
</span></span><span class="line"><span class="cl"><span class="c1"># UID_MIN 1000, UID_MAX 60000</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>
<p><strong>How <code>/etc/skel</code> works:</strong></p>
<ul>
<li>Any file or directory in <code>/etc/skel</code> gets copied into the new user’s home when the account is created (unless disabled with <code>useradd -M -k</code>).</li>
<li>Typical contents: <code>.bashrc</code>, <code>.profile</code>, <code>.bash_logout</code>.</li>
<li>You can also include a company-wide <code>README</code>, a <code>welcome.txt</code>, or even preconfigured dotfiles like <code>.vimrc</code> or <code>.gitconfig</code>.</li>
<li>Updating <code>/etc/skel</code> only affects <em>future</em> accounts, not existing ones.</li>
</ul>
</li>
<li>
<p><strong>How <code>umask</code> works:</strong></p>
<ul>
<li>It’s a <strong>subtractive mask</strong>: permissions are removed from the base defaults (666 for files, 777 for directories).</li>
<li>Example:
<ul>
<li><code>umask 0022</code> → files 644 (<code>rw-r--r--</code>), dirs 755 (<code>rwxr-xr-x</code>) → standard, readable by everyone.</li>
<li><code>umask 0077</code> → files 600 (<code>rw-------</code>), dirs 700 (<code>rwx------</code>) → private, nobody else can read.</li>
<li><code>umask 0002</code> → files 664, dirs 775 → collaborative group environments.</li>
</ul>
</li>
<li>Why change it?
<ul>
<li>Servers often use <code>0022</code> (safe default).</li>
<li>Multiuser/dev environments may prefer <code>0002</code> so teams in the same group can share files easily.</li>
<li>Security-sensitive environments often use <code>0077</code> to prevent accidental leakage.</li>
</ul>
</li>
</ul>
</li>
<li>
<p><strong>Other considerations:</strong></p>
<ul>
<li><code>umask</code> can differ between shells, cron jobs, and systemd services.</li>
<li>Linux: defaults can be set in <code>/etc/login.defs</code> or PAM config.</li>
<li>BSD: see <code>/etc/adduser.conf</code>.</li>
<li>macOS: relies on <code>sysadminctl</code> + Directory Services defaults.</li>
</ul>
</li>
</ul>

  </div>
</details>

<hr>
<h3 id="name-service-switch--domains"><span style="color:#CC0000;">Name Service Switch &amp; Domains</span></h3>
<p>On modern systems, <code>/etc/passwd</code> is just one source of truth. Enterprises often keep users in <strong>LDAP</strong>, <strong>Kerberos realms</strong>, or <strong>Active Directory</strong>. The <strong>Name Service Switch (NSS)</strong> decides where the system looks when resolving usernames, groups, and hosts.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === NSS lookup order (Linux) ===</span>
</span></span><span class="line"><span class="cl">grep passwd /etc/nsswitch.conf
</span></span><span class="line"><span class="cl"><span class="c1"># passwd: files systemd sss</span>
</span></span><span class="line"><span class="cl"><span class="c1"># &#34;files&#34; = /etc/passwd, &#34;systemd&#34; = local systemd users, &#34;sss&#34; = SSSD (LDAP/AD)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Query via NSS (all backends) ===</span>
</span></span><span class="line"><span class="cl">getent passwd alice          <span class="c1"># works even if alice is in LDAP/AD</span>
</span></span><span class="line"><span class="cl">getent group devs            <span class="c1"># query group membership</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Show only local file (not NSS aware) ===</span>
</span></span><span class="line"><span class="cl">cat /etc/passwd <span class="p">|</span> grep alice <span class="c1"># will miss LDAP/AD users</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Join an AD domain (Linux, via realmd/SSSD) ===</span>
</span></span><span class="line"><span class="cl">realm discover example.com   <span class="c1"># discover domain controllers</span>
</span></span><span class="line"><span class="cl">sudo realm join example.com  <span class="c1"># join domain</span>
</span></span><span class="line"><span class="cl">systemctl status sssd        <span class="c1"># domain users now available via SSSD</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Test a domain account ===</span>
</span></span><span class="line"><span class="cl">id alice@example.com
</span></span><span class="line"><span class="cl"><span class="c1"># uid=123456789(alice@example.com) gid=123456789(domain users) groups=...</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>
<p><strong>NSS vs PAM:</strong></p>
<ul>
<li><strong>NSS</strong> answers “does this identity exist?” (user/group lookups).</li>
<li><strong>PAM</strong> answers “can this identity log in?” (authentication, password policy, session rules).</li>
</ul>
</li>
<li>
<p><strong>Why <code>getent</code> matters:</strong></p>
<ul>
<li><code>getent</code> queries the entire NSS stack — so LDAP, AD, or other remote backends are included.</li>
<li><code>cat /etc/passwd</code> only shows local users and will <em>miss</em> network accounts.</li>
</ul>
</li>
<li>
<p><strong>SSSD &amp; caching:</strong></p>
<ul>
<li>On Linux, <strong>SSSD</strong> (System Security Services Daemon) acts as the glue for LDAP/AD lookups.</li>
<li>It caches users for performance and offline login.</li>
<li>If users/groups seem stale, clear the cache:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sss_cache -E
</span></span></code></pre></div></li>
</ul>
</li>
<li>
<p><strong>Joining domains:</strong></p>
<ul>
<li><code>realmd</code> + <code>sssd</code> is the common modern combo (RHEL, Fedora, Ubuntu).</li>
<li>Older setups may use <strong>nslcd</strong> or <strong>winbind</strong> for LDAP/AD.</li>
<li>macOS has its own directory service integration (<code>dsconfigad</code>).</li>
<li>BSD systems typically rely on <code>nss_ldap</code> + <code>pam_ldap</code>.</li>
</ul>
</li>
<li>
<p><strong>Troubleshooting tip:</strong></p>
<ul>
<li>Always check <code>nsswitch.conf</code> first — if “sss” or “ldap” isn’t listed for <code>passwd</code> and <code>group</code>, your system won’t even <em>try</em> querying the domain.</li>
</ul>
</li>
</ul>

  </div>
</details>

<hr>
<h3 id="access-control-lists-acls"><span style="color:#CC0000;">Access Control Lists (ACLs)</span></h3>
<p>The classic Unix model (user/group/other) is simple but limited. What if you want <strong>multiple users</strong> with different rights on the same file, without changing ownership or creating new groups? That’s where <strong>Access Control Lists (ACLs)</strong> come in. ACLs add fine-grained permissions on top of the traditional model.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === See current permissions (no ACL yet) ===</span>
</span></span><span class="line"><span class="cl">ls -l project.txt
</span></span><span class="line"><span class="cl"><span class="c1"># -rw-r----- 1 alice devs 42 Sep 21 17:00 project.txt</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Only alice (owner) has rw, devs group has r, others none.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Add bob with RW access via ACL ===</span>
</span></span><span class="line"><span class="cl">setfacl -m u:bob:rw project.txt
</span></span><span class="line"><span class="cl">getfacl project.txt
</span></span><span class="line"><span class="cl"><span class="c1"># file: project.txt</span>
</span></span><span class="line"><span class="cl"><span class="c1"># owner: alice</span>
</span></span><span class="line"><span class="cl"><span class="c1"># group: devs</span>
</span></span><span class="line"><span class="cl">user::rw-
</span></span><span class="line"><span class="cl">user:bob:rw-          <span class="c1"># new ACL entry</span>
</span></span><span class="line"><span class="cl">group::r--
</span></span><span class="line"><span class="cl">mask::rw-
</span></span><span class="line"><span class="cl">other::---
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Remove bob&#39;s entry later ===</span>
</span></span><span class="line"><span class="cl">setfacl -x u:bob project.txt
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Default ACL on a directory ===</span>
</span></span><span class="line"><span class="cl">setfacl -d -m g:devs:rwx /srv/project
</span></span><span class="line"><span class="cl">ls -ld /srv/project
</span></span><span class="line"><span class="cl"><span class="c1"># drwxrwxr-x+ 2 root root 4096 Sep 21 17:05 /srv/project</span>
</span></span><span class="line"><span class="cl"><span class="c1"># (+ indicates ACLs are set)</span>
</span></span><span class="line"><span class="cl">getfacl /srv/project
</span></span><span class="line"><span class="cl"><span class="c1"># default:group:devs:rwx</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Now any file created under /srv/project inherits group rwx.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === macOS NFSv4 ACLs ===</span>
</span></span><span class="line"><span class="cl">ls -le project.txt
</span></span><span class="line"><span class="cl">-rw-r-----+ <span class="m">1</span> alice staff <span class="m">42</span> Sep <span class="m">21</span> 17:10 project.txt
</span></span><span class="line"><span class="cl"> 0: user:bob allow read,write
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">chmod +a <span class="s2">&#34;bob allow read,write&#34;</span> project.txt</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>
<p><strong>Why ACLs?</strong></p>
<ul>
<li>u/g/o model works fine until you need to share a resource between <em>specific users</em> who don’t share a group.</li>
<li>ACLs let you grant per-user or per-group rights without redesigning ownership.</li>
<li>Great for project directories, shared data, or complex multiuser environments.</li>
</ul>
</li>
<li>
<p><strong>Default ACLs:</strong></p>
<ul>
<li>On directories, default ACLs ensure all <em>new files</em> inside inherit the access rules automatically.</li>
<li>Example: shared group workspaces, where all new files should be writable by <code>devs</code>.</li>
</ul>
</li>
<li>
<p><strong>Implementation differences:</strong></p>
<ul>
<li>Linux &amp; BSD: <code>setfacl</code>, <code>getfacl</code>.</li>
<li>macOS: NFSv4 ACLs with <code>ls -le</code> and <code>chmod +a</code>.</li>
<li>The <code>+</code> sign in <code>ls -l</code> output means “this file has ACLs.”</li>
</ul>
</li>
<li>
<p><strong>Gotchas:</strong></p>
<ul>
<li>Not all filesystems support ACLs (may need <code>mount -o acl</code> on ext4).</li>
<li>Backups that don’t preserve extended attributes may strip ACLs.</li>
<li>ACLs can make permissions confusing — always use <code>getfacl</code> to confirm.</li>
</ul>
</li>
</ul>

  </div>
</details>

<hr>
<h3 id="linux-capabilities"><span style="color:#CC0000;">Linux Capabilities</span></h3>
<p>Traditionally, if a program needed <em>any</em> privileged action (like opening a raw socket or binding to a low port), it had to be setuid root. That gave it <strong>full root power</strong>, even if it only needed one small permission.</p>
<p><strong>Linux capabilities</strong> break root’s powers into fine-grained units — like <code>CAP_NET_RAW</code> (raw sockets) or <code>CAP_SYS_ADMIN</code> (system-wide admin). You can then grant a binary <em>just the slice it needs</em> instead of all-or-nothing root.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Traditional setuid ping ===</span>
</span></span><span class="line"><span class="cl">ls -l /bin/ping
</span></span><span class="line"><span class="cl"><span class="c1"># -rwsr-xr-x 1 root root ...</span>
</span></span><span class="line"><span class="cl"><span class="c1"># setuid root: ping runs as full root just to open raw sockets.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Replace with capability ===</span>
</span></span><span class="line"><span class="cl">sudo chmod u-s /bin/ping                   <span class="c1"># remove setuid bit</span>
</span></span><span class="line"><span class="cl">sudo setcap cap_net_raw+ep /bin/ping       <span class="c1"># give only raw socket ability</span>
</span></span><span class="line"><span class="cl">getcap /bin/ping
</span></span><span class="line"><span class="cl"><span class="c1"># /bin/ping = cap_net_raw+ep</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Test that it works ===</span>
</span></span><span class="line"><span class="cl">ping -c1 127.0.0.1                         <span class="c1"># works without setuid</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Drop capabilities from a running process (demo with sleep) ===</span>
</span></span><span class="line"><span class="cl">sleep <span class="m">100</span> <span class="p">&amp;</span>
</span></span><span class="line"><span class="cl"><span class="nv">pid</span><span class="o">=</span><span class="nv">$!</span>
</span></span><span class="line"><span class="cl">grep CapEff /proc/<span class="nv">$pid</span>/status              <span class="c1"># shows effective caps (usually 0000000000000000)</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>
<p><strong>Safer than setuid:</strong></p>
<ul>
<li>Old way: <code>ping</code> had full root rights — if exploited, attacker gets root.</li>
<li>With capabilities: attacker only gains the one granted privilege.</li>
</ul>
</li>
<li>
<p><strong>How capabilities are assigned:</strong></p>
<ul>
<li><code>setcap cap_name+ep file</code> → give a binary capability (<code>e=effective</code>, <code>p=permitted</code>).</li>
<li><code>getcap file</code> → check capabilities.</li>
<li><code>capsh --print</code> → view current shell’s capabilities.</li>
</ul>
</li>
<li>
<p><strong>Common useful capabilities:</strong></p>
<ul>
<li><code>CAP_NET_BIND_SERVICE</code> → bind to ports &lt;1024 without root.</li>
<li><code>CAP_NET_ADMIN</code> → manage networking.</li>
<li><code>CAP_SYS_TIME</code> → set the system clock.</li>
<li><code>CAP_SYS_ADMIN</code> → (⚠️ extremely broad, “root-lite”).</li>
</ul>
</li>
<li>
<p><strong>Gotchas:</strong></p>
<ul>
<li>Capabilities are stored as extended attributes — they don’t survive a normal <code>cp</code>. Use <code>rsync -aX</code> or <code>install -m755 -o root -g root</code>.</li>
<li>Some filesystems don’t support extended attributes (e.g. older NFS).</li>
<li>Granting too many caps (especially <code>CAP_SYS_ADMIN</code>) defeats the purpose.</li>
</ul>
</li>
<li>
<p><strong>Why care?</strong></p>
<ul>
<li>Capabilities let you follow <em>least privilege</em> in service design.</li>
<li>Systemd services can also drop or restrict capabilities with <code>CapabilityBoundingSet=</code> in unit files.</li>
</ul>
</li>
</ul>

  </div>
</details>

<hr>
<h3 id="systemd-dynamic-users"><span style="color:#CC0000;">Systemd Dynamic Users</span></h3>
<p>Normally, services run under pre-created system accounts like <code>www-data</code> or <code>mysql</code>. But that clutters <code>/etc/passwd</code> with dozens of long-lived identities that stick around even if the service is removed.</p>
<p><strong>Dynamic users</strong> solve this: systemd can allocate a <strong>throwaway UID/GID at runtime</strong> when the service starts. When the service stops, the UID disappears. It’s perfect for daemons that don’t need persistent files or shells.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/systemd/system/web.service</span>
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/usr/bin/mydaemon</span>
</span></span><span class="line"><span class="cl"><span class="na">DynamicUser</span><span class="o">=</span><span class="s">yes                    # allocate ephemeral UID at runtime</span>
</span></span><span class="line"><span class="cl"><span class="na">CapabilityBoundingSet</span><span class="o">=</span><span class="s">CAP_NET_BIND_SERVICE</span>
</span></span><span class="line"><span class="cl"><span class="na">ProtectSystem</span><span class="o">=</span><span class="s">strict</span>
</span></span><span class="line"><span class="cl"><span class="na">ProtectHome</span><span class="o">=</span><span class="s">yes</span>
</span></span><span class="line"><span class="cl"><span class="na">PrivateTmp</span><span class="o">=</span><span class="s">yes</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>
<p><strong>How it works:</strong></p>
<ul>
<li>Each start assigns a UID like <code>dynamic-1000</code>.</li>
<li>No entry is written to <code>/etc/passwd</code>; it’s entirely managed by systemd.</li>
<li>The UID disappears once the service stops.</li>
</ul>
</li>
<li>
<p><strong>When to use:</strong></p>
<ul>
<li>For services that don’t need a home directory or persistent files.</li>
<li>Great for stateless daemons, network listeners, or sandboxed apps.</li>
</ul>
</li>
<li>
<p><strong>Persistent data:</strong></p>
<ul>
<li>Use <code>StateDirectory=</code>, <code>CacheDirectory=</code>, or <code>LogsDirectory=</code> in the unit file to create system-managed dirs with correct ownership.</li>
<li>Example:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">StateDirectory</span><span class="o">=</span><span class="s">mydaemon</span>
</span></span></code></pre></div>→ systemd creates <code>/var/lib/mydaemon/</code> owned by the dynamic UID.</li>
</ul>
</li>
<li>
<p><strong>Security hardening:</strong></p>
<ul>
<li>Combine <code>DynamicUser=yes</code> with:
<ul>
<li><code>ProtectSystem=strict</code> → service sees <code>/usr</code> as read-only.</li>
<li><code>ProtectHome=yes</code> → blocks access to <code>/home</code>.</li>
<li><code>PrivateTmp=yes</code> → gives the service its own <code>/tmp</code>.</li>
<li><code>NoNewPrivileges=yes</code> → prevents privilege escalation.</li>
</ul>
</li>
</ul>
</li>
<li>
<p><strong>Gotchas:</strong></p>
<ul>
<li>No permanent account entry → you can’t <code>su</code> or <code>ssh</code> into it.</li>
<li>UIDs are reused; you can’t rely on the number being the same between runs.</li>
<li>If the service needs to write to disk, you <em>must</em> use the <code>StateDirectory</code>/<code>CacheDirectory</code> approach, or files will be inaccessible.</li>
</ul>
</li>
</ul>

  </div>
</details>

<hr>
<h3 id="rootless-containers--subuids"><span style="color:#CC0000;">Rootless Containers &amp; Subuids</span></h3>
<p>Normally, containers run as <code>root</code>, which maps directly to the host’s root — a big risk if the container is compromised. Rootless containers avoid this by <strong>mapping container UIDs/GIDs to high-numbered “subuids” and “subgids” on the host</strong>.</p>
<p>That way, <code>root</code> inside the container is really just UID 100000+ on the host — isolated, unprivileged, and unable to harm the real system.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Show subuid/subgid allocation ===</span>
</span></span><span class="line"><span class="cl">grep alice /etc/subuid /etc/subgid
</span></span><span class="line"><span class="cl"><span class="c1"># alice:100000:65536</span>
</span></span><span class="line"><span class="cl"><span class="c1"># means: user &#39;alice&#39; gets a block of 65,536 UIDs/GIDs starting at 100000.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Run a rootless container ===</span>
</span></span><span class="line"><span class="cl">podman run --rm alpine id
</span></span><span class="line"><span class="cl"><span class="nv">uid</span><span class="o">=</span>0<span class="o">(</span>root<span class="o">)</span> <span class="nv">gid</span><span class="o">=</span>0<span class="o">(</span>root<span class="o">)</span> <span class="nv">groups</span><span class="o">=</span>0<span class="o">(</span>root<span class="o">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># From inside the container it looks like root,</span>
</span></span><span class="line"><span class="cl"><span class="c1"># but on the host it&#39;s actually UID 100000+ from /etc/subuid.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Inspect namespace mapping ===</span>
</span></span><span class="line"><span class="cl">podman unshare cat /proc/self/uid_map
</span></span><span class="line"><span class="cl"><span class="c1"># 0 100000 65536</span>
</span></span><span class="line"><span class="cl"><span class="c1"># &#34;container UID 0 maps to host UID 100000, size 65536&#34;</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>
<p><strong>How it works:</strong></p>
<ul>
<li><code>/etc/subuid</code> and <code>/etc/subgid</code> allocate “ranges” of host IDs to a user.</li>
<li>When a rootless container starts, container UID 0 → host UID 100000, container UID 1 → host UID 100001, etc.</li>
<li>This mapping isolates container processes from the host.</li>
</ul>
</li>
<li>
<p><strong>Why it matters:</strong></p>
<ul>
<li>Running as root in a container no longer equals root on the host.</li>
<li>Even if the container is compromised, the attacker only controls high-numbered, unprivileged UIDs.</li>
<li>This is how tools like <strong>Podman</strong>, <strong>Buildah</strong>, and rootless <strong>Docker</strong> enforce least privilege.</li>
</ul>
</li>
<li>
<p><strong>Gotchas:</strong></p>
<ul>
<li>Without entries in <code>/etc/subuid</code> and <code>/etc/subgid</code>, rootless containers fail to start.</li>
<li>Each user gets ~65k IDs by default; this can be adjusted in <code>/etc/subuid</code>.</li>
<li>Requires <strong>user namespaces</strong> in the kernel (<code>CONFIG_USER_NS=y</code>).</li>
<li>Files created by container processes will show up on the host as UID 100000+, which can look odd in <code>ls -l</code>.</li>
</ul>
</li>
<li>
<p><strong>Related commands:</strong></p>
<ul>
<li><code>podman unshare</code> → enter the container’s user namespace for debugging.</li>
<li><code>newuidmap</code> / <code>newgidmap</code> → helper programs to set up ID ranges.</li>
</ul>
</li>
</ul>

  </div>
</details>

<hr>
<h3 id="password-policy--lockouts"><span style="color:#CC0000;">Password Policy &amp; Lockouts</span></h3>
<p>User accounts aren’t just about existence — they also have <strong>lifespans and safety rules</strong>. Password policy defines how often a user must change their password, how complex it must be, and how many failed logins before the account locks.</p>
<p>This is enforced through <strong>shadow file aging fields</strong> (Linux/Unix), <strong>PAM modules</strong> for lockouts, and platform-specific tools on macOS and BSD.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Linux: show password aging/expiry ===</span>
</span></span><span class="line"><span class="cl">chage -l alice
</span></span><span class="line"><span class="cl"><span class="c1"># Last password change                                    : Sep 21, 2025</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Password expires                                       : Nov 20, 2025</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Password inactive                                      : never</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Account expires                                        : never</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Minimum number of days between password change         : 0</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Maximum number of days between password change         : 60</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Number of days of warning before password expires      : 7</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Linux: check failed logins ===</span>
</span></span><span class="line"><span class="cl">faillock --user alice
</span></span><span class="line"><span class="cl"><span class="c1"># alice:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># When        Type  Source</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 2025-09-21  TTY   ssh:notty</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 2025-09-21  TTY   ssh:notty</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Lock out after 3 failures (RHEL/Ubuntu with pam_faillock)</span>
</span></span><span class="line"><span class="cl">sudo faillock --setdeny<span class="o">=</span><span class="m">3</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === BSD: enforce minimum password age ===</span>
</span></span><span class="line"><span class="cl">passwd -n <span class="m">30</span> alice     <span class="c1"># must wait 30 days before changing again</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === macOS: show password policy ===</span>
</span></span><span class="line"><span class="cl">pwpolicy -u alice -getpolicy
</span></span><span class="line"><span class="cl"><span class="c1"># prints dictionary of rules:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># usingHistory=15 minChars=8 requiresMixedCase=1 requiresNumeric=1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Set stricter policy on macOS</span>
</span></span><span class="line"><span class="cl">sudo pwpolicy -u alice -setpolicy <span class="s2">&#34;minChars=12 requiresMixedCase=1 requiresNumeric=1 requiresSymbol=1&#34;</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>
<p><strong>Why it matters:</strong></p>
<ul>
<li>Password aging prevents accounts from using the same password forever.</li>
<li>Lockouts protect against brute force attacks but can cause accidental denial of service.</li>
<li>Compliance frameworks (HIPAA, PCI-DSS, etc.) often mandate specific expiry and complexity rules.</li>
</ul>
</li>
<li>
<p><strong>Linux specifics:</strong></p>
<ul>
<li><code>chage</code> edits shadow file fields directly.</li>
<li><code>faillock</code> (PAM module) counts failed attempts and locks accounts temporarily.</li>
<li>Debian historically used <code>pam_tally2</code>, but newer distros prefer <code>faillock</code>.</li>
<li>Lockouts can be reset: <code>faillock --user alice --reset</code>.</li>
</ul>
</li>
<li>
<p><strong>BSD specifics:</strong></p>
<ul>
<li><code>passwd</code> options enforce password minimum/maximum ages.</li>
<li>Some BSDs use <code>login.conf</code> for global policy.</li>
</ul>
</li>
<li>
<p><strong>macOS specifics:</strong></p>
<ul>
<li><code>pwpolicy</code> manages per-user or global rules.</li>
<li>Many system accounts (like <code>_spotlight</code>) are exempt.</li>
<li><strong>SecureToken</strong>: separate from password policy, it controls FileVault unlock ability. Losing SecureToken can lock a user out of disk encryption.</li>
</ul>
</li>
<li>
<p><strong>Gotchas:</strong></p>
<ul>
<li>Expired accounts often just show as “login incorrect” with no obvious hint. Always check <code>chage -l</code>.</li>
<li>Too strict a lockout policy can become a DoS if an attacker keeps intentionally failing logins.</li>
<li>Remote directory systems (LDAP/AD) often enforce their own policies that override local rules.</li>
</ul>
</li>
</ul>

  </div>
</details>

<hr>
<h3 id="audit--logs"><span style="color:#CC0000;">Audit &amp; Logs</span></h3>
<p>When a user can’t log in, sudo fails, or permissions seem wrong, the <strong>logs tell the story</strong>. Different Unix-like systems store them in different places, but the principles are the same: check authentication logs, check system journals, and look at login history.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Debian/Ubuntu: SSH &amp; sudo events ===</span>
</span></span><span class="line"><span class="cl">tail -f /var/log/auth.log
</span></span><span class="line"><span class="cl"><span class="c1"># Sep 21 17:40 server sshd[2345]: Failed password for bob from 192.168.1.20 port 55312 ssh2</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Sep 21 17:42 server sudo:   alice : TTY=pts/0 ; PWD=/home/alice ; USER=root ; COMMAND=/bin/ls</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === RHEL/Fedora equivalents ===</span>
</span></span><span class="line"><span class="cl">tail -f /var/log/secure
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === systemd journal: filter by UID ===</span>
</span></span><span class="line"><span class="cl">journalctl <span class="nv">_UID</span><span class="o">=</span><span class="m">1000</span> --since today
</span></span><span class="line"><span class="cl"><span class="c1"># shows all messages generated by UID 1000 (alice)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === systemd journal: filter by command ===</span>
</span></span><span class="line"><span class="cl">journalctl <span class="nv">_COMM</span><span class="o">=</span>sudo -S today
</span></span><span class="line"><span class="cl"><span class="c1"># shows all sudo invocations since today</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Login history: successful ===</span>
</span></span><span class="line"><span class="cl">last
</span></span><span class="line"><span class="cl"><span class="c1"># alice   pts/0        192.168.1.20     Sun Sep 21 17:00   still logged in</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Login history: failed ===</span>
</span></span><span class="line"><span class="cl">lastb
</span></span><span class="line"><span class="cl"><span class="c1"># bob     ssh:notty    192.168.1.20     Sun Sep 21 17:40   still failed login</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>
<p><strong>File locations:</strong></p>
<ul>
<li>Debian/Ubuntu → <code>/var/log/auth.log</code>.</li>
<li>RHEL/Fedora → <code>/var/log/secure</code>.</li>
<li>BSD → <code>/var/log/auth.log</code> or <code>/var/log/messages</code> depending on config.</li>
<li>macOS → <code>/var/log/asl/</code> (older) or <code>log show --predicate 'eventMessage contains &quot;sshd&quot;'</code>.</li>
</ul>
</li>
<li>
<p><strong>systemd journal tips:</strong></p>
<ul>
<li><code>_UID=1000</code> → filter logs from a specific user ID.</li>
<li><code>_COMM=sudo</code> → filter by executable name.</li>
<li><code>-S yesterday</code> / <code>--since &quot;2025-09-20 18:00&quot;</code> → time filters.</li>
<li>Enable persistence:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo mkdir -p /var/log/journal
</span></span><span class="line"><span class="cl">sudo systemctl restart systemd-journald
</span></span></code></pre></div></li>
</ul>
</li>
<li>
<p><strong>Login history:</strong></p>
<ul>
<li><code>last</code> reads <code>/var/log/wtmp</code> → shows successful logins.</li>
<li><code>lastb</code> reads <code>/var/log/btmp</code> → shows failed logins (may need root to read).</li>
<li>Use <code>last -f /path/to/wtmp.old</code> to read rotated logs.</li>
</ul>
</li>
<li>
<p><strong>Why this matters:</strong></p>
<ul>
<li>Failed logins reveal brute-force attempts.</li>
<li><code>sudo</code> log entries show exactly which commands were run and by whom.</li>
<li>Filtering by UID is useful for tracing a specific account across the system.</li>
</ul>
</li>
<li>
<p><strong>Gotchas:</strong></p>
<ul>
<li>Journald defaults to in-memory logs; without persistence, entries vanish after reboot.</li>
<li>Log rotation may remove history faster than expected (<code>/etc/logrotate.d/</code>).</li>
<li><code>lastb</code> output can flood if you’re under SSH brute-force attack; use <code>grep</code> to filter by username.</li>
</ul>
</li>
</ul>

  </div>
</details>

<hr>
<h3 id="ssh-key-restrictions"><span style="color:#CC0000;">SSH Key Restrictions</span></h3>
<p>SSH public keys don’t just allow or deny login — you can <strong>control what they’re allowed to do</strong>. This is especially useful for automation accounts (backups, deploy scripts, CI/CD) where you don’t want full shell access.</p>
<p>Restrictions are written in <code>~/.ssh/authorized_keys</code> before the key itself. Multiple restrictions can be combined with commas.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># === Restrict login to a specific subnet ===
</span></span><span class="line"><span class="cl">from=&#34;192.168.1.0/24&#34; ssh-ed25519 AAAAC3Nza...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># === Force a command (ignore user input) ===
</span></span><span class="line"><span class="cl">command=&#34;/usr/local/bin/backup.sh&#34; ssh-ed25519 AAAAC3Nza...
</span></span><span class="line"><span class="cl"># When this key logs in, it *always* runs backup.sh — no shell access.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># === Disable shell/TTY allocation ===
</span></span><span class="line"><span class="cl">no-pty,command=&#34;/usr/bin/rsync --server --sender ...&#34; ssh-ed25519 AAAAC3Nza...
</span></span><span class="line"><span class="cl"># Useful for file transfers only.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># === Combine multiple restrictions ===
</span></span><span class="line"><span class="cl">from=&#34;10.0.0.5&#34;,no-pty,command=&#34;/usr/local/bin/deploy.sh&#34; ssh-ed25519 AAAAC3Nza...
</span></span><span class="line"><span class="cl"># Only works from 10.0.0.5, no interactive shell, forced deploy script.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># === Log key usage for auditing ===
</span></span><span class="line"><span class="cl">environment=&#34;DEPLOY_KEY_ID=ci-runner&#34; ssh-ed25519 AAAAC3Nza...
</span></span><span class="line"><span class="cl"># Adds DEPLOY_KEY_ID to environment for logging in scripts.</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>
<p><strong>Why do this?</strong></p>
<ul>
<li>Automation accounts (backups, deployments, monitoring) don’t need full shell access.</li>
<li>Restricting keys reduces the blast radius if a key leaks.</li>
</ul>
</li>
<li>
<p><strong>Common options:</strong></p>
<ul>
<li><code>from=&quot;addrlist&quot;</code> → restrict to specific IPs or subnets.</li>
<li><code>command=&quot;cmd&quot;</code> → always run this command instead of a shell.</li>
<li><code>no-pty</code> → disables interactive sessions.</li>
<li><code>environment=&quot;VAR=value&quot;</code> → injects env vars, useful for logging or scripts.</li>
<li><code>restrict</code> (newer OpenSSH) → a safe default that implies multiple restrictions (no port forwarding, no agent, no PTY).</li>
</ul>
</li>
<li>
<p><strong>Extra hardening:</strong></p>
<ul>
<li>Combine with <code>Match User</code> or <code>Match Address</code> blocks in <code>sshd_config</code>.</li>
<li>Example:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">Match User backup
</span></span><span class="line"><span class="cl">    ChrootDirectory /backups
</span></span><span class="line"><span class="cl">    ForceCommand /usr/local/bin/backup.sh
</span></span></code></pre></div></li>
</ul>
</li>
<li>
<p><strong>Gotchas:</strong></p>
<ul>
<li>Syntax is strict — options must come <em>before</em> the key, separated by commas.</li>
<li>A single typo can prevent login.</li>
<li>Forced commands must use <strong>absolute paths</strong>.</li>
<li>Debug failures with <code>sshd -T</code> (shows effective config) and <code>ssh -vvv user@host</code>.</li>
</ul>
</li>
</ul>

  </div>
</details>

<hr>
<h3 id="other-os-specific-nuggets"><span style="color:#CC0000;">Other OS-Specific Nuggets</span></h3>
<p>Not every Unix-like does user management the same way. Here are a few platform-specific details that matter when you step outside Linux.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === macOS: FileVault &amp; SecureToken ===</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Check if a user has SecureToken (needed to unlock FileVault at boot)</span>
</span></span><span class="line"><span class="cl">sysadminctl -secureTokenStatus alice
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Grant FileVault unlock rights to an existing account</span>
</span></span><span class="line"><span class="cl">sudo fdesetup add -usertoadd alice
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># List all FileVault-enabled users</span>
</span></span><span class="line"><span class="cl">fdesetup list
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === BSD: doas instead of sudo ===</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Install and configure a simple allow rule for wheel group</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;permit :wheel&#34;</span> <span class="p">|</span> sudo tee /usr/local/etc/doas.conf
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Test it</span>
</span></span><span class="line"><span class="cl">doas whoami
</span></span><span class="line"><span class="cl"><span class="c1"># root</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Linux/systemd: user sessions ===</span>
</span></span><span class="line"><span class="cl"><span class="c1"># List all active systemd user sessions</span>
</span></span><span class="line"><span class="cl">loginctl list-users
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show details for a single user session</span>
</span></span><span class="line"><span class="cl">loginctl user-status alice</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>
<p><strong>macOS:</strong></p>
<ul>
<li><strong>SecureToken</strong> is a flag that determines whether a user can unlock FileVault at boot.</li>
<li>Creating a new admin user does <em>not</em> automatically give it SecureToken. You may need to explicitly grant it via another SecureToken user.</li>
<li><code>fdesetup</code> controls FileVault enrollment, token grants, and recovery keys.</li>
</ul>
</li>
<li>
<p><strong>BSD (<code>doas</code>):</strong></p>
<ul>
<li><code>doas</code> is OpenBSD’s minimalist alternative to <code>sudo</code>.</li>
<li>Config lives in <code>/etc/doas.conf</code> or <code>/usr/local/etc/doas.conf</code>.</li>
<li>Syntax is simple:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">permit :wheel
</span></span><span class="line"><span class="cl">permit nopass keepenv alice as root cmd /usr/bin/pkg_add
</span></span></code></pre></div></li>
<li>Security philosophy: fewer moving parts, less chance of misconfiguration.</li>
</ul>
</li>
<li>
<p><strong>Linux/systemd (<code>loginctl</code>):</strong></p>
<ul>
<li><code>loginctl</code> shows how users map to systemd sessions. Useful for debugging lingering sessions, <code>linger</code> (allowing services to keep running after logout), and session limits.</li>
<li>Example:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">loginctl enable-linger alice   <span class="c1"># keep user services running after logout</span>
</span></span></code></pre></div></li>
<li>Helpful when running background daemons or timers under a regular user account.</li>
</ul>
</li>
<li>
<p><strong>General tip:</strong></p>
<ul>
<li>When troubleshooting across OSes, always check “what’s the local identity backend?”
<ul>
<li>macOS → Directory Services (<code>dscl</code>, <code>sysadminctl</code>).</li>
<li>BSD → <code>pw</code> / <code>doas</code>.</li>
<li>Linux → <code>systemd-logind</code>, NSS, SSSD.</li>
</ul>
</li>
</ul>
</li>
</ul>

  </div>
</details>

<hr>
<figure style="text-align:center; margin: 1em auto;">
  <img src="loot.jpg" 
       alt="pixel art image of a party of fantasy adventurers happily gathered around a treasure chest" 
       style="display:block; margin:0 auto; width:min(100%, 400px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    If you made it this far you've earned some loot and XP!
  </figcaption>
</figure>
<h2 id="links--stuff">Links &amp; Stuff</h2>
<h3 id="core-resources">Core Resources</h3>
<ul>
<li><a href="https://man7.org/linux/man-pages/man5/passwd.5.html">man 5 passwd</a>, <a href="https://man7.org/linux/man-pages/man5/shadow.5.html">man 5 shadow</a>, <a href="https://man7.org/linux/man-pages/man5/group.5.html">man 5 group</a>, <a href="https://man7.org/linux/man-pages/man5/sudoers.5.html">man 5 sudoers</a></li>
<li><a href="https://wiki.archlinux.org/title/Users_and_groups">Linux User Management Guide</a> (Arch Wiki, applies broadly)</li>
<li><a href="https://docs.freebsd.org/en/books/handbook/basics/#users-and-basic-account-management">FreeBSD Handbook: Users and Basic Account Management</a></li>
</ul>
<h3 id="macos-specific-resources">macOS-Specific Resources</h3>
<ul>
<li><a href="https://support.apple.com/guide/mac-help/add-a-user-or-group-mchl3e281fc9/mac">Add a user or group on Mac - Apple Support</a></li>
<li><a href="https://support.apple.com/guide/mac-help/change-users-groups-settings-mtusr001/mac">Change Users &amp; Groups settings on Mac - Apple Support</a></li>
<li><a href="https://ss64.com/mac/dscl.html">dscl Man Page - SS64.com</a> — comprehensive dscl reference</li>
<li><a href="https://www.macos.utah.edu/documentation/authentication/dscl.html">dscl Examples - University of Utah</a> — practical dscl usage</li>
<li><a href="https://developer.apple.com/library/archive/documentation/Porting/Conceptual/PortingUnix/additionalfeatures/additionalfeatures.html">Additional Features - Apple Developer Archive</a> — includes user creation examples</li>
</ul>
<h3 id="history--lore">History &amp; Lore</h3>
<ul>
<li><a href="https://en.wikipedia.org/wiki/User_identifier">Why is root UID 0?</a></li>
<li><a href="https://idolinux.com/nobody-user-linux-unix/">The nobody user</a></li>
</ul>
<br>
<h2 id="conclusion">Conclusion</h2>
<p>Every file belongs to someone. Every process runs as someone.<br>
Unix enforces those boundaries with precision.</p>
<p>User and group management isn’t glamorous, but it’s foundational.<br>
Once you can inspect, create, modify, and delete accounts across platforms,<br>
you hold the keys to the kingdom.</p>
<p>Drop me a line if you found this guide useful or if I missed something:<br>
<a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
<p>Happy administering ✨</p>
]]></content:encoded>
    </item>
    <item>
      <title>Git Customization</title>
      <link>https://adminjitsu.com/posts/git-customization/</link>
      <pubDate>Thu, 18 Sep 2025 00:03:52 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/git-customization/</guid>
      <description>A practical guide to git config: persistence, safe defaults, aliases, and custom themes to make Git your own.</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<blockquote>
<p><em>&ldquo;A rolling stone gathers momentum.&rdquo;</em> &ndash; anonymous fortune</p></blockquote>
<p>Git is one of those tools that grows with you. At first, you’re just trying to <code>commit</code> without breaking things, then one day you realize Git has a <em>whole personality</em> you can tweak—aliases, safer defaults, and even custom color schemes.</p>
<p>This guide is the companion piece to my earlier post <strong><a href="/posts/git-fu/">Git-Fu</a></strong>. That one focused on daily commands and workflows. This one goes under the hood: <code>git config</code>, what lives in the config files, and how you can bend Git to your will.</p>
<h2 id="how-git-config-works">How <code>git config</code> Works</h2>
<p>Git&rsquo;s configuration is layered into three scopes:</p>
<ul>
<li><strong>System</strong>: applies to all users
<ul>
<li>Linux/Mac: <code>/etc/gitconfig</code></li>
<li>Windows: <code>C:\Program Files\Git\etc\gitconfig</code></li>
</ul>
</li>
<li><strong>Global</strong>: applies to you
<ul>
<li>Primary: <code>~/.gitconfig</code></li>
<li>Alternative: <code>~/.config/git/config</code> (XDG standard)</li>
</ul>
</li>
<li><strong>Local</strong>: applies to a single repo (<code>.git/config</code>)</li>
</ul>
<p>Git reads these in order, with more specific scopes overriding broader ones. So a local setting beats a global one, which beats a system one.</p>
<p>The safe way to set values is with the <code>git config</code> command, which writes to the correct file for you. For example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global user.name <span class="s2">&#34;Your Name&#34;</span>
</span></span><span class="line"><span class="cl">git config --global user.email <span class="s2">&#34;you@example.com&#34;</span>
</span></span></code></pre></div><p>Use <code>--system</code>, <code>--global</code>, or <code>--local</code> to control the scope. Handy flags include <code>--get</code>, <code>--unset</code>, and <code>--list</code>.</p>
<p>See everything Git knows about you right now with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --list --show-origin
</span></span></code></pre></div><hr>
<h2 id="where-git-config-lives-persistence-saving--resetting">Where <code>git config</code> Lives (Persistence, Saving &amp; Resetting)</h2>
<p>When you run <code>git config</code>, you’re not changing runtime state — you’re editing a config file. That’s why these tweaks persist across reboots and repos.</p>
<h3 id="saving-your-config">Saving Your Config</h3>
<p>Back up everything:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global --list &gt; my-gitconfig.txt
</span></span></code></pre></div><p>Or just a subset:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global --get-regexp <span class="s1">&#39;^alias\.&#39;</span> &gt; my-aliases.txt
</span></span></code></pre></div><h3 id="restoring-a-config">Restoring a Config</h3>
<p>Reapply from a dump:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">while</span> <span class="nb">read</span> key value<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  git config --global <span class="s2">&#34;</span><span class="nv">$key</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$value</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span> &lt; my-gitconfig.txt
</span></span></code></pre></div><p>Or just copy <code>~/.gitconfig</code> between machines.</p>
<h3 id="resetting-to-defaults">Resetting to Defaults</h3>
<p>Remove a section:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global --remove-section <span class="nb">alias</span>
</span></span></code></pre></div><p>Unset a single key:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global --unset core.editor
</span></span></code></pre></div><p>Full reset:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mv ~/.gitconfig<span class="o">{</span>,.bak<span class="o">}</span>   <span class="c1"># quick backup</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># or, if you’re feeling reckless</span>
</span></span><span class="line"><span class="cl">rm ~/.gitconfig
</span></span></code></pre></div><p>⚠️ <span class="hl yellow">Nukes everything—identity, aliases, colors, etc.</span><br>
Use the <code>mv</code> method unless you’re 100% sure. Back up first!</p>
<h3 id="takeaways">Takeaways</h3>
<ul>
<li><code>git config</code> is <strong>persistent</strong> because it writes to config files.</li>
<li>You can <strong>save and restore</strong> with simple dump/reapply steps.</li>
<li>Resetting can be surgical (<code>--unset</code>) or nuclear (<code>rm ~/.gitconfig</code>).</li>
</ul>
<p>Knowing this up front makes the cheatsheet more useful, because you’ll understand where the settings live and how to move them around.</p>
<br>
<figure style="text-align:center; margin: 1em auto;">
  <img src="ninja-slash.jpg" 
       alt="a video game style pixel art ninja slashing in a dramatic arc" 
       style="display:block; margin:0 auto; width:min(100%, 500px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Ninjas make every post better!
  </figcaption>
</figure>
<h2 id="cheatsheet-useful-git-config-commands">Cheatsheet: Useful Git Config Commands</h2>
<p>The examples below use <code>git config</code> commands, but you can also open your config file directly with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --edit
</span></span></code></pre></div><hr>
<h3 id="quick-start-5-safe-defaults">Quick Start: 5 Safe Defaults</h3>
<p>If you only set a handful of Git configs, make it these. They improve safety, keep repos tidy, and save you headaches later:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global user.name <span class="s2">&#34;Your Name&#34;</span>
</span></span><span class="line"><span class="cl">git config --global user.email <span class="s2">&#34;you@example.com&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">git config --global init.defaultBranch main   <span class="c1"># new repos start on &#39;main&#39;</span>
</span></span><span class="line"><span class="cl">git config --global push.default simple       <span class="c1"># safer pushes</span>
</span></span><span class="line"><span class="cl">git config --global fetch.prune <span class="nb">true</span>          <span class="c1"># auto-clean dead branches</span>
</span></span></code></pre></div><p>That’s it—your identity + three defaults. You can stop here and still have a nicer Git.<br>
When you’re ready, scroll down for more customization (aliases, pretty logs, colors, etc.).</p>
<hr>
<h3 id="identity">Identity</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global user.name <span class="s2">&#34;Your Name&#34;</span>
</span></span><span class="line"><span class="cl">git config --global user.email <span class="s2">&#34;you@example.com&#34;</span>
</span></span></code></pre></div><hr>
<h3 id="usability-defaults">Usability Defaults</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global init.defaultBranch main   <span class="c1"># new repos start with &#39;main&#39;</span>
</span></span><span class="line"><span class="cl">git config --global push.default simple       <span class="c1"># safer pushes</span>
</span></span><span class="line"><span class="cl">git config --global fetch.prune <span class="nb">true</span>          <span class="c1"># auto-clean old branches</span>
</span></span><span class="line"><span class="cl">git config --global pull.rebase <span class="nb">false</span>         <span class="c1"># or true if you’re a rebasing fan</span>
</span></span></code></pre></div><hr>
<h3 id="editor">Editor</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global core.editor <span class="s2">&#34;nano&#34;</span>  <span class="c1"># or vim, code --wait, etc.</span>
</span></span><span class="line"><span class="cl">git config --global diff.tool vimdiff     <span class="c1"># or your preferred diff tool</span>
</span></span><span class="line"><span class="cl">git config --global merge.tool vimdiff    <span class="c1"># or your preferred merge tool</span>
</span></span></code></pre></div><hr>
<h3 id="pretty-logs">Pretty Logs</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global format.pretty oneline
</span></span><span class="line"><span class="cl">git config --global log.abbrevCommit <span class="nb">true</span>
</span></span><span class="line"><span class="cl">git config --global log.date relative
</span></span></code></pre></div><hr>
<h3 id="safer-merges">Safer Merges</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global merge.ff only
</span></span><span class="line"><span class="cl">git config --global diff.mnemonicprefix <span class="nb">true</span>
</span></span><span class="line"><span class="cl">git config --global rerere.enabled <span class="nb">true</span>  <span class="c1"># reuse recorded resolutions</span>
</span></span></code></pre></div><hr>
<h3 id="aliases-muscle-memory-ftw">Aliases (muscle memory FTW)</h3>
<p>Aliases let you shorten Git commands into quick shorthands. For example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global alias.st status
</span></span></code></pre></div><p>Now instead of typing <code>git status</code>, you can just type:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git st
</span></span></code></pre></div><p>That’s all aliases do—they don’t add new features, but they save keystrokes and speed up common commands. Aliases can point to <strong>any Git verb</strong>, including ones with arguments. For example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global alias.lg <span class="s2">&#34;log --oneline --decorate --graph --all&#34;</span>
</span></span><span class="line"><span class="cl">git lg   <span class="c1"># expands to &#39;git log --oneline --decorate --graph --all&#39;</span>
</span></span></code></pre></div><p>And here’s the cool part: if your alias starts with <code>!</code>, Git treats it as a raw <strong>shell command</strong> instead of a Git subcommand. That means you can make Git run non-Git commands—or Git commands wrapped in shell logic:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global alias.today <span class="s1">&#39;!date&#39;</span>
</span></span><span class="line"><span class="cl">git today   <span class="c1"># runs your system &#39;date&#39; command</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">git config --global alias.root <span class="s1">&#39;!git rev-parse --show-toplevel&#39;</span>
</span></span><span class="line"><span class="cl">git root    <span class="c1"># prints the repo’s root directory</span>
</span></span></code></pre></div><p><span class="tag yellow">Note:</span><br>
Aliases that start with <code>!</code> run exactly as written in your shell. They don’t take extra arguments the way normal Git commands do—<code>git root sub/dir</code> won’t work. For more flexibility, write a shell script and call it from your alias.</p>
<p>Here are some popular everyday aliases you can copy straight in:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global alias.st status
</span></span><span class="line"><span class="cl">git config --global alias.co checkout
</span></span><span class="line"><span class="cl">git config --global alias.br branch
</span></span><span class="line"><span class="cl">git config --global alias.cm <span class="s1">&#39;commit -m&#39;</span>
</span></span><span class="line"><span class="cl">git config --global alias.lg <span class="s2">&#34;log --oneline --decorate --graph --all&#34;</span>
</span></span></code></pre></div><br>
<h2 id="-how-git-colors-work">🎨 How Git Colors Work</h2>
<p>Git doesn’t store colors itself—it just spits out ANSI escape codes. Your <strong>terminal</strong> decides what “yellow” or “cyan” actually looks like. That’s why the default yellow sometimes looks like <em>Dijon mustard</em> depending on your theme.</p>
<p>Git supports three kinds of values:</p>
<ul>
<li><strong>Named ANSI colors</strong>: <code>red</code>, <code>green</code>, <code>blue</code>, <code>cyan</code>, <code>yellow</code>, <code>magenta</code>, <code>white</code>, <code>black</code></li>
<li><strong>Attributes</strong>: <code>bold</code>, <code>ul</code> (underline), <code>reverse</code>, <code>blink</code></li>
<li><strong>Hex RGB</strong> (<code>#rrggbb</code>) since Git 2.16 — the most reliable way to get <em>exact</em> shades across terminals</li>
</ul>
<hr>
<h3 id="quick-test">Quick Test</h3>
<p>If you just want to see Git output in color, enable it globally:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global color.ui always
</span></span></code></pre></div><p>Now run <code>git status</code> or <code>git diff</code> — you’ll get Git’s built-in red/green/yellow scheme.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Test your new colors</span>
</span></span><span class="line"><span class="cl">git status
</span></span><span class="line"><span class="cl">git log --oneline --decorate -10
</span></span><span class="line"><span class="cl">git diff HEAD~1 HEAD
</span></span><span class="line"><span class="cl">git branch -a
</span></span><span class="line"><span class="cl">git grep <span class="s2">&#34;function&#34;</span> <span class="c1"># (if you have any code files)</span>
</span></span></code></pre></div><hr>
<figure style="text-align:center; margin: 1em auto;">
  <img src="dj-ninja.jpg" 
       alt="a video game style pixel art ninja dj using a pair of turntables in a club" 
       style="display:block; margin:0 auto; width:min(100%, 400px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
<br>
<h3 id="sample-theme-cool-dark">Sample Theme: &ldquo;Cool Dark&rdquo;</h3>
<p>Here’s a complete theme you can paste in to try out. It uses <strong>golden yellow</strong>, <strong>light cyan/blue</strong>, <strong>mint green</strong>, and <strong>bright grey</strong> accents chosen for a black terminal background.</p>
<p>Paste the whole block into your terminal (or save as a script). VS Code&rsquo;s inline color picker (which appears when you hover over hex colors like <code>#ffd75f</code>) makes customizing these colors especially easy.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global color.ui always
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Branches ──</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Affects &#39;git branch&#39; output and branch decorations in &#39;git log&#39;</span>
</span></span><span class="line"><span class="cl">git config --global color.branch.current <span class="s2">&#34;#ffd75f&#34;</span>   <span class="c1"># golden yellow (current branch)</span>
</span></span><span class="line"><span class="cl">git config --global color.branch.local   <span class="s2">&#34;#5fd7ff&#34;</span>   <span class="c1"># light cyan (local branches)</span>
</span></span><span class="line"><span class="cl">git config --global color.branch.remote  <span class="s2">&#34;#87afff&#34;</span>   <span class="c1"># soft sky blue (remotes)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Diffs ──</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Affects &#39;git diff&#39; output</span>
</span></span><span class="line"><span class="cl">git config --global color.diff.meta       <span class="s2">&#34;#ffd75f&#34;</span>  <span class="c1"># metadata (index info, etc.)</span>
</span></span><span class="line"><span class="cl">git config --global color.diff.frag       <span class="s2">&#34;#5fd7ff&#34;</span>  <span class="c1"># hunk headers</span>
</span></span><span class="line"><span class="cl">git config --global color.diff.old        <span class="s2">&#34;#ff5f5f&#34;</span>  <span class="c1"># red for removed lines</span>
</span></span><span class="line"><span class="cl">git config --global color.diff.new        <span class="s2">&#34;#87ffaf&#34;</span>  <span class="c1"># mint green for added lines</span>
</span></span><span class="line"><span class="cl">git config --global color.diff.whitespace <span class="s2">&#34;#ff5f87&#34;</span>  <span class="c1"># pink highlight for whitespace errors</span>
</span></span><span class="line"><span class="cl">git config --global color.diff.commit     <span class="s2">&#34;#5fd7ff&#34;</span>  <span class="c1"># commit hashes in &#39;git log --oneline&#39; (older Git)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Status ──</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Affects &#39;git status&#39; output</span>
</span></span><span class="line"><span class="cl">git config --global color.status.added     <span class="s2">&#34;#87ffaf&#34;</span> <span class="c1"># mint green (staged new files)</span>
</span></span><span class="line"><span class="cl">git config --global color.status.changed   <span class="s2">&#34;#ffd75f&#34;</span> <span class="c1"># yellow (modified files)</span>
</span></span><span class="line"><span class="cl">git config --global color.status.untracked <span class="s2">&#34;#5fd7ff&#34;</span> <span class="c1"># cyan (new untracked files)</span>
</span></span><span class="line"><span class="cl">git config --global color.status.branch    <span class="s2">&#34;#bcbcbc&#34;</span> <span class="c1"># grey (branch info line)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Grep ──</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Affects &#39;git grep&#39; output</span>
</span></span><span class="line"><span class="cl">git config --global color.grep.match      <span class="s2">&#34;#ffd75f&#34;</span>  <span class="c1"># yellow (matches)</span>
</span></span><span class="line"><span class="cl">git config --global color.grep.filename   <span class="s2">&#34;#5fd7ff&#34;</span>  <span class="c1"># cyan (filenames)</span>
</span></span><span class="line"><span class="cl">git config --global color.grep.linenumber <span class="s2">&#34;#bcbcbc&#34;</span>  <span class="c1"># grey (line numbers)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Log / Decorations ──</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Affects &#39;git log --decorate&#39; output</span>
</span></span><span class="line"><span class="cl">git config --global color.decorate.head         <span class="s2">&#34;#87ffaf&#34;</span> <span class="c1"># mint green (HEAD)</span>
</span></span><span class="line"><span class="cl">git config --global color.decorate.branch       <span class="s2">&#34;#5fd7ff&#34;</span> <span class="c1"># cyan (local branches)</span>
</span></span><span class="line"><span class="cl">git config --global color.decorate.remoteBranch <span class="s2">&#34;#ffd75f&#34;</span> <span class="c1"># yellow (remote branches)</span>
</span></span><span class="line"><span class="cl">git config --global color.decorate.tag          <span class="s2">&#34;#ff5f87&#34;</span> <span class="c1"># pink (tags)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Interactive ──</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Affects interactive commands (git rebase -i, git add -p)</span>
</span></span><span class="line"><span class="cl">git config --global color.interactive.prompt <span class="s2">&#34;#ffd75f&#34;</span> <span class="c1"># yellow (prompts)</span>
</span></span><span class="line"><span class="cl">git config --global color.interactive.header <span class="s2">&#34;#87afff&#34;</span> <span class="c1"># blue (section headers)</span>
</span></span><span class="line"><span class="cl">git config --global color.interactive.help   <span class="s2">&#34;#bcbcbc&#34;</span> <span class="c1"># grey (help text)</span>
</span></span><span class="line"><span class="cl">git config --global color.interactive.error  <span class="s2">&#34;#ff5f5f&#34;</span> <span class="c1"># red (errors)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Pager highlight ──</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Affects search matches inside &#39;less&#39;</span>
</span></span><span class="line"><span class="cl">git config --global color.pager.highlight <span class="s2">&#34;#87afff&#34;</span>   <span class="c1"># soft blue</span>
</span></span></code></pre></div><details >
  <summary>Details</summary>
  <div class="collapse-body">
    <p>This is what the exact same theme looks like if you edit your <code>~/.gitconfig</code> directly instead of running commands:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[color]</span>
</span></span><span class="line"><span class="cl">    <span class="na">ui</span> <span class="o">=</span> <span class="s">always</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[color &#34;branch&#34;]</span>
</span></span><span class="line"><span class="cl">    <span class="na">current</span> <span class="o">=</span> <span class="s">#ffd75f
</span></span></span><span class="line"><span class="cl"><span class="s">    local   = #5fd7ff
</span></span></span><span class="line"><span class="cl"><span class="s">    remote  = #87afff</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[color &#34;diff&#34;]</span>
</span></span><span class="line"><span class="cl">    <span class="na">meta</span>       <span class="o">=</span> <span class="s">#ffd75f
</span></span></span><span class="line"><span class="cl"><span class="s">    frag       = #5fd7ff
</span></span></span><span class="line"><span class="cl"><span class="s">    old        = #ff5f5f
</span></span></span><span class="line"><span class="cl"><span class="s">    new        = #87ffaf
</span></span></span><span class="line"><span class="cl"><span class="s">    whitespace = #ff5f87
</span></span></span><span class="line"><span class="cl"><span class="s">    commit     = #5fd7ff   # commit hashes (older Git)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[color &#34;status&#34;]</span>
</span></span><span class="line"><span class="cl">    <span class="na">added</span>     <span class="o">=</span> <span class="s">#87ffaf
</span></span></span><span class="line"><span class="cl"><span class="s">    changed   = #ffd75f
</span></span></span><span class="line"><span class="cl"><span class="s">    untracked = #5fd7ff
</span></span></span><span class="line"><span class="cl"><span class="s">    branch    = #bcbcbc</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[color &#34;grep&#34;]</span>
</span></span><span class="line"><span class="cl">    <span class="na">match</span>      <span class="o">=</span> <span class="s">#ffd75f
</span></span></span><span class="line"><span class="cl"><span class="s">    filename   = #5fd7ff
</span></span></span><span class="line"><span class="cl"><span class="s">    linenumber = #bcbcbc</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[color &#34;decorate&#34;]</span>
</span></span><span class="line"><span class="cl">    <span class="na">head</span>         <span class="o">=</span> <span class="s">#87ffaf
</span></span></span><span class="line"><span class="cl"><span class="s">    branch       = #5fd7ff
</span></span></span><span class="line"><span class="cl"><span class="s">    remoteBranch = #ffd75f
</span></span></span><span class="line"><span class="cl"><span class="s">    tag          = #ff5f87</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[color &#34;interactive&#34;]</span>
</span></span><span class="line"><span class="cl">    <span class="na">prompt</span> <span class="o">=</span> <span class="s">#ffd75f
</span></span></span><span class="line"><span class="cl"><span class="s">    header = #87afff
</span></span></span><span class="line"><span class="cl"><span class="s">    help   = #bcbcbc
</span></span></span><span class="line"><span class="cl"><span class="s">    error  = #ff5f5f</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[color &#34;pager&#34;]</span>
</span></span><span class="line"><span class="cl">    <span class="na">highlight</span> <span class="o">=</span> <span class="s">#87afff</span></span></span></code></pre></div>
  </div>
</details>

<figure style="text-align:center; margin: 1em auto;">
  <img src="git-cooldark.jpg" 
       alt="screenshot of git gcheck function output with cooldark theme applied" 
       style="display:block; margin:0 auto; width:min(100%, 8000px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    It's nice to finally override the ugly default colors
  </figcaption>
</figure>
<hr>
<h3 id="resetting-to-defaults-1">Resetting to Defaults</h3>
<p>If you want Git’s classic red/green/yellow back, just clear your overrides:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global --remove-section color.branch   2&gt;/dev/null
</span></span><span class="line"><span class="cl">git config --global --remove-section color.diff     2&gt;/dev/null
</span></span><span class="line"><span class="cl">git config --global --remove-section color.status   2&gt;/dev/null
</span></span><span class="line"><span class="cl">git config --global --remove-section color.interactive 2&gt;/dev/null
</span></span><span class="line"><span class="cl">git config --global --remove-section color.grep     2&gt;/dev/null
</span></span><span class="line"><span class="cl">git config --global --remove-section color.pager    2&gt;/dev/null
</span></span><span class="line"><span class="cl">git config --global --unset color.ui                2&gt;/dev/null
</span></span></code></pre></div><br>
<h2 id="-further-reading">📚 Further Reading</h2>
<ul>
<li><a href="https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration">Git Book: Customizing Git</a></li>
<li><a href="https://git-scm.com/docs/git-config">Git Documentation: <code>git-config</code></a></li>
<li><a href="https://en.wikipedia.org/wiki/ANSI_escape_code#Colors">ANSI color codes explained</a></li>
</ul>
<br>
<h2 id="conclusion">Conclusion</h2>
<figure style="text-align:center; margin: 1em auto;">
  <img src="tsuba-2.png" 
       alt="a pixel art tsuba from a katana. decorative" 
       style="display:block; margin:0 auto; width:min(100%, 300px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
<p>Git config is where you make Git <em>yours</em>. Start with the essentials—your name, safe defaults, a couple of aliases—and once you’re comfortable, branch into aesthetics. A splash of cyan and a better yellow can turn Git from hostile text walls into something scannable at a glance.</p>
<p>This article is a companion to <strong><a href="/posts/git-fu/">Git-Fu</a></strong>.</p>
<p>If you’ve got tips, tricks, or a cool theme of your own, I’d love to hear about it: <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
<p>Happy committing ✨</p>
]]></content:encoded>
    </item>
    <item>
      <title>Git-Fu</title>
      <link>https://adminjitsu.com/posts/git-fu/</link>
      <pubDate>Mon, 15 Sep 2025 04:09:11 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/git-fu/</guid>
      <description>Git doesn’t have to feel cryptic. This guide breaks it down with clear metaphors, a structured cheatsheet, and real-world helpers like the `gcheck` function and handy aliases. From staging and commits to branching, merging, and undoing mistakes, learn how to navigate Git with confidence.</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<p> Git is one of those tools that sits quietly at the center of nearly everything we build — from solo side projects to giant open-source collaborations. It keeps track of changes, lets us experiment without fear, and makes it possible to sync work across machines and teams. Without it, keeping track of changes in complex projects would be chaos.</p>
<p>But Git can sometimes feel cryptic with numerous commands, strange terminology, and the occasional <em>“oh no, what did I just do?”</em> moment. The truth is, you don’t need to know every dark corner of Git to use it well. With a few key concepts, a solid cheatsheet, and some helper scripts, Git becomes a trusted and powerful tool.</p>
<p>In this guide I’ll share:</p>
<ul>
<li>A <strong>conceptual crash course</strong> to demystify Git</li>
<li>A <strong>task-based cheatsheet</strong> with collapsible sections for quick reference</li>
<li>My <code>gcheck</code> function — a Bash helper that gives a friendly status report on any repo</li>
<li>Handy aliases I rely on daily</li>
<li>Links and resources for when you want to go deeper</li>
</ul>
<h2 id="what-is-git">What is Git?</h2>
<p>At its heart, Git is a <strong>time machine for text files</strong>.<br>
It lets you move backward and forward through the history of a project, explore alternate timelines, and share those timelines with others.</p>
<ul>
<li>
<p><strong>Commits are snapshots.</strong><br>
Each commit is like a photograph of your project at a moment in time.<br>
It doesn’t just save what changed, but how the whole directory looked — so you can always step back to that state.<br>
Specifically, Git stores full snapshots (not just diffs) and shows diffs only when you compare snapshots.</p>
</li>
<li>
<p><strong>Branches are pointers.</strong><br>
A branch isn’t a copy of your work — it’s just a movable label pointing to a commit.<br>
This makes branching lightweight and cheap, so you can spin off experiments freely without bloating your repo.<br>
The <code>HEAD</code> is a pointer too—it tells Git where you&rsquo;re currently standing.
Switching branches just moves <code>HEAD</code>—your current position—to a different branch reference.</p>
</li>
<li>
<p><strong>Merging is weaving timelines.</strong><br>
When two branches diverge, merging pulls their histories together into a single story.<br>
Sometimes this is automatic; other times you have to resolve conflicts where the timelines disagree.<br>
Sometimes Git can simply fast-forward a branch if there&rsquo;s no divergence—no new merge commit needed.</p>
</li>
<li>
<p><strong>Rebasing is replaying commits.</strong><br>
Instead of weaving two histories together, rebasing lifts your commits and replays them onto a different base.<br>
The result is a cleaner-looking history — like rewriting your diary so events flow in a straight line.</p>
</li>
<li>
<p><strong>Remotes are copies of the archive.</strong><br>
A remote (like GitHub, GitLab, or Gitea) is just another place your repo lives.<br>
You can fetch from it, push to it, or clone it elsewhere. Remotes make collaboration and backup possible.</p>
</li>
</ul>
<p>Together, these metaphors paint Git not as a mess of arcane commands, but as a system for <strong>time travel, parallel universes, and shared archives</strong>.</p>
<p>Think of it as a directed graph of changes where you always have a way back to safety</p>
<h2 id="key-concepts">Key Concepts</h2>
<figure style="text-align:center; margin: 1em auto;">
  <img src="sensei.jpg" 
       alt="a video game style pixel art ninja and his master sitting in a dojo" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Mastering Git just takes time and practice
  </figcaption>
</figure>
<p>Now that we’ve framed Git in simple metaphors, here’s the more technical version — the details behind the magic.</p>
<ul>
<li>
<p><strong>Repos: local vs remote</strong><br>
A local repo is your working copy with a <code>.git/</code> folder inside it.<br>
A remote is just another full copy of the repo, usually hosted on a server (GitHub, GitLab, Gitea).<br>
By default, the first remote is called <code>origin</code>. You can rename it, or add others (<code>upstream</code>, <code>backup</code>, etc).</p>
</li>
<li>
<p><strong>Staging area</strong><br>
Also called the <em>index</em>. This is where you prepare changes before committing. Think of it as a shopping cart or draft tray: you can stage some edits, leave others unstaged, and commit only what you want.</p>
</li>
<li>
<p><strong>Commits</strong><br>
Immutable snapshots with parent pointers. Each has a SHA-1 ID (SHA-256 is becoming standard), author info, and message. Together they form a graph (specifically a <a href="https://en.wikipedia.org/wiki/Directed_acyclic_graph">🔗directed acyclic graph or DAG</a> ) of your project history.</p>
</li>
<li>
<p><strong>Branches</strong><br>
Lightweight movable labels pointing at commits. Switching branches just moves the <code>HEAD</code> pointer to a different branch reference.</p>
</li>
<li>
<p><strong>Rebase vs Merge</strong></p>
<ul>
<li><em>Merge</em> combines two histories into one, preserving all bumps.</li>
<li><em>Rebase</em> rewrites your commits so they look like they happened on top of another branch, creating a cleaner line of history.</li>
</ul>
</li>
<li>
<p><strong>Upstream/Tracking branches</strong><br>
When a local branch “tracks” a remote one, Git knows where to pull and push by default.</p>
</li>
</ul>
<h2 id="the-local-git-repo">The Local Git Repo</h2>
<p>When you <code>git init</code> in a folder, Git creates a hidden directory called <code>.git/</code>.<br>
That directory <em>is</em> the repository — everything else is just your working files.</p>
<p>This is what makes Git powerful and portable: if you copy a project folder with its <code>.git/</code> intact, you’ve copied the entire history, branches, tags, and settings. You don’t need a server, a database, or any special tooling.</p>
<h3 id="anatomy-of-git-simplified">Anatomy of <code>.git/</code> (simplified)</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">└─$ tree -L <span class="m">1</span> .git/
</span></span><span class="line"><span class="cl">.git/
</span></span><span class="line"><span class="cl">├── COMMIT_EDITMSG
</span></span><span class="line"><span class="cl">├── FETCH_HEAD
</span></span><span class="line"><span class="cl">├── HEAD
</span></span><span class="line"><span class="cl">├── ORIG_HEAD
</span></span><span class="line"><span class="cl">├── branches
</span></span><span class="line"><span class="cl">├── config
</span></span><span class="line"><span class="cl">├── description
</span></span><span class="line"><span class="cl">├── hooks
</span></span><span class="line"><span class="cl">├── index
</span></span><span class="line"><span class="cl">├── info
</span></span><span class="line"><span class="cl">├── logs
</span></span><span class="line"><span class="cl">├── objects
</span></span><span class="line"><span class="cl">├── packed-refs
</span></span><span class="line"><span class="cl">└── refs
</span></span></code></pre></div><ul>
<li><strong>HEAD</strong> → a text file that tells Git which branch you’re currently on.</li>
<li><strong>config</strong> → local repo configuration (remotes, branch tracking, custom settings).</li>
<li><strong>objects/</strong> → the actual data store. Commits, trees, and blobs (file contents) live here, addressed by their SHA-1 hash.</li>
<li><strong>refs/</strong> → pointers to commits: branches, tags, remotes.</li>
<li><strong>logs/</strong> → the <em>reflog</em>, Git’s “black box recorder” of branch movements.</li>
<li><strong>index</strong> → the staging area, stored as a binary file.</li>
<li><strong>hooks/</strong> → scripts you can run automatically on events (commits, pushes, merges).</li>
</ul>
<p>Most of the time you never touch these directly, but it’s useful to know they exist.<br>
Git isn’t a black box — it’s just a folder full of plain-text files and hashed objects. (Peek at <code>COMMIT_EDITMSG</code> if you ever want to see the last commit message you wrote.)</p>
<h3 id="why-it-matters">Why it matters</h3>
<ul>
<li><strong>Portability</strong> → copy the folder to a USB stick, another machine, or zip it up, and you’ve moved the repo with its entire history.</li>
<li><strong>Independence</strong> → no central server required; you always have a complete copy locally.</li>
<li><strong>Transparency</strong> → advanced users can inspect <code>.git/config</code> or <code>.git/refs/heads/</code> to see what’s really happening under the hood.</li>
</ul>
<figure style="text-align:center; margin: 1em auto;">
  <img src="commit-ninja.jpg" 
       alt="a video game style pixel art ninja holding a scroll bearing the word Commit" 
       style="display:block; margin:0 auto; width:min(100%, 400px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
<br>
<h2 id="cheatsheet">Cheatsheet</h2>
<p>Git has a lot of commands so I like to group them into related tasks to make it easier to find what you need based on what you&rsquo;re trying to do.</p>
<h3 id="getting-started-init--remotes"><span style="color:#CC0000;">Getting Started: Init &amp; Remotes</span></h3>
<p>Every Git journey starts by either <strong>creating a new repo</strong> or <strong>cloning an existing one</strong>.<br>
Then, if you want to sync with a remote (GitHub, GitLab, Gitea), you connect it.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Create a new repo ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Initialize a repo in the current folder</span>
</span></span><span class="line"><span class="cl">git init
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Clone an existing repo</span>
</span></span><span class="line"><span class="cl">git clone https://github.com/user/project.git
</span></span><span class="line"><span class="cl">git clone git@github.com:user/project.git   <span class="c1"># SSH form</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Check current remotes ===</span>
</span></span><span class="line"><span class="cl">git remote -v
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Add a new remote (usually &#39;origin&#39;)</span>
</span></span><span class="line"><span class="cl">git remote add origin https://github.com/user/project.git
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Change the remote URL (e.g., switch to SSH)</span>
</span></span><span class="line"><span class="cl">git remote set-url origin git@github.com:user/project.git
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Remove a remote</span>
</span></span><span class="line"><span class="cl">git remote remove origin
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === First push ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Push the current branch to &#39;origin&#39; and set it as upstream</span>
</span></span><span class="line"><span class="cl">git push -u origin main
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># After upstream is set, future pushes are simpler:</span>
</span></span><span class="line"><span class="cl">git push
</span></span><span class="line"><span class="cl">git pull</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li><code>git init</code> creates a <code>.git/</code> folder; that’s the repo.</li>
<li>HTTPS is simpler to start with, but SSH keys are better long-term (no passwords every push).</li>
<li>The <code>-u</code> flag on first push links your branch to the remote, so you can just <code>git push</code> / <code>git pull</code> afterward.</li>
<li>You can have multiple remotes (e.g., <code>origin</code> for GitHub, <code>backup</code> for Gitea).</li>
</ul>
  </div>
</details>

<hr>
<h3 id="daily-workflow-add-commit-push-pull"><span style="color:#CC0000;">Daily Workflow: Add, Commit, Push, Pull</span></h3>
<p>Most of the time you’re just making changes, saving them, and syncing with a remote.<br>
These are the <strong>everyday Git commands</strong> you’ll run dozens of times a week.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Checking status ===</span>
</span></span><span class="line"><span class="cl">git status          <span class="c1"># See which files changed, staged, or untracked</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Staging changes ===</span>
</span></span><span class="line"><span class="cl">git add file.txt    <span class="c1"># Stage one file</span>
</span></span><span class="line"><span class="cl">git add -A          <span class="c1"># Stage all changes (tracked + untracked)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Committing ===</span>
</span></span><span class="line"><span class="cl">git commit -m <span class="s2">&#34;Message here&#34;</span>   <span class="c1"># Save staged changes with a message</span>
</span></span><span class="line"><span class="cl">git commit --amend             <span class="c1"># Fix last commit (message or staged content)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Pushing to remote ===</span>
</span></span><span class="line"><span class="cl">git push            <span class="c1"># Send local commits to upstream</span>
</span></span><span class="line"><span class="cl">git push origin main  <span class="c1"># Explicit remote + branch</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Pulling updates ===</span>
</span></span><span class="line"><span class="cl">git pull            <span class="c1"># Fetch + merge changes from upstream</span>
</span></span><span class="line"><span class="cl">git pull --rebase   <span class="c1"># Fetch + replay your commits on top (cleaner history)</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li><code>git status</code> is your compass — run it constantly.</li>
<li><code>git add</code> = “move to staging area” → nothing is committed until you do.</li>
<li><code>git commit --amend</code> is safe <em>if you haven’t pushed yet</em>.</li>
<li>The first <code>git push</code> usually needs <code>-u origin main</code> to set upstream (covered in <em>Init &amp; Remotes</em>).</li>
<li>Prefer <code>git pull --rebase</code> to avoid “merge commits” from trivial updates.</li>
</ul>
  </div>
</details>

<hr>
<h3 id="branching--merging"><span style="color:#CC0000;">Branching &amp; Merging</span></h3>
<p>Branches let you explore ideas without messing up your main line of work.<br>
Merging and rebasing are how you bring those timelines back together.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Working with branches ===</span>
</span></span><span class="line"><span class="cl">git branch               <span class="c1"># List local branches</span>
</span></span><span class="line"><span class="cl">git branch -r            <span class="c1"># List remote branches</span>
</span></span><span class="line"><span class="cl">git branch new-feature   <span class="c1"># Create a new branch (stays on current)</span>
</span></span><span class="line"><span class="cl">git checkout new-feature <span class="c1"># Switch to a branch (old syntax)</span>
</span></span><span class="line"><span class="cl">git switch new-feature   <span class="c1"># Switch to a branch (new syntax)</span>
</span></span><span class="line"><span class="cl">git switch -c bugfix     <span class="c1"># Create + switch in one step</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Merging ===</span>
</span></span><span class="line"><span class="cl">git checkout main
</span></span><span class="line"><span class="cl">git merge new-feature    <span class="c1"># Merge branch into main</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Merge conflicts: Git marks conflicts in files</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Edit manually, then:</span>
</span></span><span class="line"><span class="cl">git add conflicted-file
</span></span><span class="line"><span class="cl">git commit               <span class="c1"># Finish the merge</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Rebasing ===</span>
</span></span><span class="line"><span class="cl">git checkout new-feature
</span></span><span class="line"><span class="cl">git rebase main          <span class="c1"># Replay commits on top of main</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># If conflicts happen during rebase:</span>
</span></span><span class="line"><span class="cl">git status               <span class="c1"># See what’s wrong</span>
</span></span><span class="line"><span class="cl">git add fixed-file
</span></span><span class="line"><span class="cl">git rebase --continue    <span class="c1"># Resume rebase</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Abort if it goes sideways</span>
</span></span><span class="line"><span class="cl">git merge --abort        <span class="c1"># during a merge</span>
</span></span><span class="line"><span class="cl">git rebase --abort       <span class="c1"># during a rebase</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>Use <code>git branch -a</code> to see local + remote branches together.</li>
<li><code>git switch</code> is the modern command; <code>git checkout</code> still works.</li>
<li>Merges preserve full history (good for collaboration).</li>
<li>Rebases rewrite history (good for keeping it clean, but don’t rebase shared branches).</li>
<li>If things go wrong, <code>git status</code> and <code>git reflog</code> are your lifelines.</li>
</ul>
  </div>
</details>

<hr>
<h3 id="undo--rescue"><span style="color:#CC0000;">Undo &amp; Rescue</span></h3>
<p>Everyone makes mistakes in Git — and that’s fine.<br>
Git has a <strong>time machine and a safety net</strong> built in. These commands help you undo changes, roll back commits, or recover “lost” history.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Unstaging changes ===</span>
</span></span><span class="line"><span class="cl">git restore --staged file.txt    <span class="c1"># Unstage a file (leave working copy alone)</span>
</span></span><span class="line"><span class="cl">git reset HEAD file.txt          <span class="c1"># Older syntax, same effect</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Discarding local changes ===</span>
</span></span><span class="line"><span class="cl">git restore file.txt             <span class="c1"># Throw away unstaged changes</span>
</span></span><span class="line"><span class="cl">git checkout -- file.txt         <span class="c1"># Old form, still works</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Undoing commits ===</span>
</span></span><span class="line"><span class="cl">git commit --amend               <span class="c1"># Edit last commit (message or staged content)</span>
</span></span><span class="line"><span class="cl">git reset --soft HEAD~1          <span class="c1"># Undo last commit, keep changes staged</span>
</span></span><span class="line"><span class="cl">git reset --mixed HEAD~1         <span class="c1"># Undo last commit, keep changes unstaged</span>
</span></span><span class="line"><span class="cl">git reset --hard HEAD~1          <span class="c1"># Rewind + throw away changes (⚠️ destructive)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Reverting commits safely ===</span>
</span></span><span class="line"><span class="cl">git revert &lt;commit&gt;              <span class="c1"># Create a new commit that undoes the given one</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Reflog (magic undo history) ===</span>
</span></span><span class="line"><span class="cl">git reflog                       <span class="c1"># Show where HEAD has been (local movements)</span>
</span></span><span class="line"><span class="cl">git checkout &lt;commit-hash&gt;       <span class="c1"># Recover a commit from reflog</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Aborting operations ===</span>
</span></span><span class="line"><span class="cl">git merge --abort                <span class="c1"># Cancel a merge in progress</span>
</span></span><span class="line"><span class="cl">git rebase --abort               <span class="c1"># Cancel a rebase in progress</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>Use <code>git restore</code> for day-to-day “oops” moments (unstaging or discarding edits).</li>
<li><code>git reset</code> is more powerful — soft/mixed/hard decide what happens to your changes.</li>
<li><code>git revert</code> is safer in shared branches because it adds a new commit instead of rewriting history.</li>
<li><code>git reflog</code> is the secret weapon — even after resets or rebases, you can usually recover.</li>
<li>When in doubt: stop, run <code>git status</code>, then check the reflog before panicking.</li>
</ul>
  </div>
</details>

<hr>
<h3 id="remotes--sync"><span style="color:#CC0000;">Remotes &amp; Sync</span></h3>
<p>Remotes are just other copies of your repo (GitHub, GitLab, Gitea, etc.).<br>
You fetch changes from them, push your commits to them, and pull to stay in sync.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Checking remotes ===</span>
</span></span><span class="line"><span class="cl">git remote -v                   <span class="c1"># Show remotes and their URLs</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Add a remote (commonly &#39;origin&#39;)</span>
</span></span><span class="line"><span class="cl">git remote add origin https://github.com/user/project.git
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Change URL (switch HTTPS ↔ SSH)</span>
</span></span><span class="line"><span class="cl">git remote set-url origin git@github.com:user/project.git
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Fetching &amp; pulling ===</span>
</span></span><span class="line"><span class="cl">git fetch                       <span class="c1"># Download commits/branches from remote (no merge)</span>
</span></span><span class="line"><span class="cl">git pull                        <span class="c1"># Fetch + merge remote changes into current branch</span>
</span></span><span class="line"><span class="cl">git pull --rebase               <span class="c1"># Fetch + replay your commits on top (cleaner history)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Pushing ===</span>
</span></span><span class="line"><span class="cl">git push                        <span class="c1"># Push current branch to its upstream</span>
</span></span><span class="line"><span class="cl">git push -u origin main         <span class="c1"># First push: set upstream branch</span>
</span></span><span class="line"><span class="cl">git push origin feature         <span class="c1"># Push a different branch by name</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Push all tags</span>
</span></span><span class="line"><span class="cl">git push --tags
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Delete a remote branch</span>
</span></span><span class="line"><span class="cl">git push origin --delete old-branch</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li><code>git fetch</code> is safe — it just updates your local view of the remote without touching your files.</li>
<li>Use <code>git pull --rebase</code> to keep history clean (avoids “merge commit” clutter).</li>
<li>The <code>-u</code> flag sets tracking so you can just run <code>git push</code> / <code>git pull</code> afterward.</li>
<li>Remotes are just names: you can have multiple (<code>origin</code>, <code>backup</code>, <code>upstream</code>).</li>
</ul>
  </div>
</details>

<hr>
<h3 id="tags--releases"><span style="color:#CC0000;">Tags &amp; Releases</span></h3>
<p>Tags mark important points in history — often used for releases (<code>v1.0</code>, <code>v2.1.3</code>).<br>
Unlike branches, they don’t move: they’re permanent labels on commits.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Creating tags ===</span>
</span></span><span class="line"><span class="cl">git tag v1.0.0                 <span class="c1"># Lightweight tag on latest commit</span>
</span></span><span class="line"><span class="cl">git tag -a v1.0.0 -m <span class="s2">&#34;Release&#34;</span> <span class="c1"># Annotated tag (with message, recommended)</span>
</span></span><span class="line"><span class="cl">git tag v1.0.0 &lt;commit&gt;        <span class="c1"># Tag a specific commit</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Listing &amp; inspecting ===</span>
</span></span><span class="line"><span class="cl">git tag                        <span class="c1"># List all tags</span>
</span></span><span class="line"><span class="cl">git show v1.0.0                <span class="c1"># Show commit + info behind a tag</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Pushing tags ===</span>
</span></span><span class="line"><span class="cl">git push origin v1.0.0         <span class="c1"># Push one tag</span>
</span></span><span class="line"><span class="cl">git push --tags                <span class="c1"># Push all tags</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Deleting tags ===</span>
</span></span><span class="line"><span class="cl">git tag -d v1.0.0              <span class="c1"># Delete local tag</span>
</span></span><span class="line"><span class="cl">git push origin --delete v1.0.0 <span class="c1"># Delete remote tag</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>Prefer annotated tags (<code>-a</code>) — they carry messages and metadata.</li>
<li>Tags are great for marking release points or milestones.</li>
<li>GitHub/GitLab can auto-generate releases when you push a tag.</li>
</ul>
  </div>
</details>

<hr>
<h3 id="cleanup--maintenance"><span style="color:#CC0000;">Cleanup &amp; Maintenance</span></h3>
<p>Over time, branches pile up, and Git’s object database grows.<br>
These commands keep your repo tidy and lean.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Branch cleanup ===</span>
</span></span><span class="line"><span class="cl">git branch -d feature          <span class="c1"># Delete local branch (safe, only if merged)</span>
</span></span><span class="line"><span class="cl">git branch -D feature          <span class="c1"># Force delete local branch</span>
</span></span><span class="line"><span class="cl">git push origin --delete old   <span class="c1"># Delete remote branch</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Pruning ===</span>
</span></span><span class="line"><span class="cl">git fetch --prune              <span class="c1"># Remove local refs to deleted remote branches</span>
</span></span><span class="line"><span class="cl">git remote prune origin        <span class="c1"># Same effect, manual trigger</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Garbage collection ===</span>
</span></span><span class="line"><span class="cl">git gc                         <span class="c1"># Cleanup loose objects &amp; optimize repo</span>
</span></span><span class="line"><span class="cl">git gc --prune<span class="o">=</span>now             <span class="c1"># Aggressive cleanup (⚠️ careful)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Checking size ===</span>
</span></span><span class="line"><span class="cl">git count-objects -vH          <span class="c1"># Show object counts + repo size</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>Always check <code>git branch -a</code> before deleting — make sure the branch isn’t needed.</li>
<li><code>git fetch --prune</code> is safe to run regularly (it just cleans stale refs).</li>
<li><code>git gc</code> runs automatically sometimes, but you can run it manually if a repo feels sluggish.</li>
<li>For giant repos, consider tools like <a href="https://rtyley.github.io/bfg-repo-cleaner/">BFG Repo-Cleaner</a> to remove large files from history.</li>
</ul>
  </div>
</details>

<br>
<figure style="text-align:center; margin: 1em auto;">
  <img src="bamboo.jpg" 
       alt="a pixel art ninja dashing through a bamboo forest in a dramatic action pose" 
       style="display:block; margin:0 auto; width:min(100%, 500px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
<h2 id="tricky-areas-demystified">Tricky Areas Demystified</h2>
<p>Some parts of Git trip people up again and again. Here are a few quick clarifications that save a lot of head-scratching.</p>
<h3 id="merge-vs-rebase">Merge vs Rebase</h3>
<p>Both are ways of bringing one branch’s changes into another, but they tell different stories:</p>
<ul>
<li>
<p><strong>Merge</strong> → preserves history. Two timelines come together, and the graph shows the divergence + merge point.</p>
<ul>
<li>Good for shared branches (everyone sees the real history).</li>
<li>Downside: history can look “messy” with lots of merge commits.</li>
</ul>
</li>
<li>
<p><strong>Rebase</strong> → rewrites history. Your commits are replayed as if they happened after the other branch.</p>
<ul>
<li>Good for private branches you haven’t shared yet (keeps history linear).</li>
<li>Downside: <span class="hl orange">don’t rebase</span> commits you’ve already pushed/shared — it confuses collaborators.</li>
</ul>
</li>
</ul>
<p>💡 <em>Rule of thumb:</em> Merge when collaborating, rebase when cleaning up your own local work.</p>
<hr>
<h3 id="how-git-diff-works">How Git Diff Works</h3>
<p>Diffs compare <strong>snapshots</strong>, not files directly. That’s why you can run diffs between:</p>
<ul>
<li><strong>Working directory vs staging</strong> → <code>git diff</code></li>
<li><strong>Staging vs last commit</strong> → <code>git diff --cached</code></li>
<li><strong>Any two commits</strong> → <code>git diff &lt;commit1&gt; &lt;commit2&gt;</code></li>
<li><strong>Current branch vs remote</strong> → <code>git diff origin/main</code></li>
</ul>
<p>This flexibility comes from Git’s DAG of commits. A “diff” is just “what would I need to apply to one snapshot to make it look like another?”</p>
<hr>
<h3 id="detached-head">Detached HEAD</h3>
<p>When you check out a commit by hash (not a branch), <code>HEAD</code> points directly at that commit.<br>
You can poke around safely, but new commits won’t belong to any branch unless you explicitly create one.</p>
<ul>
<li>Checking out a commit: <code>git checkout abc123</code></li>
<li>Getting back to a branch: <code>git switch main</code></li>
<li>Saving your detached work: <code>git switch -c experiment</code></li>
</ul>
<hr>
<h3 id="the-safety-net-reflog">The Safety Net: Reflog</h3>
<p>Every time <code>HEAD</code> moves, Git logs it in <code>.git/logs/HEAD</code>.<br>
That means even after a reset, rebase, or branch delete, you can usually recover.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git reflog              <span class="c1"># Show recent HEAD movements</span>
</span></span><span class="line"><span class="cl">git checkout &lt;hash&gt;     <span class="c1"># Jump back to a lost commit</span>
</span></span></code></pre></div><p>Think of it as Git’s <strong>black box recorder</strong>. When all else fails, check the reflog.</p>
<br>
<h2 id="gcheck-a-friendly-git-status-report">gcheck: A Friendly Git Status Report</h2>
<p>Over time I found myself running the same few Git commands again and again:</p>
<ul>
<li><code>git status</code> to see what’s staged</li>
<li><code>git fetch</code> to check for updates</li>
<li><code>git log</code> or <code>git diff</code> to compare with upstream</li>
</ul>
<p>That’s a lot of typing just to answer the question: <em>“What’s the state of this repo?”</em></p>
<p>So I wrote a little Bash function called <code>gcheck</code>. It gives you a clear, human-readable summary of your current repository, including whether you’re ahead or behind your remote branch. I’ve been using it daily for months now and it’s become second nature.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">gcheck<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> ! git rev-parse --show-toplevel <span class="p">&amp;</span>&gt;/dev/null<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;🚫 Not inside a Git repository.&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;🔍 Checking Git repo status for: </span><span class="k">$(</span>basename <span class="s2">&#34;</span><span class="k">$(</span>git rev-parse --show-toplevel<span class="k">)</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;------------------------------------------&#34;</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  git status <span class="o">||</span> <span class="k">return</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> -e <span class="s2">&#34;\n📡 Fetching updates from remote...&#34;</span>
</span></span><span class="line"><span class="cl">  git fetch <span class="o">||</span> <span class="k">return</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> branch current remote
</span></span><span class="line"><span class="cl">  <span class="nv">current</span><span class="o">=</span><span class="k">$(</span>git symbolic-ref --short HEAD<span class="k">)</span>
</span></span><span class="line"><span class="cl">  <span class="nv">remote</span><span class="o">=</span><span class="k">$(</span>git <span class="k">for</span>-each-ref --format<span class="o">=</span><span class="s1">&#39;%(upstream:short)&#39;</span> <span class="s2">&#34;refs/heads/</span><span class="nv">$current</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[</span> -z <span class="s2">&#34;</span><span class="nv">$remote</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;⚠️  No remote tracking branch set for &#39;</span><span class="nv">$current</span><span class="s2">&#39;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> -e <span class="s2">&#34;\n🔄 Comparing local &#39;</span><span class="nv">$current</span><span class="s2">&#39; with &#39;</span><span class="nv">$remote</span><span class="s2">&#39;...&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> ahead behind
</span></span><span class="line"><span class="cl">  <span class="nv">ahead</span><span class="o">=</span><span class="k">$(</span>git rev-list --count <span class="s2">&#34;</span><span class="nv">$remote</span><span class="s2">&#34;</span>..HEAD<span class="k">)</span>
</span></span><span class="line"><span class="cl">  <span class="nv">behind</span><span class="o">=</span><span class="k">$(</span>git rev-list --count HEAD..<span class="s2">&#34;</span><span class="nv">$remote</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">((</span> <span class="nv">ahead</span> <span class="o">==</span> <span class="m">0</span> <span class="o">&amp;&amp;</span> <span class="nv">behind</span> <span class="o">==</span> <span class="m">0</span> <span class="o">))</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;✅ Branch is up to date with &#39;</span><span class="nv">$remote</span><span class="s2">&#39;&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;⚠️  Local is </span><span class="nv">$ahead</span><span class="s2"> commit(s) ahead and </span><span class="nv">$behind</span><span class="s2"> commit(s) behind &#39;</span><span class="nv">$remote</span><span class="s2">&#39;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> -e <span class="s2">&#34;\n📋 Commits on remote not in local:&#34;</span>
</span></span><span class="line"><span class="cl">    git log HEAD..<span class="s2">&#34;</span><span class="nv">$remote</span><span class="s2">&#34;</span> --oneline
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> -e <span class="s2">&#34;\n🔍 Run this to view changes:\n  git diff </span><span class="nv">$remote</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><p>I drop this in my shell config (<code>~/.bashrc</code> / <code>~/.zshrc</code>) and now <code>gcheck</code> is just part of my daily workflow.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="gcheck-issues.jpg" 
       alt="output of the gcheck function showing some files and sync issues" 
       style="display:block; margin:0 auto; width:min(100%, 900px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    gcheck combines several commands in a nice simple format to show you the status of your repo at a glance
  </figcaption>
</figure>
<figure style="text-align:center; margin: 1em auto;">
  <img src="gcheck-clean.jpg" 
       alt="output of the gcheck function showing a clean, synced repo" 
       style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    This display showing a cleanly synced repo is always a welcome sight
  </figcaption>
</figure>
<h3 id="why-i-like-it">Why I like it</h3>
<ul>
<li>One glance tells you if you’re up to date with upstream.</li>
<li>If you’re behind, it lists the missing commits so you can see what’s coming.</li>
<li>If you’re ahead, it reminds you to push.</li>
<li>If you’re both — you know it’s merge/rebase time.</li>
<li>And if you’re not even in a Git repo, it politely tells you.</li>
</ul>
<figure style="text-align:center; margin: 1em auto;">
  <img src="ninja-meditate.jpg" 
       alt="a video game style pixel art ninja seated in a meditation pose surrounded by swirling blue energy and symbols" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
<h2 id="handy-aliases">Handy Aliases</h2>
<p>I use the following aliases in my environments. <code>glo</code> is especially handy since it&rsquo;s useful but long and hard to remember. The others are nice when you find yourself running the full command for the thousandth time while working on a project. You can simply add the following to your shell startup files (<code>.bashrc</code> or <code>.zshrc</code>) and reload.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Git</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">gs</span><span class="o">=</span><span class="s1">&#39;git status&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">glo</span><span class="o">=</span><span class="s1">&#39;git log --oneline --graph --decorate --all&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">ga</span><span class="o">=</span><span class="s1">&#39;git add&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">gc</span><span class="o">=</span><span class="s1">&#39;git commit&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">gd</span><span class="o">=</span><span class="s1">&#39;git diff&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">gco</span><span class="o">=</span><span class="s1">&#39;git checkout&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">gp</span><span class="o">=</span><span class="s1">&#39;git push&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">gl</span><span class="o">=</span><span class="s1">&#39;git pull&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">gitcha</span><span class="o">=</span><span class="s1">&#39;gcheck&#39;</span> <span class="c1"># an alias i&#39;m used to for the gcheck function</span>
</span></span></code></pre></div><figure style="text-align:center; margin: 1em auto;">
  <img src="git-glo.jpg" 
       alt="output of glo alias, git log --oneline --graph --decorate --all" 
       style="display:block; margin:0 auto; width:min(100%, 1000px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <code>glo</code> is a really handy alias that I run all the time
  </figcaption>
</figure>
<br>
<h2 id="links--stuff">Links &amp; Stuff</h2>
<h3 id="core-learning-resources"><strong>Core Learning Resources</strong></h3>
<ul>
<li><a href="https://git-scm.com/book/en/v2">Pro Git book (free online)</a> — The definitive Git reference</li>
<li><a href="https://www.atlassian.com/git/tutorials">Atlassian Git Tutorials</a> — Visual, task-oriented guides</li>
<li><a href="https://learngitbranching.js.org/">Learn Git Branching</a> — Interactive visual tutorial</li>
<li><a href="https://gitimmersion.com/">Git Immersion</a> — Hands-on walkthrough tutorial</li>
<li><a href="https://en.wikipedia.org/wiki/Git">Git (Wikipedia)</a> — History, design, and technical details</li>
</ul>
<h3 id="visual--interactive-tools"><strong>Visual &amp; Interactive Tools</strong></h3>
<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=mhutchie.git-graph">Git Graph (VS Code extension)</a> — Visual repo browser</li>
<li><a href="https://www.gitkraken.com/">GitKraken</a> — Popular GUI client</li>
<li><a href="https://www.sourcetreeapp.com/">Sourcetree</a> — Free visual Git client</li>
</ul>
<h3 id="advancedspecialized"><strong>Advanced/Specialized</strong></h3>
<ul>
<li><a href="https://ohshitgit.com/">Oh Shit, Git!?!</a> — How to fix common Git mistakes</li>
<li><a href="http://www-cs-students.stanford.edu/~blynn/gitmagic/">Git Magic</a> — Advanced concepts explained simply</li>
<li><a href="https://github.com/jupyter/nbdime">nbdime</a> — Better jupyter notebook diffs for data science</li>
<li><a href="https://pre-commit.com/">pre-commit</a> — Git hooks framework for code quality</li>
</ul>
<h3 id="quick-reference"><strong>Quick Reference</strong></h3>
<ul>
<li><a href="https://training.github.com/downloads/github-git-cheat-sheet.pdf">Git Cheat Sheet (GitHub)</a> — Official PDF cheat sheet</li>
</ul>
<br>
<h2 id="conclusion">Conclusion</h2>
<figure style="text-align:center; margin: 1em auto;">
  <img src="tsuba.png" 
       alt="a pixel art tsuba or guard from a katana" 
       style="display:block; margin:0 auto; width:min(100%, 300px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
<p>Git is a deep and sometimes intimidating system, but it doesn’t have to be a mystery. With just a handful of concepts, a few daily commands, and some helpers like <code>gcheck</code>, you can navigate most workflows with confidence.</p>
<p>This guide isn’t exhaustive — whole books (like <a href="https://git-scm.com/book/en/v2">Pro Git</a>) exist for a reason — but it covers the moves you’ll use constantly: starting repos, committing, branching, merging, pulling, pushing, and recovering from mistakes.</p>
<p>The rest comes with practice. Every time you branch, merge, or rescue a commit from the reflog, you build muscle memory and intuition. Soon Git feels less like fighting with a tool and more like a trusty time machine you always have at your side.</p>
<p>If you’ve got tips, tricks, or favorite aliases of your own, I’d love to hear them: <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a>.</p>
<p>Happy committing ✨</p>
]]></content:encoded>
    </item>
    <item>
      <title>SchemaSpy with SQLite</title>
      <link>https://adminjitsu.com/posts/schemaspy/</link>
      <pubDate>Mon, 08 Sep 2025 18:30:00 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/schemaspy/</guid>
      <description>SchemaSpy supports SQLite, but only if you use the Xerial JDBC driver and pass the right schema/catalog flags. This guide shows the exact working command.</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<p>SchemaSpy is my preferred tool for generating clear, browsable HTML documentation and ER diagrams for SQL databases. It works seamlessly with engines like MySQL and PostgreSQL, but SQLite has historically been difficult to configure and poorly supported. After considerable trial and error, I’ve identified a reliable combination of components and the exact command needed to make SchemaSpy work consistently with SQLite databases.</p>
<h2 id="the-pieces-you-need">The Pieces You Need</h2>
<p>SchemaSpy itself is “just a JAR,” but to actually generate diagrams you need a few companion pieces: a JDBC driver, a working Java runtime, and (optionally) GraphViz. Getting it to work with SQLite has historically been finicky, if not downright impossible, but the following setup finally did the trick for me</p>
<table>
  <thead>
      <tr>
          <th>Component</th>
          <th>Why It Matters</th>
          <th>Version / Download Link</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>SchemaSpy (fat JAR)</strong></td>
          <td>Engine for generating schema docs &amp; ER diagrams</td>
          <td><a href="https://github.com/schemaspy/schemaspy/releases/download/v6.2.4/schemaspy-6.2.4.jar">Download v6.2.4</a></td>
      </tr>
      <tr>
          <td><strong>SQLite JDBC Driver (Xerial)</strong></td>
          <td>Java connectivity to SQLite <code>.db</code> files</td>
          <td><a href="https://github.com/xerial/sqlite-jdbc/releases/download/3.45.1.0/sqlite-jdbc-3.45.1.0.jar">Download v3.45.1.0</a></td>
      </tr>
      <tr>
          <td><strong>Java 11+ runtime</strong></td>
          <td>Needed to run SchemaSpy (requires Java 8+, but 11+ is safer)</td>
          <td>Install via your package manager (<code>sudo apt install openjdk-11-jre</code> on Debian/Ubuntu) or <a href="https://adoptium.net/">Adoptium.net</a></td>
      </tr>
      <tr>
          <td><strong>GraphViz</strong> <em>(optional)</em></td>
          <td>Use <code>dot</code> for static diagrams</td>
          <td>Install with <code>sudo apt install graphviz</code> or <a href="https://graphviz.org/download/">graphviz.org</a></td>
      </tr>
      <tr>
          <td><strong>Viz.js (<code>-vizjs</code>)</strong> <em>(recommended)</em></td>
          <td>Renders diagrams in-browser without GraphViz</td>
          <td>Built into SchemaSpy v6.1+ — just add the <code>-vizjs</code> flag</td>
      </tr>
  </tbody>
</table>
<h3 id="quick-download-commands">Quick Download Commands</h3>
<p>If you prefer the CLI to clicking links, you can grab the JARs like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Download SchemaSpy 6.2.4</span>
</span></span><span class="line"><span class="cl">curl -L https://github.com/schemaspy/schemaspy/releases/download/v6.2.4/schemaspy-6.2.4.jar <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>     -o schemaspy-6.2.4.jar
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Download SQLite JDBC driver 3.45.1.0</span>
</span></span><span class="line"><span class="cl">curl -L https://repo1.maven.org/maven2/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>     -o sqlite-jdbc-3.45.1.0.jar
</span></span></code></pre></div><h3 id="example-database-chinook">Example Database: Chinook</h3>
<p>If you don’t want to run SchemaSpy against your own data right away, the <a href="https://github.com/lerocha/chinook-database/tree/master/ChinookDatabase/DataSources">Chinook sample database</a> is a great test case.</p>
<ul>
<li>Download the SQL script: <a href="https://raw.githubusercontent.com/lerocha/chinook-database/master/ChinookDatabase/DataSources/Chinook_Sqlite.sql">Chinook_Sqlite.sql</a></li>
<li>Create a SQLite database file from it:</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sqlite3 chinook.db &lt; Chinook_Sqlite.sql
</span></span></code></pre></div><p>This will generate <code>chinook.db</code> populated with a realistic music store schema (artists, albums, tracks, invoices) that works perfectly for testing SchemaSpy.</p>
<hr>
<h2 id="the-command">The Command</h2>
<p>The following example assumes that the SchemaSpy jar file, the sqlite-xerial jdbc driver and the db file (chinook.db) are in the current working directory. Otherwise simply provide the correct path to the required files.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">java -jar schemaspy-6.2.4.jar <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -dp sqlite-jdbc-3.45.1.0.jar <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -t sqlite-xerial -cat % -s main -sso <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -db ./chinook.db <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -o ./schemaspy_output <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -vizjs
</span></span></code></pre></div><p><strong>Breakdown:</strong></p>
<ul>
<li><code>-dp</code> → path to the SQLite JDBC driver jar (Xerial).</li>
<li><code>-t sqlite-xerial</code> → use the modern Xerial driver profile.</li>
<li><code>-cat %</code> → wildcard catalog (SQLite doesn’t really have catalogs).</li>
<li><code>-s main</code> → SQLite’s default schema is always <code>main</code>.</li>
<li><code>-sso</code> → skip username/password (SQLite doesn’t use them).</li>
<li><code>-db</code> → path to your <code>.db</code> file.</li>
<li><code>-o</code> → output folder for the HTML docs.</li>
<li><code>-vizjs</code> → generate diagrams in-browser without GraphViz.</li>
</ul>
<p><strong>Why These Flags Matter</strong></p>
<ul>
<li>Without <code>-dp</code>, SchemaSpy won’t find the correct driver.</li>
<li>Without <code>-t sqlite-xerial</code>, it tries the old/dead SQLite profile.</li>
<li>Without <code>-cat % -s main</code>, you’ll get <em>schema &rsquo;null&rsquo;</em> or <em>catalog not provided</em> errors.</li>
<li><code>-sso</code> avoids pointless login prompts which do not apply to Sqlite.</li>
<li><code>-vizjs</code> keeps the setup lightweight.</li>
</ul>
<hr>
<h2 id="results">Results</h2>
<p>SchemaSpy will generate a lovely, full HTML site in <code>./schemaspy_output</code>. Open <code>index.html</code> to browse:</p>
<ul>
<li>Overview</li>
<li>Table list</li>
<li>Columns and constraints</li>
<li>Relationship diagrams (ERD)</li>
</ul>
<br>
<p><span class="tag green">DEMO</span> <a href="https://forfaxx.github.io/chinook-schemaspy-demo/"><strong>View live Chinook database demo</strong></a></p>
<br>
<figure style="text-align:center; margin: 1em auto;">
  <img src="schemaspy-tables.png" 
       alt="Screenshot of SchemaSpy tables overview page"
       style="display:block; margin:0 auto; width:min(100%, 1000px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    The tables overview generated by SchemaSpy, listing all tables in the database with row counts and basic metadata.
  </figcaption>
</figure>
<figure style="text-align:center; margin: 1em auto;">
  <img src="schemaspy-columns.png" 
       alt="Screenshot of SchemaSpy columns detail page"
       style="display:block; margin:0 auto; width:min(100%, 1000px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    A detail view of columns in a selected table, showing column names, types, nullability, and constraints.
  </figcaption>
</figure>
<figure style="text-align:center; margin: 1em auto;">
  <img src="schemaspy-relationships.png" 
       alt="Screenshot of SchemaSpy ER diagram with table relationships"
       style="display:block; margin:0 auto; width:min(100%, 1000px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    The ER diagram view, with tables connected by their foreign key relationships.
  </figcaption>
</figure>
<p>If you haven&rsquo;t used it before, the generated site is static HTML ready to share with your team and easy to refer to while working with the database!</p>
<hr>
<h2 id="sqlite-in-the-wild">SQLite in the Wild</h2>
<p>One reason it’s worth solving these setup issues is that SQLite is everywhere. Because it’s lightweight, reliable, and requires no separate server process, many applications ship with embedded SQLite databases by default. A few well-known examples:</p>
<ul>
<li>
<p><strong>Apple macOS and iOS</strong></p>
<ul>
<li>Messages: <code>~/Library/Messages/chat.db</code> holds iMessage and SMS history.</li>
<li>Safari: <code>History.db</code>, <code>Downloads.db</code>, and <code>WebpageIcons.db</code> store browsing data.</li>
<li>Photos: <code>Photos.sqlite</code> contains library metadata.</li>
<li>Mail: databases in <code>~/Library/Mail/</code>.</li>
</ul>
</li>
<li>
<p><strong>Web Browsers</strong></p>
<ul>
<li><strong>Chromium/Chrome</strong>: uses SQLite for <code>History</code>, <code>Cookies</code>, <code>Login Data</code>, and more.</li>
<li><strong>Firefox</strong>: <code>places.sqlite</code> for bookmarks and history, <code>cookies.sqlite</code> for cookie storage.</li>
</ul>
</li>
<li>
<p><strong>Messaging and Communication</strong></p>
<ul>
<li><strong>Skype</strong>: <code>main.db</code> contains chat logs.</li>
<li><strong>Signal</strong> and <strong>WhatsApp Desktop</strong>: local SQLite databases maintain encrypted state.</li>
</ul>
</li>
<li>
<p><strong>Other Tools</strong></p>
<ul>
<li><strong>Thunderbird</strong>: mail indexing.</li>
<li><strong>Slack desktop client</strong>: caches workspace data.</li>
<li><strong>VS Code extensions</strong>: sometimes use SQLite for project state.</li>
</ul>
</li>
</ul>
<p>But SQLite isn’t just hidden inside big applications — it’s also ideal for <strong>your own projects</strong>. If you need a lightweight, file-based database with zero setup, SQLite is often the easiest choice. It’s great for prototypes, local tools, logging, side projects, or anything where you want structured storage without spinning up MySQL or Postgres. Pairing SQLite with SchemaSpy gives you a way to instantly visualize and document your schema, even on small personal databases.</p>
<hr>
<h2 id="links--stuff">Links &amp; Stuff</h2>
<ul>
<li>
<p><strong>SchemaSpy</strong><br>
<a href="https://github.com/schemaspy/schemaspy/releases/download/v6.2.4/schemaspy-6.2.4.jar">GitHub Releases – schemaspy-6.2.4.jar</a><br>
<a href="https://schemaspy.readthedocs.io/">SchemaSpy documentation</a></p>
</li>
<li>
<p><strong>SQLite JDBC Driver (Xerial)</strong><br>
<a href="https://github.com/xerial/sqlite-jdbc/releases">GitHub Releases – sqlite-jdbc</a><br>
<a href="https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc">Maven Central – org.xerial:sqlite-jdbc</a></p>
</li>
<li>
<p><strong>Java Runtime</strong><br>
Debian/Ubuntu: <code>sudo apt install openjdk-11-jre</code><br>
macOS (Homebrew): <code>brew install openjdk</code><br>
<a href="https://adoptium.net/">Adoptium.net – Java binaries</a></p>
</li>
<li>
<p><strong>GraphViz</strong> (optional)<br>
<a href="https://graphviz.org/download/">Download from graphviz.org</a><br>
Debian/Ubuntu: <code>sudo apt install graphviz</code><br>
macOS: <code>brew install graphviz</code></p>
</li>
<li>
<p><strong>Chinook Sample Database</strong><br>
<a href="https://raw.githubusercontent.com/lerocha/chinook-database/master/ChinookDatabase/DataSources/Chinook_Sqlite.sql">Chinook_Sqlite.sql</a><br>
<a href="https://github.com/lerocha/chinook-database/tree/master/ChinookDatabase/DataSources">Chinook DataSources repo</a></p>
</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>SchemaSpy remains one of the most effective tools for documenting database schemas and generating ER diagrams. With the correct JDBC driver and options, it now works reliably with SQLite — a database engine that powers not only major applications like Safari, Chrome, and Apple Messages but also countless personal projects.</p>
<p>The working recipe is straightforward once you know it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">-t sqlite-xerial  -dp &lt;sqlite-jdbc.jar&gt;  -cat %  -s main  [-vizjs]
</span></span></code></pre></div><p>This guide demonstrated that configuration with both Apple’s <code>chat.db</code> and the Chinook sample database. The same approach can be applied to almost any SQLite file you encounter, whether it comes from a commercial application or your own side projects.</p>
<p>If you found this guide useful or ran into issues following it, I’d love to hear your feedback. Send comments, corrections, or ideas to <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Dockerizing a Python Script</title>
      <link>https://adminjitsu.com/posts/dockerize-a-script/</link>
      <pubDate>Sat, 06 Sep 2025 00:02:54 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/dockerize-a-script/</guid>
      <description>Building containers isn’t just for big apps — you can package even the smallest Python script into a reliable, portable Docker image. This tutorial walks through the full workflow with a simple example (Smurfify), covering Dockerfile basics, choosing base images, building and running containers, and troubleshooting common issues. By the end, you’ll be ready to containerize your own tools with confidence.</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<p>In <a href="/posts/docker-grimoire/">Docker Grimoire</a> we looked at the commands and concepts that make Docker work. But there is another part of the process that deserves special attention: actually creating a Docker container of your own. In this post we’ll take a simple Python script and package it into a full Docker container that is ready to share — covering the ins and outs of the process.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="blue-mage.png" 
       alt="a pixel art mage in blue robes casting a spell" 
       style="display:block; margin:0 auto; width:min(100%, 400px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Let's learn some more Docker magic
  </figcaption>
</figure>
<p>Docker is fantastic for running other people’s code without worrying about requirements, dependencies, or OS quirks — you just <code>docker run</code> and it works. But the real magic is when you flip that around: <strong>you can package your own scripts the same way.</strong> By building a container, you freeze your code together with just the environment it needs, so it runs the same on your laptop, your server, or someone else’s machine. No “works on my machine” headaches, no dependency juggling.</p>
<h2 id="understanding-dockerfiles">Understanding Dockerfiles</h2>
<p>A <strong>Dockerfile</strong> is the blueprint for building an image. Think of it as a provisioning script: each line makes a small, repeatable change to a base OS image. When you run <code>docker build</code>, Docker executes those steps in order and layers the results into a new image.</p>
<p>The basic loop looks like this:</p>
<pre tabindex="0"><code>Dockerfile  →  docker build  →  Image  →  docker run  →  Container
</code></pre><p>A few important points:</p>
<ul>
<li><strong>Layered builds:</strong> each instruction (<code>FROM</code>, <code>COPY</code>, <code>RUN</code>, etc.) becomes its own cached layer. If you rebuild and nothing in that layer changed, Docker reuses the cached result.</li>
<li><strong>Repeatable and portable:</strong> once you have a Dockerfile, you can rebuild the same environment anywhere, as many times as you like.</li>
<li><strong>Minimal by design:</strong> unlike a full VM image, Dockerfiles usually start from a tiny base (like <code>python:3.12-slim</code>) and add only what’s needed for your app.</li>
</ul>
<p>This makes Dockerfiles both transparent (you can read exactly how an image is built) and reproducible (anyone else can build the same image from the same file).</p>
<h3 id="anatomy-of-a-dockerfile">Anatomy of a Dockerfile</h3>
<p>Most real-world Dockerfiles use just a handful of instructions:</p>
<ul>
<li><strong><code>FROM</code></strong> — pick a base image to build on (e.g. <code>FROM python:3.12-slim</code>)</li>
<li><strong><code>WORKDIR</code></strong> — set the working directory inside the container (<code>WORKDIR /app</code>)</li>
<li><strong><code>COPY</code></strong> — bring files from your project into the image (<code>COPY smurfify.py .</code>)</li>
<li><strong><code>RUN</code></strong> — run commands to install dependencies (<code>RUN pip install -r requirements.txt</code>, <code>RUN apt-get update &amp;&amp; apt-get install -y curl</code>)</li>
<li><strong><code>ENTRYPOINT</code></strong> or <strong><code>CMD</code></strong> — define what should run by default when the container starts (<code>ENTRYPOINT [&quot;python&quot;, &quot;smurfify.py&quot;]</code>)</li>
</ul>
<p>💡 <strong>Tip:</strong> Docker executes instructions <em>top to bottom</em>. Group things that change less often (like installing system packages) near the top so they’re cached, and keep frequently changing code copies (<code>COPY . .</code>) near the bottom.</p>
<p>Together, these instructions create a transparent, reproducible recipe. You can read a Dockerfile and know exactly how an image was built — no mysteries.</p>
<h2 id="choosing-a-base-image">Choosing a Base Image</h2>
<p>Every Dockerfile begins with a <code>FROM</code> line. That single choice sets the foundation for everything else: which OS your container is built on, how big the image will be, and how much work you’ll need to do to get your app running.</p>
<p>Where do these base images come from?</p>
<ul>
<li><strong><a href="https://hub.docker.com/">Docker Hub</a></strong> is the default registry. If you write <code>FROM python:3.12-slim</code>, Docker pulls it from <code>hub.docker.com/library/python</code>.</li>
<li>You can browse tags on Docker Hub or check the source Dockerfiles (often maintained on GitHub).</li>
<li>Many projects also publish to <strong>GitHub Container Registry (ghcr.io)</strong> or <strong>Quay.io</strong>, but Docker Hub is the most common.</li>
</ul>
<p>Common base image patterns:</p>
<ul>
<li><strong><code>python:X.Y</code></strong> → Full Debian-based image. Includes Python and lots of tools. Bigger, but very compatible.</li>
<li><strong><code>python:X.Y-slim</code></strong> → Stripped-down Debian variant. Smaller size, still good compatibility. A great default.</li>
<li><strong><code>python:X.Y-alpine</code></strong> → Based on Alpine Linux. Tiny, but can cause headaches when Python packages need C libraries.</li>
<li><strong><code>ubuntu</code>, <code>debian</code>, <code>alpine</code></strong> → Bare OS bases, good if you want to install tools yourself.</li>
</ul>
<p>💡 <strong>Rule of thumb:</strong></p>
<ul>
<li>Use <strong>slim</strong> for most apps — small, reliable, and widely supported.</li>
<li>Use <strong>alpine</strong> if size really matters and you’re comfortable fixing build errors.</li>
<li>Use the <strong>full image</strong> if you need lots of system packages or want fewer surprises.</li>
</ul>
<hr>
<h3 id="researching-images">Researching Images</h3>
<p>Docker doesn’t have an <code>apt show</code> equivalent — most image research happens on the registry pages themselves. For <a href="https://hub.docker.com/">Docker Hub</a> images, that means checking pages like <a href="https://hub.docker.com/_/python">Python on Docker Hub</a>.</p>
<p>Here’s what to look for: available tags, size comparisons, and the <strong>Image Variants</strong> section that explains trade-offs. For Python, you’ll see:</p>
<ul>
<li><code>python:3.12</code> (1.02GB) — full Debian base with build tools</li>
<li><code>python:3.12-slim</code> (131MB) — stripped down but still compatible</li>
<li><code>python:3.12-alpine</code> (48MB) — tiny Alpine base, but can cause build issues</li>
</ul>
<p>The same pattern applies to other languages:</p>
<ul>
<li><code>node:20</code> vs <code>node:20-slim</code> vs <code>node:20-alpine</code></li>
<li><code>golang:1.21</code> vs <code>golang:1.21-alpine</code></li>
</ul>
<p>The registry page tells you what each variant includes and when to use it.</p>
<p>Now let’s put this into practice with a real example.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="crystal-golem.png" 
       alt="a golem made of crystal, angular, light blue" 
       style="display:block; margin:0 auto; width:min(100%, 420px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    I hope this is crystal clear
  </figcaption>
</figure>
<br>
<h2 id="dockerizing-smurfify">Dockerizing Smurfify</h2>
<p>We’ll use my <a href="/posts/smurfify/">Smurfify</a> script as the example. It’s lighthearted, but it makes a perfect example: no dependencies, easy to test, and fun to run. By the end, you’ll understand the workflow for containerizing any script or small app.</p>
<hr>
<h3 id="project-setup">Project Setup</h3>
<p>The first step is to create a project directory to hold everything related to the container. On my system, I keep container projects under <code>~/codelab/containers/</code>, so let’s make a home for Smurfify:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p ~/codelab/containers/smurfify
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ~/codelab/containers/smurfify
</span></span><span class="line"><span class="cl">git init
</span></span></code></pre></div><p>I like to track these builds in Git so I can version changes and keep my Dockerfiles tidy. That means adding a <code>.gitignore</code> right away so you don’t end up committing build artifacts and temp files. Create a file named <code>.gitignore</code> with contents like this:</p>
<pre tabindex="0"><code class="language-gitignore" data-lang="gitignore"># Ignore Python cruft
__pycache__/
*.pyc
*.pyo

# Ignore Docker build artifacts
*.tar
*.log

# Ignore anything generated at runtime
.env
</code></pre><p>This keeps the repo focused on just your source and Dockerfile.</p>
<p>Now copy the script into the folder:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cp ~/codelab/bin/smurfify.py .
</span></span></code></pre></div><p>At this point you should have a clean project directory with Git initialized, a <code>.gitignore</code> in place, and your script ready to go.</p>
<p>For this example, we’ll base our container on <strong><code>python:3.12-slim</code></strong> — small enough to be efficient, but big enough to avoid the headaches Alpine images can cause when building Python dependencies.</p>
<hr>
<h3 id="writing-the-dockerfile">Writing the Dockerfile</h3>
<p>A <strong>Dockerfile</strong> is the recipe for building an image — each line makes a small, repeatable change to a base OS. For Smurfify we don’t need much: just Python and the script itself.</p>
<p>Create a file called <code>Dockerfile</code> inside <code>~/codelab/containers/smurfify/</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="cl"><span class="c"># Start with a minimal Python base image</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">FROM</span><span class="s"> python:3.12-slim</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="c"># Set a working directory inside the container</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">WORKDIR</span><span class="s"> /app</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="c"># If you have Python requirements you would uncomment these lines</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="c"># COPY requirements.txt .</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="c"># RUN pip install -r requirements.txt</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="c"># Copy the script into the image</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">COPY</span> smurfify.py .<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="c"># Run the script by default</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">ENTRYPOINT</span> <span class="p">[</span><span class="s2">&#34;python&#34;</span><span class="p">,</span> <span class="s2">&#34;smurfify.py&#34;</span><span class="p">]</span><span class="err">
</span></span></span></code></pre></div><p>That’s the entire recipe:</p>
<ul>
<li>Start with Python</li>
<li>Drop in your script</li>
<li>Define the default command</li>
</ul>
<p>If your script had external dependencies, you would uncomment the requirements.txt and pip install lines in the example above</p>
<p><span class="tag green">Note</span> <strong>ENTRYPOINT vs CMD:</strong> <code>ENTRYPOINT</code> locks in your command - arguments from <code>docker run</code> get appended to it. <code>CMD</code> is flexible - arguments completely replace it. For scripts that take input (like Smurfify), use <code>ENTRYPOINT</code> so users can run <code>docker run smurfify &quot;hello world&quot;</code> and it becomes <code>python smurfify.py &quot;hello world&quot;</code>. Use <code>CMD</code> for utility containers where users might want to run different commands entirely.</p>
<p>Now your project tree should look like this:</p>
<pre tabindex="0"><code>codelab/containers/smurfify/
├── .git/
├── .gitignore
├── smurfify.py
└── Dockerfile
</code></pre><hr>
<h3 id="building-the-image">Building the Image</h3>
<p>Now that we’ve written a Dockerfile, the next step is to <strong>build it into an image</strong> — turning our text recipe into an actual runnable package.</p>
<p>From inside the project directory (<code>~/codelab/containers/smurfify/</code>), run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker build -t smurfify:latest .
</span></span></code></pre></div><p>Here’s what’s happening:</p>
<ul>
<li><strong><code>docker build</code></strong> → tells Docker to create an image from a Dockerfile.</li>
<li><strong><code>-t smurfify:latest</code></strong> → tags the image with a name (<code>smurfify</code>) and version (<code>latest</code>).</li>
<li><strong><code>.</code></strong> → sets the build context to the current directory (everything here is available for <code>COPY</code>).</li>
</ul>
<p>Docker will step through the file line by line: pulling the Python base image, creating <code>/app</code>, copying in <code>smurfify.py</code>, and wiring up the entrypoint. You’ll see each instruction logged as it runs like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">└─$ docker build -t smurfify:latest .
</span></span><span class="line"><span class="cl"><span class="o">[</span>+<span class="o">]</span> Building 0.6s <span class="o">(</span>8/8<span class="o">)</span> FINISHED                                                                         docker:default
</span></span><span class="line"><span class="cl"> <span class="o">=</span>&gt; <span class="o">[</span>internal<span class="o">]</span> load build definition from Dockerfile                                                               0.0s
</span></span><span class="line"><span class="cl"> <span class="o">=</span>&gt; <span class="o">=</span>&gt; transferring dockerfile: 282B                                                                               0.0s
</span></span><span class="line"><span class="cl"> <span class="o">=</span>&gt; <span class="o">[</span>internal<span class="o">]</span> load metadata <span class="k">for</span> docker.io/library/python:3.12-slim                                                0.2s
</span></span><span class="line"><span class="cl"> <span class="o">=</span>&gt; <span class="o">[</span>internal<span class="o">]</span> load .dockerignore                                                                                  0.0s
</span></span><span class="line"><span class="cl"> <span class="o">=</span>&gt; <span class="o">=</span>&gt; transferring context: 2B                                                                                    0.0s
</span></span><span class="line"><span class="cl"> <span class="o">=</span>&gt; <span class="o">[</span>1/3<span class="o">]</span> FROM docker.io/library/python:3.12-slim@sha256:d67a7b66b989ad6b6d6b10d428dcc5e0bfc3e5f88906e67d490c4d3d  0.0s
</span></span><span class="line"><span class="cl"> <span class="o">=</span>&gt; <span class="o">[</span>internal<span class="o">]</span> load build context                                                                                  0.0s
</span></span><span class="line"><span class="cl"> <span class="o">=</span>&gt; <span class="o">=</span>&gt; transferring context: 33B                                                                                   0.0s
</span></span><span class="line"><span class="cl"> <span class="o">=</span>&gt; CACHED <span class="o">[</span>2/3<span class="o">]</span> WORKDIR /app                                                                                      0.0s
</span></span><span class="line"><span class="cl"> <span class="o">=</span>&gt; CACHED <span class="o">[</span>3/3<span class="o">]</span> COPY smurfify.py .                                                                                0.0s
</span></span><span class="line"><span class="cl"> <span class="o">=</span>&gt; exporting to image                                                                                             0.0s
</span></span><span class="line"><span class="cl"> <span class="o">=</span>&gt; <span class="o">=</span>&gt; exporting layers                                                                                            0.0s
</span></span><span class="line"><span class="cl"> <span class="o">=</span>&gt; <span class="o">=</span>&gt; writing image sha256:1b0752cd62e6222c9cb5ec2f84e9ad2b625baad4cac8ecb08070682c2e2cab0c                       0.0s
</span></span><span class="line"><span class="cl"> <span class="o">=</span>&gt; <span class="o">=</span>&gt; naming to docker.io/library/smurfify:latest     
</span></span></code></pre></div><p>💡 <strong>Tip:</strong> Docker caches layers. If you rebuild after making small edits, only the changed steps rerun — everything else comes from cache. This makes rebuilds much faster once the base layers are downloaded.</p>
<hr>
<p><strong>Where did the image go?</strong></p>
<p>After the build, you won’t see anything new in your project folder. That’s expected:</p>
<ul>
<li>Your project directory only holds the <em>recipe</em> (<code>Dockerfile</code>, source code).</li>
<li>The <em>resulting image</em> is stored inside Docker’s <strong>local image store</strong> (by default under <code>/var/lib/docker/</code> on Linux).</li>
</ul>
<p>To see it, list your local images:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker images
</span></span><span class="line"><span class="cl"><span class="c1"># or</span>
</span></span><span class="line"><span class="cl">docker image ls
</span></span></code></pre></div><p>Example output (I use grep because I have a lot of images):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">└─$ docker image ls <span class="p">|</span> grep smurfify
</span></span><span class="line"><span class="cl">smurfify                                 latest        1b0752cd62e6   About an hour ago   144MB
</span></span></code></pre></div><p>So think of it this way: your working directory holds the recipe, and Docker’s internal registry holds the finished meal.</p>
<hr>
<h3 id="running-the-container">Running the Container</h3>
<p>With the image built, we can test it out and see it in action.</p>
<p>Basic run with an argument:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run smurfify <span class="s2">&#34;Help me, Obi-Wan Kenobi, you&#39;re my only hope&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># outputs: Smurf me, Obi-Wan Kenobi, you&#39;re my only smurf</span>
</span></span></code></pre></div><p>Or pipe input with STDIN:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;All the world&#39;s a stage&#34;</span> <span class="p">|</span> docker run -i smurfify
</span></span><span class="line"><span class="cl"><span class="c1"># outputs: All the smurf&#39;s a stage</span>
</span></span></code></pre></div><h3 id="interactive-mode">Interactive Mode</h3>
<p>Because our script has a REPL mode (it reads from stdin if no arguments are provided), we can drop into it directly using <code>-it</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run -it smurfify
</span></span></code></pre></div><p>Example session:</p>
<pre tabindex="0"><code>smurfify.py 💙 - Type a line to smurf. Ctrl-D (or Ctrl-Z) to quit.
Damn the torpedoes, full speed ahead!
Smurf! the torpedoes, full speed ahead!
Ask not what your country can do for you -- ask what you can do for your country.
Smurf not what your country can do for you -- smurf what you can do for your country.
</code></pre><p>💡 <strong>What <code>-it</code> means:</strong></p>
<ul>
<li><code>-i</code> → <em>interactive</em>: keeps STDIN open so the container can accept your input.</li>
<li><code>-t</code> → <em>TTY</em>: allocates a pseudo-terminal, so you get proper line editing and prompts.</li>
</ul>
<p>Together, <code>-it</code> makes containers behave like a program you can talk to in real time — perfect for scripts like Smurfify.</p>
<hr>
<p>At this point, you can:</p>
<ul>
<li>Run it once with arguments.</li>
<li>Pipe text into it.</li>
<li>Use it interactively with <code>-it</code>.</li>
</ul>
<p>That covers the full set of ways you’d normally interact with a CLI tool inside Docker. Of course, not every container is this simple — if you were shipping a whole web server or database, you’d likely be exposing ports, mounting volumes, or running multiple services. But the same core idea applies: image → container → run.</p>
<br>
<h2 id="troubleshooting--tips">Troubleshooting &amp; Tips</h2>
<figure style="text-align:center; margin: 1em auto;">
  <img src="skeleton-warrior.png" 
       alt="a skeletal warrior holding a sword and wearing a tattered tunic and red cape" 
       style="display:block; margin:0 auto; width:min(100%, 320px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
     A few monsters lurk in the dungeon but nothing we can't handle
   </figcaption>
</figure>
<br>
<p>💡 <strong>Our example is simple on purpose.</strong><br>
Real projects get messy — different languages, heavier dependencies, weird edge cases. The good news: the workflow doesn’t change. Here are a few common pitfalls that you might run into:</p>
<hr>
<p><strong>Common Gotchas</strong></p>
<ul>
<li>
<p><strong>Image too big?</strong><br>
Try a <code>-slim</code> or <code>-alpine</code> variant. Just note Alpine can break builds when native extensions need glibc.</p>
</li>
<li>
<p><strong>Container exits immediately?</strong><br>
Containers stop when their command finishes. For scripts, that’s normal — they run and then quit. Use <code>-it</code> for interactive use, or design a service that keeps running.</p>
</li>
<li>
<p><strong><code>COPY</code> or <code>ADD</code> not working?</strong><br>
Check your build context. Only files in the same directory (and subdirectories) as your <code>Dockerfile</code> get included.</p>
</li>
<li>
<p><strong>Dependency errors?</strong><br>
Make sure installs happen <strong>inside</strong> the image:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="cl"><span class="k">RUN</span> pip install -r requirements.txt<span class="err">
</span></span></span></code></pre></div><p>not on your host machine.</p>
</li>
<li>
<p><strong>Builds are slow or images feel bloated?</strong><br>
Add a <code>.dockerignore</code> next to your <code>Dockerfile</code> — it works like <code>.gitignore</code> and keeps junk (<code>.git/</code>, caches, logs, secrets) out of your build context.</p>
</li>
</ul>
<hr>
<p><strong>Pro-Tips</strong></p>
<ul>
<li>Always pin your base image version (<code>python:3.12-slim</code>, not <code>python:latest</code>) for reproducibility.</li>
<li>Keep Dockerfiles minimal: smaller images build faster, pull faster, and break less.</li>
<li>For debugging builds, temporarily add a shell:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="cl"><span class="k">RUN</span> apt-get update <span class="o">&amp;&amp;</span> apt-get install -y vim curl<span class="err">
</span></span></span></code></pre></div>then strip it out later.</li>
</ul>
<br>
<h2 id="links-and-stuff">Links and Stuff</h2>
<p><strong>Essential References:</strong></p>
<ul>
<li><strong><a href="https://docs.docker.com/reference/dockerfile/">Dockerfile Reference</a></strong> — The complete command reference for all Dockerfile instructions</li>
<li><strong><a href="https://hub.docker.com/">Docker Hub</a></strong> — Browse and search for base images</li>
<li><strong><a href="https://docs.docker.com/reference/cli/docker/">Docker CLI Reference</a></strong> — All the <code>docker</code> commands you&rsquo;ll use</li>
</ul>
<p><strong>Best Practices &amp; Guides:</strong></p>
<ul>
<li><strong><a href="https://docs.docker.com/develop/dev-best-practices/">Dockerfile Best Practices</a></strong> — Official guidance on writing efficient, maintainable Dockerfiles</li>
<li><strong><a href="https://www.docker.com/blog/intro-guide-to-dockerfile-best-practices/">Docker Build Best Practices</a></strong> — Covers incremental build time, image size, maintainability, security and repeatability</li>
<li><strong><a href="https://docs.docker.com/build/building/multi-stage/">Multi-stage Builds</a></strong> — For keeping production images lean</li>
</ul>
<p><strong>When Things Go Wrong:</strong></p>
<ul>
<li><strong><a href="https://docs.docker.com/build/checks/">Build Checks</a></strong> — Validate your build configuration and catch common issues</li>
<li><strong><a href="https://docs.docker.com/reference/cli/docker/logs/">Docker Logs</a></strong> — Essential for debugging container problems</li>
</ul>
<br>
<h2 id="conclusion">Conclusion</h2>
<p>That&rsquo;s the whole workflow: <strong>script → Dockerfile → image → container.</strong></p>
<p>This workflow works for any tool you want to containerize. Now when you need to demo that sweet script or share a utility with the world, you&rsquo;ll have a tool that actually works — no more &ldquo;but it works on my machine&rdquo; disasters or impromptu troubleshooting sessions.</p>
<p>Time to practice! Containerize your own scripts and keep the <a href="/posts/docker-grimoire/">Docker Grimoire</a> handy for reference.</p>
<p>Have a Docker war story? Killer tip? I&rsquo;d love to hear about it: <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Docker Grimoire</title>
      <link>https://adminjitsu.com/posts/docker-grimoire/</link>
      <pubDate>Thu, 04 Sep 2025 22:17:11 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/docker-grimoire/</guid>
      <description>Docker can feel overwhelming, but it doesn’t have to be. This guide organizes commands into task-oriented cheatsheets — from running and inspecting containers to Compose, Dockerfiles, and troubleshooting. Each section pairs commands with plain-English explanations, usage notes, and official docs, turning Docker into a tool you can actually master.</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<p>Docker has become one of those critical technologies powering everything from scrappy homelabs to enterprise infrastructure. It can feel intimidating at first—so many new terms, layers, and tools—but armed with a good cheatsheet and the right explanations, Docker turns out to be surprisingly fun and almost <em>magically</em> powerful.</p>
<p>With it, you can host an entire network of services on your own machine: media servers like <strong>Jellyfin</strong> and <strong>Immich</strong>, full <strong>LAMP stacks</strong> for web development, databases, your own git server or even your own custom scripts packaged and deployed cleanly.</p>
<p>In this guide we’ll compile commands, concepts, official docs, and practical examples into one handy resource. Because Docker is less confusing than it seems—and with a few best practices, it can become one of the most reliable and versatile parts of your network.</p>
<h2 id="what-is-docker">What is Docker?</h2>
<p>At its core, Docker is a way to run <strong>little computers inside your computer.</strong></p>
<p>Each Docker container is its own lightweight environment, with just enough OS, libraries, and binaries to do one job well—whether that’s serving a web app, crunching data, or running a database. Instead of cluttering your base OS with packages and configs, you pull a prebuilt image and run it in isolation.</p>
<p>Technically speaking, Docker builds on core Linux kernel features like <strong><a href="https://en.wikipedia.org/wiki/Linux_namespaces">namespaces</a></strong>, <strong><a href="https://en.wikipedia.org/wiki/Cgroups">cgroups</a></strong>, and <strong><a href="https://kernel.org/doc/html/latest/filesystems/overlayfs.html">union (overlay) filesystems</a></strong> to provide process isolation and resource management. Unlike traditional virtualization—which uses a <strong><a href="https://en.wikipedia.org/wiki/Hypervisor">hypervisor</a></strong> to run full guest operating systems on abstracted hardware—Docker employs <strong><a href="https://en.wikipedia.org/wiki/OS-level_virtualization">OS-level virtualization</a></strong> (containerization), sharing the host kernel across containers and making it far more lightweight and efficient.</p>
<p>A bit of history:</p>
<ul>
<li>Docker was created in 2013 by <strong>Solomon Hykes</strong> and the team at dotCloud (a PaaS startup), and has since evolved into a massive open-source ecosystem.</li>
<li>It’s primarily written in <strong>Go</strong>, with CLI tooling and APIs that work across Linux, Windows, and macOS.</li>
<li>In just over a decade, it’s become a near-ubiquitous layer in modern development and operations: used in CI/CD pipelines, cloud deployments, and, increasingly, personal projects and homelabs.</li>
</ul>
<p>Why is it so popular? A few key benefits:</p>
<ul>
<li><strong>Isolation without the overhead</strong> of full VMs.</li>
<li><strong>Consistency across environments</strong>: the same image runs on your laptop, server, or in the cloud.</li>
<li><strong>Portability and sharing</strong>: Docker Hub and registries make distributing software trivial.</li>
<li><strong>Resource efficiency</strong>: containers spin up in seconds and can be packed densely on a single host.</li>
<li><strong>Flexibility</strong>: from one-off experiments to long-lived production services.</li>
</ul>
<p>Today, Docker shows up everywhere—from billion-dollar cloud platforms to basement labs. On my own server <code>gir.darkstar.home</code>, I run over twenty containers: Jellyfin, Portainer, Dashy, Uptime Kuma, Gitea, Jupyter Labs and more. It keeps everything clean and manageable and easy to backup. I kind of love Docker!</p>
<p>In the sections ahead, we’ll start with a practical cheatsheet of Docker commands, then build toward hands-on tutorials: installing Portainer, containerizing a script, updating a service without losing data, and even migrating containers to a new host.</p>
<hr>
<h2 id="usage-the-docker-cli-cheatsheet">Usage: The Docker CLI Cheatsheet</h2>
<figure style="float:left; margin:0 1rem 1rem 0; width:clamp(260px, 45%, 550px);">
  <img src="arcmage1.png" 
       alt="a wise old wizard. video game style pixel art" 
       style="display:block; width:100%; height:auto;">
  <figcaption style="font-size:85%; color:#666; line-height:1.4; margin-top:0.4em; text-align:center;">
    Unlike stage magicians, Unix wizards are happy to share their tricks!
  </figcaption>
</figure>
<p>Even if you lean on Docker Desktop, Portainer, or other GUIs, the <strong>command line is always there</strong>.<br>
It’s the universal interface: every container operation can be expressed at the CLI,<br>
whether you’re hacking on a laptop, administering a headless server, or scripting a CI/CD pipeline.</p>
<p>If you can run it from the CLI, you can automate it, debug it, or wrap it in a script later.</p>
<p>This cheatsheet is organized into <strong>task-oriented sections</strong>, each with:</p>
<ul>
<li>A quick explanation of <em>why</em> the task matters</li>
<li>A collapsible block of relevant commands</li>
<li>Footnotes, Usage caveats and more</li>
</ul>
<p>Commands may appear in more than one section if they’re useful for that workflow.<br>
By the end, you’ll have a mental map of what to reach for, whether you’re checking logs, moving files,<br>
or pruning old containers that are eating up disk space.</p>
<hr>
<h3 id="checking-whats-running"><span style="color:#CC0000;">Checking What&rsquo;s Running</span></h3>
<p>The first step in container management is knowing what’s happening right now.<br>
Which containers are up, which have stopped, and how much load they’re putting on your system?<br>
This section gives you the heartbeat of your Docker host — status, ports, resource usage, and quick inspections.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># List running containers (default view)</span>
</span></span><span class="line"><span class="cl">docker ps
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show ALL containers (running + stopped)</span>
</span></span><span class="line"><span class="cl">docker ps -a
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Quiet mode (IDs only) — handy for scripting</span>
</span></span><span class="line"><span class="cl">docker ps -q
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Filter example: show only stopped/exited containers</span>
</span></span><span class="line"><span class="cl">docker ps -f <span class="s2">&#34;status=exited&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Custom table view: names, status, ports</span>
</span></span><span class="line"><span class="cl">docker ps --format <span class="s2">&#34;table {{.Names}}\t{{.Status}}\t{{.Ports}}&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Live resource usage (CPU, MEM, I/O, PIDs)</span>
</span></span><span class="line"><span class="cl">docker stats
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># One-shot resource snapshot (no stream)</span>
</span></span><span class="line"><span class="cl">docker stats --no-stream
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show port mappings for a container</span>
</span></span><span class="line"><span class="cl">docker port &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show processes running inside a container</span>
</span></span><span class="line"><span class="cl">docker top &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Inspect container details (JSON: state, mounts, networks)</span>
</span></span><span class="line"><span class="cl">docker inspect &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Quick: container IP address</span>
</span></span><span class="line"><span class="cl">docker inspect -f <span class="s1">&#39;{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}&#39;</span> &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Quick: restart count (useful for crash loops)</span>
</span></span><span class="line"><span class="cl">docker inspect -f <span class="s1">&#39;{{.RestartCount}}&#39;</span> &lt;container_name&gt;</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li><code>docker ps</code> shows only <em>running</em> containers; add <code>-a</code> to include stopped/exited ones.</li>
<li>Use <code>--format</code> for clean custom output; in PowerShell, wrap the template in single quotes.</li>
<li><code>docker stats</code> streams by default — use <code>--no-stream</code> for scripts or snapshots.</li>
<li><code>docker port</code> only shows published (host-exposed) ports; for internal-only, check the container network.</li>
<li><code>docker top</code> is a quick sanity check for runaway processes without attaching a shell.</li>
<li>For storage usage (<code>docker system df</code>), see the <strong>Cleanup &amp; Maintenance</strong> section.</li>
</ul>
  </div>
</details>

<hr>
<h3 id="starting-stopping-and-restarting"><span style="color:#CC0000;">Starting, Stopping, and Restarting</span></h3>
<p>Once you’ve identified a container, the next step is controlling its <strong>lifecycle</strong>.<br>
Containers aren’t meant to be long-lived processes in the same sense as system daemons; they’re designed to start fast, stop cleanly, and be replaced when needed.<br>
Knowing how to start, stop, restart, and remove containers lets you manage your stack gracefully instead of reaching for host-level reboots or <code>kill -9</code>.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <br>
<p>💡 <strong>Tip:</strong> If you don’t know the container’s name or ID, list them first:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker ps -a --format <span class="s2">&#34;table {{.Names}}\t{{.ID}}\t{{.Status}}&#34;</span>
</span></span></code></pre></div><br>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Start a stopped container</span>
</span></span><span class="line"><span class="cl">docker start &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Stop a running container (graceful SIGTERM, then SIGKILL after timeout)</span>
</span></span><span class="line"><span class="cl">docker stop &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Force-stop immediately (no grace period)</span>
</span></span><span class="line"><span class="cl">docker <span class="nb">kill</span> &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Restart a container (stop + start)</span>
</span></span><span class="line"><span class="cl">docker restart &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Pause/resume all processes in a container</span>
</span></span><span class="line"><span class="cl">docker pause &lt;container_name&gt;
</span></span><span class="line"><span class="cl">docker unpause &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Remove (delete) a container entirely</span>
</span></span><span class="line"><span class="cl">docker rm &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Remove a container that’s still running (force)</span>
</span></span><span class="line"><span class="cl">docker rm -f &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Automatically remove a container after it exits (good for throwaway/test runs)</span>
</span></span><span class="line"><span class="cl">docker run --rm &lt;image&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Start a new container in the background (detached mode)</span>
</span></span><span class="line"><span class="cl">docker run -d --name &lt;name&gt; &lt;image&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Start a new container and attach to it interactively</span>
</span></span><span class="line"><span class="cl">docker run -it --name &lt;name&gt; &lt;image&gt; /bin/bash</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li><code>docker ps -a</code> shows both running and stopped containers — use this before lifecycle actions.</li>
<li><code>docker stop</code> tries a graceful shutdown (default 10s timeout) before sending <code>SIGKILL</code>.</li>
<li>Use <code>docker kill</code> if you need immediate termination, but it skips cleanup inside the container.</li>
<li><code>docker rm</code> deletes only the container metadata; <strong>images and volumes remain</strong> until explicitly removed.</li>
<li><code>docker rm -f</code> is a shortcut for “stop + remove” but can interrupt clean shutdowns.</li>
<li><code>docker pause</code> is niche but useful when you want to freeze a container’s CPU/memory usage without stopping it.</li>
<li><code>--rm</code> is great for <strong>temporary containers</strong> (debug shells, quick tests) since they clean up after themselves.</li>
<li>For production services, prefer <code>docker run -d</code> with a name and mapped ports so it persists and can be restarted later.</li>
</ul>

  </div>
</details>

<hr>
<h3 id="watching-what-containers-are-doing"><span style="color:#CC0000;">Watching What Containers Are Doing</span></h3>
<p>Once a container is running, you’ll often need to peek inside.<br>
Maybe it’s to check logs for errors, monitor activity in real time, or open a shell to troubleshoot directly.<br>
Think of this as your <strong>observation toolkit</strong> — the ways to see what’s happening under the hood without tearing the container apart.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <p>💡 <strong>Tip:</strong> First, list container names/IDs so you know what to target:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker ps --format <span class="s2">&#34;table {{.Names}}\t{{.ID}}\t{{.Status}}&#34;</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Logs ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Follow logs in real time (like `tail -f`)</span>
</span></span><span class="line"><span class="cl">docker logs -f &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show the last 50 log lines</span>
</span></span><span class="line"><span class="cl">docker logs --tail <span class="m">50</span> &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show logs since a specific timestamp (ISO 8601 or &#34;1h&#34; for last hour)</span>
</span></span><span class="line"><span class="cl">docker logs --since 1h &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Interactive Access ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Open a shell inside a running container (bash if available)</span>
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> -it &lt;container_name&gt; /bin/bash
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Fallback: sh is more universally present</span>
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> -it &lt;container_name&gt; sh
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run a one-off command inside a container</span>
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> -it &lt;container_name&gt; &lt;command&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Resource Monitoring ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Live resource usage (CPU, memory, network, disk I/O) for one container</span>
</span></span><span class="line"><span class="cl">docker stats &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show resource usage for all running containers</span>
</span></span><span class="line"><span class="cl">docker stats
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Process Inspection ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show processes running inside a container</span>
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> &lt;container_name&gt; ps aux
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === File System Inspection ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Browse container filesystem from outside</span>
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> &lt;container_name&gt; ls -la /app
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Search for config files inside container</span>
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> &lt;container_name&gt; find /etc -name <span class="s2">&#34;*.conf&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Attach to Main Process ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Attach directly to the container’s main process (stdout/stderr)</span>
</span></span><span class="line"><span class="cl">docker attach &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Detach safely with CTRL-p + CTRL-q (not CTRL-c!)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># CTRL-c will usually stop the container</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li><code>docker logs</code> only shows what the container’s <strong>main process</strong> writes to stdout/stderr. For apps writing to files, use <code>docker exec</code>.</li>
<li>Combine <code>--tail</code> and <code>--since</code> to make logs manageable for long-running services.</li>
<li><code>docker exec</code> is the safest way to explore — it runs a new process inside without disturbing the main one.</li>
<li><code>docker stats</code> is great for spotting runaway containers eating CPU or memory. Add <code>--no-stream</code> for a single snapshot.</li>
<li>For quick process checks, <code>docker top &lt;container&gt;</code> also works, but <code>docker exec ... ps aux</code> gives more familiar detail.</li>
<li>File browsing (<code>ls</code>, <code>find</code>) via <code>docker exec</code> is invaluable for checking mounts, configs, or missing files.</li>
<li><code>docker attach</code> ties you to the main process — only use if you know what you’re doing, and remember <code>CTRL-p CTRL-q</code> to detach without killing it.</li>
<li>With Docker Compose, <code>docker compose logs -f</code> can give you a unified view across services.</li>
</ul>

  </div>
</details>

<hr>
<h3 id="images-and-building"><span style="color:#CC0000;">Images and Building</span></h3>
<p>Images are the blueprints for containers — the templates you pull, inspect, and build from.<br>
They define what’s inside: the OS layer, libraries, binaries, and startup command.<br>
With Docker you don’t build everything from scratch — you usually <strong>pull images from a registry</strong> and then run or customize them.</p>
<ul>
<li><strong><a href="https://hub.docker.com/">Docker Hub</a></strong> → the default, largest public registry.</li>
<li><strong><a href="https://ghcr.io/">GitHub Container Registry (GHCR)</a></strong> → popular for open-source projects (<code>ghcr.io/&lt;org&gt;/&lt;image&gt;</code>).</li>
<li><strong><a href="https://quay.io/">Quay.io</a></strong> → another common source for official and community images.</li>
</ul>
<p>If no registry is specified, Docker defaults to <strong>Docker Hub</strong>.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <p>💡 <strong>Tip:</strong> Use <code>docker image</code> (newer) or <code>docker</code> subcommands (older). Both work, but the <code>image</code> namespace is more explicit.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Discovering and Pulling Images ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Search for images on Docker Hub</span>
</span></span><span class="line"><span class="cl">docker search nginx
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Pull the canonical test image</span>
</span></span><span class="line"><span class="cl">docker pull hello-world
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run it to verify Docker works</span>
</span></span><span class="line"><span class="cl">docker run hello-world
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Pull the latest version of nginx from Docker Hub</span>
</span></span><span class="line"><span class="cl">docker pull nginx
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Pull a specific version/tag of nginx</span>
</span></span><span class="line"><span class="cl">docker pull nginx:1.27
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Pull from GitHub Container Registry</span>
</span></span><span class="line"><span class="cl">docker pull ghcr.io/linuxserver/jellyfin:latest
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Pull from Quay.io</span>
</span></span><span class="line"><span class="cl">docker pull quay.io/coreos/etcd:latest
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Listing and Inspecting ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># List all images on your system</span>
</span></span><span class="line"><span class="cl">docker images
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show detailed metadata for an image (JSON)</span>
</span></span><span class="line"><span class="cl">docker inspect nginx
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show image history (layer breakdown)</span>
</span></span><span class="line"><span class="cl">docker <span class="nb">history</span> nginx
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Tagging and Naming ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Tag an image with a new name (useful for pushing)</span>
</span></span><span class="line"><span class="cl">docker tag nginx myrepo/mynginx:latest
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Remove one or more images</span>
</span></span><span class="line"><span class="cl">docker rmi nginx
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Building Custom Images ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Build an image from a Dockerfile in the current directory</span>
</span></span><span class="line"><span class="cl">docker build -t myapp:latest .
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Build using a custom Dockerfile</span>
</span></span><span class="line"><span class="cl">docker build -f Dockerfile.dev -t myapp:dev .
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Build without using the cache</span>
</span></span><span class="line"><span class="cl">docker build --no-cache -t myapp:test .
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Cleaning Up ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Remove dangling (unused) images</span>
</span></span><span class="line"><span class="cl">docker image prune
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Remove ALL unused images (dangerous if you need old ones)</span>
</span></span><span class="line"><span class="cl">docker image prune -a</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li><strong>Finding images:</strong> Docker Hub is the default registry. If you run <code>docker pull nginx</code>, it comes from <code>hub.docker.com/library/nginx</code>.</li>
<li><code>hello-world</code> is the standard first test — it prints a confirmation message if Docker is working correctly.</li>
<li>Use official images when possible (they’re verified and maintained). Community images can be great, but check the <code>Dockerfile</code> and last update date.</li>
<li>Pin to a specific tag (e.g. <code>postgres:15.2</code>) — don’t rely on <code>latest</code> in production.</li>
<li><code>docker history</code> helps spot bloated layers when an image is huge.</li>
<li><code>docker tag</code> doesn’t duplicate data — it just adds a new label for an existing image.</li>
<li>Use <code>docker rmi</code> to clean up, but remember: if a container still depends on the image, it won’t be removed.</li>
<li>Keep your Dockerfiles minimal. Each <code>RUN</code> command adds a new layer and increases size.</li>
<li><code>docker image prune -a</code> removes <strong>all unused images</strong> — only run if you’re sure what’s safe.</li>
</ul>

  </div>
</details>

<hr>
<h3 id="volumes-and-persistence"><span style="color:#CC0000;">Volumes and Persistence</span></h3>
<p>By default, containers are <strong>ephemeral</strong> — remove a container, and its filesystem is gone.<br>
That’s fine for testing, but not for databases, media servers, or anything with state.<br>
<strong>Volumes</strong> (and bind mounts) solve this by storing data outside the container’s lifecycle,<br>
so you can rebuild or upgrade without losing important files.</p>
<ul>
<li><em>Named Volumes</em> → managed by Docker (<code>/var/lib/docker/volumes/...</code>)</li>
<li><em>Bind Mounts</em> → link a host path into the container (<code>/home/user/data:/app/data</code>)</li>
</ul>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <p>💡 <strong>Tip:</strong> Use volumes for long-term data (databases, media libraries). Use bind mounts when you need direct access from the host.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Managing Volumes ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># List all volumes</span>
</span></span><span class="line"><span class="cl">docker volume ls
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create a named volume</span>
</span></span><span class="line"><span class="cl">docker volume create mydata
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Inspect a volume (shows host path, usage)</span>
</span></span><span class="line"><span class="cl">docker volume inspect mydata
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Remove a volume (careful: data will be deleted)</span>
</span></span><span class="line"><span class="cl">docker volume rm mydata
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Using Volumes in Containers ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run postgres with a named volume</span>
</span></span><span class="line"><span class="cl">docker run -d --name pgtest <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -e <span class="nv">POSTGRES_PASSWORD</span><span class="o">=</span>secret <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -v pgdata:/var/lib/postgresql/data <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  postgres:15
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run nginx with a host bind mount</span>
</span></span><span class="line"><span class="cl">docker run -d --name webtest <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -v /home/user/html:/usr/share/nginx/html:ro <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -p 8080:80 <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  nginx:latest
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run jellyfin with config + media mounted</span>
</span></span><span class="line"><span class="cl">docker run -d --name jellyfin <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -v jellyfin-config:/config <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -v /mnt/media:/media <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -p 8096:8096 <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  jellyfin/jellyfin
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Copying and Backing Up Data ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Copy data from a volume to host (via a helper container)</span>
</span></span><span class="line"><span class="cl">docker run --rm -v pgdata:/data -v <span class="k">$(</span><span class="nb">pwd</span><span class="k">)</span>:/backup busybox tar czf /backup/pgdata.tar.gz -C /data .
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Copy files between host and container</span>
</span></span><span class="line"><span class="cl">docker cp &lt;container_name&gt;:/path/in/container /path/on/host
</span></span><span class="line"><span class="cl">docker cp /path/on/host &lt;container_name&gt;:/path/in/container
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Cleanup ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Remove all unused volumes (dangling, not attached to containers)</span>
</span></span><span class="line"><span class="cl">docker volume prune</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>Named volumes are best for portability — Docker manages them and they survive container removal.</li>
<li>Bind mounts are direct host paths; they’re flexible but can break if paths don’t exist or permissions mismatch.</li>
<li>Always mount database directories (e.g. <code>/var/lib/postgresql/data</code> for Postgres, <code>/config</code> for Jellyfin).</li>
<li>Use <code>docker cp</code> for quick one-off transfers; for backups, use <code>tar</code> inside a helper container.</li>
<li><code>docker volume prune</code> only deletes <strong>unused</strong> volumes — safe, but double-check before running in production.</li>
<li>Pro tip: version-control your <code>docker run</code> or <code>docker-compose.yml</code> with explicit volume mounts so you never lose track of data locations.</li>
</ul>

  </div>
</details>

<hr>
<h3 id="networking"><span style="color:#CC0000;">Networking</span></h3>
<p>Networking determines how containers talk to each other and to the outside world.<br>
By default, Docker puts containers on an isolated <strong>bridge</strong> network, but you can create your own or attach containers to the host network directly.<br>
For homelabs, this often means setting up reverse proxies (like <strong>NGINX Proxy Manager</strong> or <strong>Traefik</strong>) so you can run many services behind a single IP with SSL.</p>
<p><strong>Key network modes:</strong></p>
<ul>
<li><em>Bridge (default):</em> Containers get an internal IP, NAT-ed through the host.</li>
<li><em>Custom bridge:</em> Like bridge, but with user-defined DNS and container-to-container resolution.</li>
<li><em>Host:</em> Container shares the host network stack (fast, but less isolation; Linux only).</li>
<li><em>Macvlan:</em> Gives a container its own IP on your LAN (advanced homelab setups).</li>
</ul>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <p>💡 <strong>Tip:</strong> For multi-container apps, always prefer a custom bridge network. Containers can reach each other by name, and it avoids random port collisions.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Inspecting Networks ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># List all Docker networks</span>
</span></span><span class="line"><span class="cl">docker network ls
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Inspect a network (shows connected containers, subnets, drivers)</span>
</span></span><span class="line"><span class="cl">docker network inspect bridge
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Connecting Containers ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create a custom bridge network</span>
</span></span><span class="line"><span class="cl">docker network create mynet
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run containers on the same custom network</span>
</span></span><span class="line"><span class="cl">docker run -d --name web --network mynet nginx
</span></span><span class="line"><span class="cl">docker run -d --name app --network mynet busybox sleep <span class="m">3600</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Connect an existing container to another network</span>
</span></span><span class="line"><span class="cl">docker network connect mynet &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Disconnect a container from a network</span>
</span></span><span class="line"><span class="cl">docker network disconnect mynet &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Publishing Ports ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Map host port 8080 -&gt; container port 80</span>
</span></span><span class="line"><span class="cl">docker run -d -p 8080:80 nginx
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Map multiple host ports to different container ports</span>
</span></span><span class="line"><span class="cl">docker run -d -p 8080:80 -p 8443:443 nginx
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Advanced Networking ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run a container with host networking (Linux only)</span>
</span></span><span class="line"><span class="cl">docker run -d --network host nginx
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run a container with macvlan (container gets its own LAN IP)</span>
</span></span><span class="line"><span class="cl">docker network create -d macvlan <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --subnet<span class="o">=</span>192.168.1.0/24 <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --gateway<span class="o">=</span>192.168.1.1 <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -o <span class="nv">parent</span><span class="o">=</span>eth0 macnet
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">docker run -d --network macnet --ip 192.168.1.50 nginx
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Reverse Proxies (Homelab Example) ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># NGINX Proxy Manager example (manages SSL + hostnames)</span>
</span></span><span class="line"><span class="cl">docker run -d --name npm <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -v npm-data:/data <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -v npm-ssl:/etc/letsencrypt <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -p 80:80 -p 81:81 -p 443:443 <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  jc21/nginx-proxy-manager
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Traefik example (auto-discovers containers by labels)</span>
</span></span><span class="line"><span class="cl">docker run -d --name traefik <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -v /var/run/docker.sock:/var/run/docker.sock <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -p 80:80 -p 443:443 <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  traefik:v2.10
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Troubleshooting ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show which ports a container is exposing/mapping</span>
</span></span><span class="line"><span class="cl">docker port &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Test connectivity between containers</span>
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> &lt;container_name&gt; ping &lt;other_container_name&gt;
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> &lt;container_name&gt; nslookup &lt;other_container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Inspect network traffic/connections inside a container</span>
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> &lt;container_name&gt; netstat -tlnp</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>The <strong>default bridge</strong> network doesn’t allow DNS-based container name resolution; use a <strong>custom bridge</strong> for that.</li>
<li>With <code>-p host:container</code>, host ports must be free; otherwise the container won’t start.</li>
<li><code>--network host</code> is simple but removes isolation and only works on Linux.</li>
<li><strong>Macvlan</strong> lets a container appear as a separate device on your LAN (great for media servers that need their own IP).</li>
<li>Reverse proxies like <strong>NGINX Proxy Manager</strong> or <strong>Traefik</strong> let you host many services behind one IP/SSL cert — perfect for homelabs.</li>
<li>Always secure reverse proxies with SSL certs (Let’s Encrypt support is built into both NPM and Traefik).</li>
<li><code>docker port</code> is a fast way to verify published ports without running <code>inspect</code>.</li>
<li>Use <code>ping</code> and <code>nslookup</code> from inside a container to test service discovery on custom networks.</li>
<li><code>netstat</code> is invaluable for checking what ports a containerized service is really listening on.</li>
<li>Compose files (<code>docker-compose.yml</code>) can define networks, making multi-service setups easier to reproduce.</li>
</ul>

  </div>
</details>

<hr>
<h3 id="cleanup-and-maintenance"><span style="color:#CC0000;">Cleanup and Maintenance</span></h3>
<p>Over time, Docker hosts collect cruft: old images, stopped containers, unused volumes, and dangling networks.<br>
Left unchecked, these can eat gigabytes of space and slow down operations.<br>
Docker provides commands to safely prune unused resources and check disk usage so your host stays lean and responsive.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <br>
<p>💡 <strong>Tip:</strong> Always run a system usage check (<code>docker system df</code>) before pruning. It shows what’s taking up space and what will be affected.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === System Usage Overview ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show disk usage by images, containers, and volumes</span>
</span></span><span class="line"><span class="cl">docker system df
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show detailed disk usage (per image/layer)</span>
</span></span><span class="line"><span class="cl">docker system df -v
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Pruning Resources ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Remove stopped containers, unused networks, dangling images</span>
</span></span><span class="line"><span class="cl">docker system prune
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Prune EVERYTHING (containers, images, networks, volumes not in use)</span>
</span></span><span class="line"><span class="cl">docker system prune -a --volumes
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Removing Containers ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Remove a stopped container</span>
</span></span><span class="line"><span class="cl">docker rm &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Remove ALL stopped containers</span>
</span></span><span class="line"><span class="cl">docker container prune
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Removing Images ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Remove a specific image</span>
</span></span><span class="line"><span class="cl">docker rmi &lt;image_id&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Remove dangling (unnamed) images</span>
</span></span><span class="line"><span class="cl">docker image prune
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Remove ALL unused images (careful!)</span>
</span></span><span class="line"><span class="cl">docker image prune -a
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Removing Volumes ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># List volumes</span>
</span></span><span class="line"><span class="cl">docker volume ls
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Remove a specific volume</span>
</span></span><span class="line"><span class="cl">docker volume rm &lt;volume_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Remove all unused volumes</span>
</span></span><span class="line"><span class="cl">docker volume prune
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Removing Networks ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># List networks</span>
</span></span><span class="line"><span class="cl">docker network ls
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Remove a specific network</span>
</span></span><span class="line"><span class="cl">docker network rm &lt;network_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Remove all unused networks</span>
</span></span><span class="line"><span class="cl">docker network prune</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li><code>docker system df</code> is your friend — always check usage before pruning.</li>
<li><code>docker system prune</code> is relatively safe (it won’t touch volumes or images in use).</li>
<li>Adding <code>-a --volumes</code> will nuke <strong>all unused images and volumes</strong> — only use if you’re sure.</li>
<li><code>docker container prune</code> only removes <strong>stopped</strong> containers, not running ones.</li>
<li><code>docker image prune</code> removes dangling layers (<code>&lt;none&gt;</code> tags) — run it often to reclaim space.</li>
<li>Volumes are persistent by design; pruning them is irreversible. Back up important volumes before cleanup.</li>
<li>Networks usually don’t take much space, but unused ones can clutter output.</li>
<li>Pro tip: automate periodic cleanup with <code>cron</code> or systemd timers, but keep backups of critical data.</li>
</ul>

  </div>
</details>

<hr>
<h3 id="copying-data-in-and-out"><span style="color:#CC0000;">Copying Data In and Out</span></h3>
<p>Sometimes you need to move files between the host and a container — for quick config edits, pulling logs, or testing scripts.<br>
For <strong>long-term persistence</strong>, volumes are the right answer. But when you just need to push/pull files on the fly, <code>docker cp</code> and <code>docker exec</code> are your tools.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <br>
<p>💡 <strong>Tip:</strong> Always verify container names/IDs first:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker ps --format <span class="s2">&#34;table {{.Names}}\t{{.ID}}\t{{.Status}}&#34;</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Host → Container ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Copy a file from host to container</span>
</span></span><span class="line"><span class="cl">docker cp ./config.yml &lt;container_name&gt;:/etc/app/config.yml
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Copy a directory from host to container</span>
</span></span><span class="line"><span class="cl">docker cp ./scripts &lt;container_name&gt;:/usr/local/bin/
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Container → Host ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Copy a file from container to host</span>
</span></span><span class="line"><span class="cl">docker cp &lt;container_name&gt;:/var/log/app.log ./app.log
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Copy a directory from container to host</span>
</span></span><span class="line"><span class="cl">docker cp &lt;container_name&gt;:/data ./backup-data
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Container ↔ Container (via host) ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Copy from one container to another (two-step)</span>
</span></span><span class="line"><span class="cl">docker cp &lt;container_A&gt;:/file ./file
</span></span><span class="line"><span class="cl">docker cp ./file &lt;container_B&gt;:/file
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Tar + Exec Trick (large or many files) ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Export a directory as tar and extract on host</span>
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> &lt;container_name&gt; tar czf - /data &gt; data.tar.gz
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Import tar archive into container</span>
</span></span><span class="line"><span class="cl">cat data.tar.gz <span class="p">|</span> docker <span class="nb">exec</span> -i &lt;container_name&gt; tar xzf - -C /</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li><code>docker cp</code> works like <code>scp</code>: <code>source → destination</code>. Paths must be absolute inside the container.</li>
<li>Permissions inside the container may differ — root-owned files may need <code>sudo</code> or careful mapping.</li>
<li>Copying large datasets is slower with <code>docker cp</code>; for big/active data, use <strong>volumes or bind mounts</strong> instead.</li>
<li>The tar+exec trick is faster for large directories or when preserving permissions is critical.</li>
<li>For multi-service stacks, prefer <code>docker-compose</code> volumes to manage data consistently across containers.</li>
</ul>

  </div>
</details>

<hr>
<h3 id="exporting-and-importing"><span style="color:#CC0000;">Exporting and Importing</span></h3>
<p>For migrations and backups, Docker lets you either <strong>export containers</strong> or <strong>save/load images</strong>.</p>
<ul>
<li><strong>Export/Import</strong> works on containers: it snapshots the container’s filesystem (no history, no image metadata).</li>
<li><strong>Save/Load</strong> works on images: it preserves layers, tags, and metadata, making it better for moving images between hosts.</li>
</ul>
<p>Use <strong>export/import</strong> when you want a quick one-off copy of a container’s state.<br>
Use <strong>save/load</strong> when you want to migrate images or move them to another registry/host.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <br>
<p>💡 <strong>Tip:</strong> For long-term data, back up <strong>volumes</strong> separately — container exports won’t capture external volumes.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Exporting and Importing Containers ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Export a container’s filesystem as a tar archive</span>
</span></span><span class="line"><span class="cl">docker <span class="nb">export</span> &lt;container_name&gt; &gt; container.tar
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Import an exported container as a new image</span>
</span></span><span class="line"><span class="cl">docker import container.tar newimage:latest
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Import directly from a URL or stdin</span>
</span></span><span class="line"><span class="cl">curl http://example.com/container.tar <span class="p">|</span> docker import - myimage:tag
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Saving and Loading Images ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Save an image (with all layers and tags) to a tar archive</span>
</span></span><span class="line"><span class="cl">docker save -o myimage.tar &lt;image_name&gt;:&lt;tag&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Load an image from a tar archive</span>
</span></span><span class="line"><span class="cl">docker load -i myimage.tar
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Practical Example: Migrating Between Hosts ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># On source host: save an image</span>
</span></span><span class="line"><span class="cl">docker save -o nginx.tar nginx:1.27
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Copy file to new host (scp, rsync, USB, etc.)</span>
</span></span><span class="line"><span class="cl">scp nginx.tar user@newhost:/tmp/
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># On destination host: load image</span>
</span></span><span class="line"><span class="cl">docker load -i /tmp/nginx.tar
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run it again on the new host</span>
</span></span><span class="line"><span class="cl">docker run -d -p 8080:80 nginx:1.27</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li><code>docker export</code> flattens a container into a single tarball — history, environment variables, and build metadata are lost.</li>
<li><code>docker import</code> creates a new image from that tarball, but without tags/history it’s more like a snapshot.</li>
<li><code>docker save</code> preserves tags, layers, and history — use this for migrating or archiving images.</li>
<li><code>docker load</code> restores an image exactly as it was, ready to run again.</li>
<li>Neither export nor save captures <strong>volumes</strong> — always back those up separately (<code>docker volume</code> or <code>tar</code> tricks).</li>
<li>For multi-container stacks, <code>docker compose</code> plus volume backups is usually the better migration path.</li>
</ul>

  </div>
</details>

<hr>
<h3 id="diagnostics-and-troubleshooting"><span style="color:#CC0000;">Diagnostics and Troubleshooting</span></h3>
<p>When containers misbehave, Docker gives you tools to peek under the hood and figure out what’s going wrong.<br>
Whether it’s a crash loop, a port conflict, or networking issues, these commands help you diagnose problems without guesswork.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <br>
<p>💡 <strong>Tip:</strong> Start with <code>docker ps -a</code> and <code>docker logs &lt;container&gt;</code> — they solve 80% of issues before you dive deeper.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Container Health and Status ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show container status and restart counts</span>
</span></span><span class="line"><span class="cl">docker ps -a --format <span class="s2">&#34;table {{.Names}}\t{{.Status}}\t{{.State}}\t{{.Ports}}&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Inspect restart count (useful for crash loops)</span>
</span></span><span class="line"><span class="cl">docker inspect -f <span class="s1">&#39;{{.RestartCount}}&#39;</span> &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Inspect container healthcheck status</span>
</span></span><span class="line"><span class="cl">docker inspect -f <span class="s1">&#39;{{.State.Health.Status}}&#39;</span> &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Logs and Events ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show container logs</span>
</span></span><span class="line"><span class="cl">docker logs &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Follow logs in real-time</span>
</span></span><span class="line"><span class="cl">docker logs -f &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show Docker daemon events (global activity)</span>
</span></span><span class="line"><span class="cl">docker events
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Inspecting Deep Details ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Inspect container details (JSON: env vars, mounts, networks)</span>
</span></span><span class="line"><span class="cl">docker inspect &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Inspect a specific property (e.g., IP address)</span>
</span></span><span class="line"><span class="cl">docker inspect -f <span class="s1">&#39;{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}&#39;</span> &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># List environment variables for a container</span>
</span></span><span class="line"><span class="cl">docker inspect -f <span class="s1">&#39;{{.Config.Env}}&#39;</span> &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Resource and Process Debugging ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show live resource usage (CPU, memory, I/O)</span>
</span></span><span class="line"><span class="cl">docker stats &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show processes running inside a container</span>
</span></span><span class="line"><span class="cl">docker top &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run ps inside a container (for more detail)</span>
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> &lt;container_name&gt; ps aux
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Networking Issues ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show which ports a container is exposing</span>
</span></span><span class="line"><span class="cl">docker port &lt;container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Test connectivity to another container</span>
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> &lt;container_name&gt; ping &lt;other_container_name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># DNS resolution test inside container</span>
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> &lt;container_name&gt; nslookup &lt;other_container_name&gt;</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li><code>docker ps -a</code> shows stopped containers — always check here if something exited unexpectedly.</li>
<li>Crash loops? Look at the <strong>restart count</strong> (<code>docker inspect -f '{{.RestartCount}}'</code>). If it’s climbing, check logs for the root cause.</li>
<li>Healthchecks (if defined in the image) will show as <code>healthy</code>, <code>unhealthy</code>, or <code>starting</code>.</li>
<li><code>docker events</code> is noisy but invaluable for catching container start/stop in real time.</li>
<li>Use <code>docker inspect</code> with Go templates to extract just what you need (IP, env vars, restart count).</li>
<li><code>docker stats</code> helps spot containers hogging CPU or memory.</li>
<li>Networking issues are often DNS-related — custom networks allow name resolution, default bridge does not.</li>
<li>Port conflicts are common: if a container won’t start with <code>-p 80:80</code>, check if the host already has something bound to that port.</li>
<li>For multi-service apps, <code>docker compose ps</code> and <code>docker compose logs</code> give you a stack-wide view.</li>
</ul>

  </div>
</details>

<hr>
<h2 id="docker-compose-basics">Docker Compose Basics</h2>
<figure style="text-align:center; margin: 1em auto;">
  <img src="lich.png" 
       alt="a pixel art lich wearing a crown and holding a staff with a green gem" 
       style="display:block; margin:0 auto; width:min(100%, 390px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Mastering Docker doesn't have to be a boss battle
  </figcaption>
</figure>
<p>Managing one container at a time works, but most real apps involve <strong>multiple services</strong> — databases, web servers, caching layers, proxies.</p>
<p><strong>Docker Compose</strong> makes this practical by letting you define and run multi-container applications with a single YAML file.<br>
Instead of juggling long <code>docker run ...</code> commands for each container, you declare them once in <code>docker-compose.yml</code> and bring the whole stack up with one command:  <code>docker compose up -d</code></p>
<div style="clear:both"></div>
<p>With Compose, adding a database, cache, or proxy isn’t extra terminal clutter — it’s just another service block in the YAML.<br>
You’ll often see guides and project docs include a <code>docker-compose.yml</code> with multiple containers preconfigured, because it’s the easiest way to share and reproduce complex setups.</p>
<hr>
<p><strong>How it works:</strong></p>
<ul>
<li>You define services (containers) in <code>docker-compose.yml</code>.</li>
<li>Compose automatically creates a dedicated network so containers can talk by name.</li>
<li>Volumes and environment variables are declared in one place for consistency.</li>
<li>Stacks can be version-controlled and shared across machines.</li>
</ul>
<hr>
<p><strong>Example: Single Service</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;3.9&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">web</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">nginx:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;8080:80&#34;</span><span class="w">
</span></span></span></code></pre></div><p>Bring it up:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose up -d
</span></span></code></pre></div><p><strong>Where to put <code>docker-compose.yml</code>:</strong><br>
Save the file in a project folder (for example <code>~/codelab/infra-stacks/myapp/</code>).<br>
The working directory is important: when you run <code>docker compose up</code>, Compose looks for <code>docker-compose.yml</code> in the <strong>current directory</strong> by default.<br>
If your YAML file lives elsewhere, you can point Compose at it with <code>-f</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p ~/codelab/infra-stacks/myapp
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ~/codelab/infra-stacks/myapp
</span></span><span class="line"><span class="cl">nano docker-compose.yml   <span class="c1"># paste the config here</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run from the same directory</span>
</span></span><span class="line"><span class="cl">docker compose up -d
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Or specify the file explicitly</span>
</span></span><span class="line"><span class="cl">docker compose -f ~/codelab/infra-stacks/myapp/docker-compose.yml up -d
</span></span></code></pre></div><hr>
<p><strong>Example: Multi-Service (Web + Database)</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;3.9&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">db</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">postgres:15</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">secret</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">pgdata:/var/lib/postgresql/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">web</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">nginx:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;8080:80&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pgdata</span><span class="p">:</span><span class="w">
</span></span></span></code></pre></div><hr>
<p><strong>.env Files for Configuration</strong><br>
Compose automatically reads a <code>.env</code> file in the same directory as <code>docker-compose.yml</code>.<br>
This is a better way to handle secrets and environment settings:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># .env file</span>
</span></span><span class="line"><span class="cl"><span class="nv">POSTGRES_PASSWORD</span><span class="o">=</span>secret
</span></span><span class="line"><span class="cl"><span class="nv">DB_VERSION</span><span class="o">=</span><span class="m">15</span>
</span></span></code></pre></div><p>Then you can reference the env variables in <code>docker-compose.yml</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;3.9&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">db</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">postgres:${DB_VERSION}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">${POSTGRES_PASSWORD}</span><span class="w">
</span></span></span></code></pre></div><hr>
<p><strong>Essential Compose Commands</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Essential Compose Commands ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Bring stack up in background</span>
</span></span><span class="line"><span class="cl">docker compose up -d
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Stop and remove containers (keeps volumes)</span>
</span></span><span class="line"><span class="cl">docker compose down
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Stop, remove containers AND volumes (destructive!)</span>
</span></span><span class="line"><span class="cl">docker compose down -v
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># View logs from all services</span>
</span></span><span class="line"><span class="cl">docker compose logs -f
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Restart a specific service</span>
</span></span><span class="line"><span class="cl">docker compose restart web
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Rebuild images before starting</span>
</span></span><span class="line"><span class="cl">docker compose up -d --build
</span></span></code></pre></div><hr>
<p><strong>Docs and Resources:</strong></p>
<ul>
<li><a href="https://docs.docker.com/compose/">Docker Compose Overview</a></li>
<li><a href="https://docs.docker.com/compose/compose-file/">Compose File Reference</a></li>
<li><a href="https://docs.docker.com/engine/reference/commandline/compose/">Compose CLI Command Reference</a></li>
</ul>
<p><strong>Notes:</strong></p>
<ul>
<li>
<p>Compose integrates with the same Docker engine — no separate install needed on modern Docker (v20+).</p>
</li>
<li>
<p>Older systems may need the standalone <code>docker-compose</code> binary.</p>
</li>
<li>
<p>For homelabs, Compose makes backups, restores, and migrations easier (just copy the YAML + volumes).</p>
</li>
<li>
<p>You can scale services with one command:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose up --scale <span class="nv">web</span><span class="o">=</span><span class="m">3</span> -d
</span></span></code></pre></div><p>This runs three copies of the <code>web</code> service (<code>web_1</code>, <code>web_2</code>, <code>web_3</code>) using the same definition from your Compose file. Other containers on the same network can talk to <code>web</code> and Docker will spread requests across them, though exposing ports usually requires a proxy to handle load-balancing.</p>
</li>
<li>
<p>Always pin image tags in Compose files (e.g. <code>postgres:15.2</code>) — don’t rely on <code>latest</code>.</p>
</li>
<li>
<p>Track <code>docker-compose.yml</code> in Git, but exclude volume data — it belongs on disk, not in version control.</p>
</li>
</ul>
<hr>
<h2 id="docker-desktop">Docker Desktop</h2>
<p>For macOS and Windows, <strong>Docker Desktop</strong> is the simplest way to get started.<br>
It bundles the Docker Engine, CLI, and a GUI dashboard in one installer.</p>
<ul>
<li><a href="https://docs.docker.com/desktop/install/mac/">Docker Desktop for Mac</a></li>
<li><a href="https://docs.docker.com/desktop/install/windows/">Docker Desktop for Windows</a></li>
</ul>
<figure style="text-align:center; margin: 1em auto;">
  <img src="docker-desktop.png" 
       alt="a screenshot of Docker Desktop displaying the User Interface" 
       style="display:block; margin:0 auto; width:min(100%, 80000px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Docker Desktop is an easy but resource heavy way of managing your containers for those who prefer a GUI. 
  </figcaption>
</figure>
<p><strong>Notes:</strong></p>
<ul>
<li>Docker Desktop is perfect for local dev/testing.</li>
<li>On Linux, install Docker Engine directly instead (Desktop isn’t needed).</li>
<li>Resource usage can be heavy — tweak CPU/RAM under <em>Preferences → Resources</em>.</li>
<li>Once comfortable, you can move stacks to a server (like <code>gir.darkstar.home</code>) using <code>docker compose</code> or Portainer.</li>
</ul>
<hr>
<h2 id="portainer-setup">Portainer Setup</h2>
<p><strong>Portainer</strong> is a lightweight web UI that makes Docker approachable — it’s the first thing I install whenever I set up Docker.
Think of it as a control panel: manage containers, stacks, networks, and volumes without memorizing CLI flags.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="portainer-ui.png" 
       alt="a screenshot of the Portainer User Interface" 
       style="display:block; margin:0 auto; width:min(100%, 1000px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Portainer is a great web UI for managing your Docker environments
  </figcaption>
</figure>
<p><strong>Install Portainer CE:</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker volume create portainer_data
</span></span><span class="line"><span class="cl">docker run -d <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -p 8000:8000 -p 9000:9000 -p 9443:9443 <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --name<span class="o">=</span>portainer <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --restart<span class="o">=</span>always <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -v /var/run/docker.sock:/var/run/docker.sock <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -v portainer_data:/data <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  portainer/portainer-ce:latest
</span></span></code></pre></div><p><strong>First Steps:</strong></p>
<ol>
<li>Visit <code>https://&lt;host-ip&gt;:9443</code> in a browser.</li>
<li>Create the admin user.</li>
<li>Connect to the <strong>local environment</strong>.</li>
</ol>
<p><strong>Deploying a Stack:</strong></p>
<ul>
<li>In Portainer → <em>Stacks</em> → <em>Add Stack</em>.</li>
<li>Paste your <code>docker-compose.yml</code> (e.g. the nginx + postgres example).</li>
<li>Hit <strong>Deploy the stack</strong>.</li>
</ul>
<figure style="text-align:center; margin: 1em auto;">
  <img src="portainer-editor.png" 
       alt="screenshot of portainer stack editor" 
       style="display:block; margin:0 auto; width:min(100%, 1000px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    You can compose and edit the yml for your stack in a nice, polished ui
  </figcaption>
</figure>
<br>
<figure style="text-align:center; margin: 1em auto;">
  <img src="portainer-deploying.png" 
       alt="screenshot of portainer deployment in progress button" 
       style="display:block; margin:0 auto; width:min(100%, 400px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Click the deploy button and it either reports an error or builds the compose stack
  </figcaption>
</figure>
<br>
<figure style="text-align:center; margin: 1em auto;">
  <img src="portainer-stack-details.png" 
       alt="screenshot of portainers details view of our new stack" 
       style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Once deployed, you can manage lifecycle, view logs, exec a shell, etc. 
  </figcaption>
</figure>
<p><span class="tag green">Pro-Tip:</span> Portainer shows the YAML it generates. Copy that into Git (like <code>~/codelab/infra-stacks/</code>) to version your setup.</p>
<p>Links:</p>
<ul>
<li><a href="https://docs.portainer.io/">Portainer Docs</a></li>
<li><a href="https://hub.docker.com/r/portainer/portainer-ce">Portainer CE on Docker Hub</a></li>
</ul>
<hr>
<h2 id="dockerfiles-and-making-your-own-containers">Dockerfiles and making your own containers</h2>
<p>It’s one thing to understand that <em>“a container is a little computer inside your computer”</em> — but it really clicks once you build one yourself. You don’t need Kubernetes or a whole stack of services to see how it works. It can be as simple as combining a tiny script and a Dockerfile.</p>
<p>The idea is simple:</p>
<ul>
<li><strong>A script or program</strong> you want to run.</li>
<li><strong>A Dockerfile</strong> — think of it as a lightweight provisioning script. Each line makes a small, understandable change to a base OS image:
<ul>
<li>pick a starting point (<code>FROM python:3.12-slim</code>)</li>
<li>copy in files (<code>COPY smurfify.py .</code>)</li>
<li>install dependencies (<code>RUN pip install -r requirements.txt</code> or <code>apt-get install curl</code>)</li>
<li>set the default command (<code>ENTRYPOINT [...]</code>)</li>
</ul>
</li>
<li><strong><code>docker build</code></strong> to turn that recipe into an image.</li>
<li><strong><code>docker run</code></strong> to spin up a container from the image.</li>
</ul>
<p>That’s it: <em>script → image → container</em>. Once you’ve seen that loop, the rest of Docker makes sense.</p>
<p>The beauty of this model is that you ship a minimal system tailored to your code, instead of hoping it behaves on whatever messy production environment it lands in. The old excuse <em>“it works on my machine”</em> becomes a feature — because with Docker you’re packaging your machine with the code. Unlike a full virtual machine, containers don’t need their own kernel or OS image; they just add the few layers your app requires. That makes them lightweight, portable, and consistent no matter where they run.</p>
<p>If you’d like to see this process step by step, I put together a companion guide where we take a small script (<a href="/posts/smurfify/">smurfify.py</a>) and wrap it in Docker:</p>
<p style="text-align:left;">
  <a href="/posts/dockerize-a-script/" class="button">🧾 Dockerize a Python Script</a>
</p>
<hr>
<h2 id="-essential-documentation--resources">📚 Essential Documentation &amp; Resources</h2>
<h3 id="official-documentation">Official Documentation</h3>
<ul>
<li><strong>Docker Docs</strong>: <a href="https://docs.docker.com/">https://docs.docker.com/</a></li>
<li><strong>Dockerfile Reference</strong>: <a href="https://docs.docker.com/engine/reference/builder/">https://docs.docker.com/engine/reference/builder/</a></li>
<li><strong>Docker Compose</strong>: <a href="https://docs.docker.com/compose/">https://docs.docker.com/compose/</a></li>
<li><strong>Docker Hub</strong>: <a href="https://hub.docker.com/">https://hub.docker.com/</a></li>
</ul>
<h3 id="best-practices-guides">Best Practices Guides</h3>
<ul>
<li><strong>Docker Best Practices</strong>: <a href="https://docs.docker.com/develop/dev-best-practices/">https://docs.docker.com/develop/dev-best-practices/</a></li>
<li><strong>Security Best Practices</strong>: <a href="https://docs.docker.com/engine/security/">https://docs.docker.com/engine/security/</a></li>
<li><strong>Production Deployment</strong>: <a href="https://docs.docker.com/engine/swarm/">https://docs.docker.com/engine/swarm/</a></li>
</ul>
<h3 id="community-resources">Community Resources</h3>
<ul>
<li><strong>Docker Community</strong>: <a href="https://www.docker.com/community/">https://www.docker.com/community/</a></li>
<li><strong>Stack Overflow Docker Tag</strong>: <a href="https://stackoverflow.com/questions/tagged/docker">https://stackoverflow.com/questions/tagged/docker</a></li>
<li><strong>r/docker</strong>: <a href="https://reddit.com/r/docker">https://reddit.com/r/docker</a></li>
</ul>
<hr>
<h2 id="conclusion">Conclusion</h2>
<figure style="text-align:center; margin: 1em auto;">
  <img src="treasure.png" 
       alt="a pixel art treasure chest" 
       style="display:block; margin:0 auto; width:min(100%, 400px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    If you've made it this far, you deserve some loot and XP
  </figcaption>
</figure>
<br>
<p>I hope you&rsquo;ve found this little Docker Grimoire useful. With a few concepts, the commands and some examples, you&rsquo;ll be dockering with the best of them in no time.</p>
<p>If you have any tips, tricks, or feedback please feel free to reach out:  <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Signals Intelligence</title>
      <link>https://adminjitsu.com/posts/signals-intelligence/</link>
      <pubDate>Wed, 03 Sep 2025 14:44:57 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/signals-intelligence/</guid>
      <description>Learn how Unix signals work, from kill and pkill to bash traps and Python signal handlers. A practical sysadmin guide with examples, tips, and tricks for mastering inter-process communication.</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<p>Unix signals are one of the oldest forms of inter-process communication. Born in early UNIX at Bell Labs (1970s), they were designed as a simple way for the kernel to tap a process on the shoulder. Unlike pipes or sockets, signals don’t carry payloads — just a notification.</p>
<p>They remain the universal way the OS tells programs when it’s time to stop, reload, or respond to user actions. Servers, daemons, shells, even containers all depend on them. Think of signals as lifecycle control, while other IPC mechanisms handle the heavy lifting.</p>
<p><span class="tag purple">Fun Fact:</span> The very first &ldquo;signals&rdquo; were just conventions that most but not all programs hard-wired for job control (Ctrl+C, Ctrl+Z). Everything else—portability, universality, reloads, graceful quits—are clever conventions layered on later.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="doom-buttons.jpg" 
       alt="closeup of 1970s illuminated buttons on a computer station" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <i>Ctrl-C, Ctrl-C... why isn't it responding? </i><br>
    Still from WarGames (1983, MGM). Used under fair use for educational purposes.
  </figcaption>
</figure>
<p>Unix IPC signals are easy to use but there are a lot of exceptions and footnotes. I tried to make the following as clear as possible without going full-on manual.</p>
<h2 id="signals-vs-other-unix-land-ipc">Signals vs other Unix-land IPC</h2>
<blockquote>
<p><em>The more they over-think the plumbing the easier it is to stop up the drain.</em> &ndash;anonymous fortune</p></blockquote>
<ul>
<li>
<p><strong>Signals</strong> are <em>almost all</em> one-bit notifications from the kernel. They don’t carry payloads, just a type (e.g. <code>SIGTERM</code>), and they interrupt a process asynchronously. The exception being POSIX real-time signals like <code>sigqueue</code> which can carry a small int/pointer. Use them for lifecycle events (quit, reload, stop) and simple control.</p>
</li>
<li>
<p><strong>Pipes</strong> (<code>man 7 pipe</code>) are byte streams between processes, usually set up with <code>|</code> in the shell. Data flows in one direction, buffered by the kernel (half-duplex). Bidirectional requires two pipes. Think of <code>ls | grep foo</code> — that’s a pipe doing work. Pipes are synchronous: the reader blocks until there’s data. There are also <strong>named pipes</strong> or FIFOs, made with <code>mkfifo</code>, which persist in the filesystem and let unrelated processes talk. Same half-duplex rule applies.</p>
</li>
<li>
<p><strong>Sockets</strong> (<code>man 7 socket</code>) generalize pipes into a full-duplex/bidirectional, network-style interface. They can talk locally (UNIX domain sockets) or across machines (TCP/UDP). Most client-server software is built on sockets.</p>
</li>
<li>
<p><strong>Message Queues &amp; Shared Memory</strong> (<code>man 7 mq_overview</code>, <code>man 7 shm_overview</code>) are heavier IPC mechanisms. They allow structured data passing or even memory regions mapped between processes. Used when performance matters or when large state must be shared.</p>
</li>
<li>
<p><strong>DBus</strong> (<a href="https://www.freedesktop.org/wiki/Software/dbus/">freedesktop.org</a>) is a high-level message bus, running in user space via a broker daemon. Think of it as an <em>application-layer IPC system</em>: processes send structured requests and events through a central hub. Desktop environments and system services (NetworkManager, systemd, GNOME) rely on it. DBus “signals” are logical user-space events, not kernel ones.</p>
</li>
</ul>
<p><span class="tag blue">Pro-Tip:</span> If you’re just controlling a process, signals are usually enough. If you need to <em>talk</em> to it, look at sockets or DBus.</p>
<h2 id="usage-cheatsheet">Usage Cheatsheet</h2>
<p>Common signals you’ll run into:</p>
<table>
  <thead>
      <tr>
          <th>Signal</th>
          <th>Trigger / Use case</th>
          <th>Default action</th>
          <th>Notes</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>SIGINT</code></td>
          <td>Ctrl+C in a terminal</td>
          <td>Terminate</td>
          <td>Apps can catch to clean up.</td>
      </tr>
      <tr>
          <td><code>SIGTERM</code></td>
          <td><code>kill &lt;pid&gt;</code> or <code>systemctl stop</code></td>
          <td>Terminate</td>
          <td>The <em>polite</em> way to ask a process to quit.</td>
      </tr>
      <tr>
          <td><code>SIGHUP</code></td>
          <td>Terminal hangup; config reloads</td>
          <td>Terminate</td>
          <td>Many daemons repurpose it as “reload config.”</td>
      </tr>
      <tr>
          <td><code>SIGQUIT</code></td>
          <td>Ctrl+\</td>
          <td>Core dump + exit</td>
          <td>some like nginx use it for graceful quit.</td>
      </tr>
      <tr>
          <td><code>SIGKILL</code></td>
          <td><code>kill -9 &lt;pid&gt;</code></td>
          <td>Terminate (uncatchable)</td>
          <td>No cleanup, can’t be trapped.</td>
      </tr>
      <tr>
          <td><code>SIGTSTP</code></td>
          <td>Ctrl+Z</td>
          <td>Stop (suspend)</td>
          <td>Background job control; <code>fg</code> to resume.</td>
      </tr>
  </tbody>
</table>
<p>You can list them all with <code>kill -l</code> and you will get a list like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">└─$ <span class="nb">kill</span> -l
</span></span><span class="line"><span class="cl"> 1<span class="o">)</span> SIGHUP       2<span class="o">)</span> SIGINT       3<span class="o">)</span> SIGQUIT      4<span class="o">)</span> SIGILL       5<span class="o">)</span> SIGTRAP
</span></span><span class="line"><span class="cl"> 6<span class="o">)</span> SIGABRT      7<span class="o">)</span> SIGBUS       8<span class="o">)</span> SIGFPE       9<span class="o">)</span> SIGKILL     10<span class="o">)</span> SIGUSR1
</span></span><span class="line"><span class="cl">11<span class="o">)</span> SIGSEGV     12<span class="o">)</span> SIGUSR2     13<span class="o">)</span> SIGPIPE     14<span class="o">)</span> SIGALRM     15<span class="o">)</span> SIGTERM
</span></span><span class="line"><span class="cl">16<span class="o">)</span> SIGSTKFLT   17<span class="o">)</span> SIGCHLD     18<span class="o">)</span> SIGCONT     19<span class="o">)</span> SIGSTOP     20<span class="o">)</span> SIGTSTP
</span></span><span class="line"><span class="cl">21<span class="o">)</span> SIGTTIN     22<span class="o">)</span> SIGTTOU     23<span class="o">)</span> SIGURG      24<span class="o">)</span> SIGXCPU     25<span class="o">)</span> SIGXFSZ
</span></span><span class="line"><span class="cl">26<span class="o">)</span> SIGVTALRM   27<span class="o">)</span> SIGPROF     28<span class="o">)</span> SIGWINCH    29<span class="o">)</span> SIGIO       30<span class="o">)</span> SIGPWR
</span></span><span class="line"><span class="cl">31<span class="o">)</span> SIGSYS      34<span class="o">)</span> SIGRTMIN    35<span class="o">)</span> SIGRTMIN+1  36<span class="o">)</span> SIGRTMIN+2  37<span class="o">)</span> SIGRTMIN+3
</span></span><span class="line"><span class="cl">38<span class="o">)</span> SIGRTMIN+4  39<span class="o">)</span> SIGRTMIN+5  40<span class="o">)</span> SIGRTMIN+6  41<span class="o">)</span> SIGRTMIN+7  42<span class="o">)</span> SIGRTMIN+8
</span></span><span class="line"><span class="cl">43<span class="o">)</span> SIGRTMIN+9  44<span class="o">)</span> SIGRTMIN+10 45<span class="o">)</span> SIGRTMIN+11 46<span class="o">)</span> SIGRTMIN+12 47<span class="o">)</span> SIGRTMIN+13
</span></span><span class="line"><span class="cl">48<span class="o">)</span> SIGRTMIN+14 49<span class="o">)</span> SIGRTMIN+15 50<span class="o">)</span> SIGRTMAX-14 51<span class="o">)</span> SIGRTMAX-13 52<span class="o">)</span> SIGRTMAX-12
</span></span><span class="line"><span class="cl">53<span class="o">)</span> SIGRTMAX-11 54<span class="o">)</span> SIGRTMAX-10 55<span class="o">)</span> SIGRTMAX-9  56<span class="o">)</span> SIGRTMAX-8  57<span class="o">)</span> SIGRTMAX-7
</span></span><span class="line"><span class="cl">58<span class="o">)</span> SIGRTMAX-6  59<span class="o">)</span> SIGRTMAX-5  60<span class="o">)</span> SIGRTMAX-4  61<span class="o">)</span> SIGRTMAX-3  62<span class="o">)</span> SIGRTMAX-2
</span></span><span class="line"><span class="cl">63<span class="o">)</span> SIGRTMAX-1  64<span class="o">)</span> SIGRTMAX
</span></span></code></pre></div><blockquote>
<p><strong>Note:</strong> the list and numbers shown are from Linux. Signal <em>names</em> are portable, but numbers and availability differ on BSD/macOS/Solaris.</p></blockquote>
<p>Most of the time you only care about the common signals, and modern tools let you use their <strong>names</strong> (<code>SIGTERM</code>, <code>SIGHUP</code>, etc.) directly.<br>
The old habit of using numbers (<code>kill -9</code> or <code>kill -1</code>) still works, but it’s less portable since signal <strong>numbers vary</strong> across Unix flavors. Stick to names unless you’re typing a quick shortcut.</p>
<p>The main tool for sending signals is the <code>kill</code> command which despite its name, it doesn’t always “kill” a process. By default it throws <code>SIGTERM</code>, but you can specify any signal. Think of it as the userland signal throwing tool that just defaults to kill:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># First, find the PID of the process you want to affect</span>
</span></span><span class="line"><span class="cl">ps aux <span class="p">|</span> grep nginx
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Then send it a signal</span>
</span></span><span class="line"><span class="cl"><span class="nb">kill</span> -TERM &lt;pid&gt;    <span class="c1"># send SIGTERM (default)</span>
</span></span><span class="line"><span class="cl"><span class="nb">kill</span> -HUP &lt;pid&gt;     <span class="c1"># send SIGHUP</span>
</span></span><span class="line"><span class="cl"><span class="nb">kill</span> -KILL &lt;pid&gt;    <span class="c1"># send SIGKILL (can’t be trapped)</span>
</span></span><span class="line"><span class="cl">killall -HUP nginx  <span class="c1"># send SIGHUP to all nginx processes</span>
</span></span></code></pre></div><br>
<p><span class="tag blue">Pro-Tip:</span> With <code>kill -9 &lt;pid&gt;</code> you must supply the PID of <strong>every process</strong> you want to kill. That’s why tools like <code>pkill</code> and <code>killall</code> are so handy.</p>
<hr>
<h3 id="beyond-kill-other-ways-to-signal-processes">Beyond <code>kill</code>: other ways to signal processes</h3>
<p>Signals aren’t just for <code>kill</code>. The shell and related tools let you affect processes in different ways:</p>
<ul>
<li>
<p><strong>Job Control (in the shell)</strong></p>
<ul>
<li>
<p><code>Ctrl+Z</code> → sends <code>SIGTSTP</code> (suspend) via the <strong>terminal driver</strong>.</p>
</li>
<li>
<p><code>bg</code> → resumes a job in the background (sends <code>SIGCONT</code>).</p>
</li>
<li>
<p><code>fg</code> → brings a job back to the foreground.</p>
</li>
<li>
<p><code>jobs -l</code> → shows current job table with PIDs.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Start a long-running process in the foreground</span>
</span></span><span class="line"><span class="cl">sleep <span class="m">600</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Press Ctrl+Z in the terminal</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Output will look like:</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>1<span class="o">]</span>+  Stopped    sleep <span class="m">600</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># See it in the job table (with PID)</span>
</span></span><span class="line"><span class="cl"><span class="nb">jobs</span> -l
</span></span><span class="line"><span class="cl"><span class="o">[</span>1<span class="o">]</span>+  <span class="m">12345</span> Stopped    sleep <span class="m">600</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Resume it in the background</span>
</span></span><span class="line"><span class="cl"><span class="nb">bg</span> %1
</span></span><span class="line"><span class="cl"><span class="o">[</span>1<span class="o">]</span>+  <span class="m">12345</span> Running    sleep <span class="m">600</span> <span class="p">&amp;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Bring it back to the foreground</span>
</span></span><span class="line"><span class="cl"><span class="nb">fg</span> %1
</span></span><span class="line"><span class="cl">sleep <span class="m">600</span>
</span></span></code></pre></div></li>
</ul>
<span class="tag blue">Notes:</span>
<ul>
<li><code>bg</code> and <code>fg</code> only work on processes started <strong>from the current shell</strong>.</li>
<li>If the process expects input, running it in the background (<code>bg</code>) won’t magically make it non-interactive — it may still block waiting for input.</li>
<li>Background jobs still write to the terminal by default. Use output redirection (<code>&gt; file 2&gt;&amp;1</code>) if you don’t want them spamming your shell.</li>
<li>If you close the terminal, jobs will get <code>SIGHUP</code> unless you’ve used <code>nohup</code> or <code>disown</code>.</li>
<li>In <code>bash</code>/<code>zsh</code> you can reference jobs by number <code>%1</code>, <code>%2</code>, or by name substring: <code>fg %sleep</code>.</li>
</ul>
</li>
</ul>
<br>
<ul>
<li>
<p><strong>nohup</strong><br>
Run a command that keeps going even after you log out of the shell:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">nohup hugo server -D <span class="p">&amp;</span>
</span></span></code></pre></div><p>This ignores <code>SIGHUP</code>, so the process keeps running if you log out of your shell or disconnect an SSH session. Output (both stdout and stderr) gets redirected to <code>nohup.out</code> by default unless you specify otherwise.</p>
<span class="tag blue">Notes:</span>
<ul>
<li>Logging out: the process keeps running in the background.</li>
<li>Logging back in (or SSH-ing again): the process is still alive, but it won’t show up in your <code>jobs</code> list since that’s tied to the original shell. Use <code>ps aux | grep hugo</code> or <code>pgrep hugo</code> to find it.</li>
<li>Reboot: <strong>nohup does not survive a reboot.</strong> For persistence across restarts, use a service manager like <code>systemd</code>, <code>supervisord</code>, or Docker.</li>
<li>Good use case: quick, long-running tasks during a session (like <code>nohup hugo server -D &amp;</code> while you hack on a blog). Not a replacement for proper service management.</li>
</ul>
<br>
</li>
<li>
<p><strong>disown</strong> (bash/zsh only)<br>
Removes a job from the shell’s job table so it won’t receive <code>SIGHUP</code>. The process keeps running, but the shell forgets about it.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sleep <span class="m">600</span> <span class="p">&amp;</span>
</span></span><span class="line"><span class="cl"><span class="nb">jobs</span> -l
</span></span><span class="line"><span class="cl"><span class="nb">disown</span> %1
</span></span><span class="line"><span class="cl"><span class="nb">jobs</span> -l    <span class="c1"># now the job no longer shows up</span>
</span></span></code></pre></div><p>Since it’s a shell builtin, there’s no <code>man disown</code>. Use:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">help</span> <span class="nb">disown</span>   <span class="c1"># bash</span>
</span></span></code></pre></div><p>or in zsh:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">man zshbuiltins <span class="p">|</span> less +/disown
</span></span></code></pre></div></li>
</ul>
<br>
<ul>
<li>
<p><strong>renice</strong><br>
Adjust scheduling priority (not a signal, but related process control).</p>
<ul>
<li>Lower nice value = <strong>higher CPU priority</strong> (use for important processes).</li>
<li>Higher nice value = <strong>lower CPU priority</strong> (good for heavy background tasks).</li>
<li>Niceness ranges from -20 (highest priority) to 19 (lowest priority). Default is 0</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">renice -n <span class="m">19</span> -p &lt;pid&gt;   <span class="c1"># very &#34;nice&#34; → gets less CPU time</span>
</span></span><span class="line"><span class="cl">renice -n <span class="m">0</span> -p &lt;pid&gt;    <span class="c1"># reset to normal priority</span>
</span></span><span class="line"><span class="cl">renice -n -5 -p &lt;pid&gt;   <span class="c1"># increase priority (root only)</span>
</span></span></code></pre></div></li>
</ul>
<br>
<ul>
<li>
<p><strong>killall / pkill</strong><br>
Send signals by process name or regex pattern, instead of hunting PIDs manually:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">kill</span> -USR1 &lt;pid&gt;       <span class="c1"># send a specific signal to a given PID (works on one or a list of PIDs)</span>
</span></span><span class="line"><span class="cl"><span class="nb">kill</span> -HUP <span class="m">1234</span> <span class="m">1235</span>    <span class="c1"># send HUP to multiple PIDs </span>
</span></span><span class="line"><span class="cl">killall -TERM vim      <span class="c1"># send TERM to all vim processes</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">pkill -HUP nginx       <span class="c1"># send HUP to all processes named nginx</span>
</span></span><span class="line"><span class="cl">pkill -HUP -f <span class="s1">&#39;nginx.*worker&#39;</span>   <span class="c1"># send SIGHUP to any process whose command matches the regex &#34;nginx.*worker&#34;</span>
</span></span><span class="line"><span class="cl">pkill -TERM -f <span class="s1">&#39;jupyter-notebook&#39;</span>   <span class="c1"># restart all Jupyter notebook servers (if multiple users)</span>
</span></span></code></pre></div></li>
</ul>
<br>
<ul>
<li>
<p><strong>systemctl</strong><br>
Modern services often live under <code>systemd</code>. Commands like <code>systemctl stop</code>, <code>reload</code>, and <code>restart</code> rely on signals under the hood, but with extra policy:</p>
<ul>
<li><code>stop</code> → sends <strong>SIGTERM</strong>, then escalates to <strong>SIGKILL</strong> if the process doesn’t exit within <code>TimeoutStopSec=</code>.</li>
<li><code>reload</code> → runs <code>ExecReload=</code> from the unit file, or if unset, sends the service’s <code>ReloadSignal=</code> (often <strong>SIGHUP</strong>).</li>
<li><code>restart</code> → does a stop, then start (so SIGTERM → SIGKILL if needed, then new process).</li>
</ul>
</li>
</ul>
<p><span class="tag blue">Pro-Tip:</span><br>
Backgrounding (<code>&amp;</code>, <code>bg</code>) isn’t just convenience — it literally controls signals (<code>SIGSTOP</code>, <code>SIGCONT</code>) at the kernel level.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="pdp-11.jpg" 
       alt="a closeup of a DEC PDP-11 switch panel" 
       style="display:block; margin:0 auto; width:min(100%, 650px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    It was a simpler time
  </figcaption>
</figure>
<h2 id="bash-traps">Bash Traps</h2>
<p>A common pattern is to trap several signals at once, so your script can clean up or log before exiting.</p>
<p>Here’s an example script that spins up a background worker, and makes sure it’s killed cleanly on exit or interrupt:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="c1"># demo-trap.sh</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Start a dummy background job</span>
</span></span><span class="line"><span class="cl">sleep <span class="m">600</span> <span class="p">&amp;</span>
</span></span><span class="line"><span class="cl"><span class="nv">worker_pid</span><span class="o">=</span><span class="nv">$!</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Worker started with PID </span><span class="nv">$worker_pid</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"># Define cleanup function</span>
</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;Caught signal, cleaning up...&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">kill</span> -TERM <span class="s2">&#34;</span><span class="nv">$worker_pid</span><span class="s2">&#34;</span> 2&gt;/dev/null
</span></span><span class="line"><span class="cl">    <span class="nb">wait</span> <span class="s2">&#34;</span><span class="nv">$worker_pid</span><span class="s2">&#34;</span> 2&gt;/dev/null
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;Worker stopped. Exiting.&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">exit</span> <span class="m">0</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="c1"># Trap multiple signals</span>
</span></span><span class="line"><span class="cl"><span class="nb">trap</span> cleanup INT TERM HUP
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Main loop</span>
</span></span><span class="line"><span class="cl"><span class="k">while</span> true<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;Main script running. Press Ctrl+C to stop.&#34;</span>
</span></span><span class="line"><span class="cl">    sleep <span class="m">10</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span></code></pre></div><ul>
<li><code>INT</code> → user pressed <strong>Ctrl+C</strong></li>
<li><code>TERM</code> → process was asked to stop politely (<code>kill &lt;pid&gt;</code> or <code>systemctl stop</code>)</li>
<li><code>HUP</code> → hangup (often used as “reload”)</li>
</ul>
<p><span class="tag blue">Pro-Tip:</span> You can trap multiple signals in one line. Here, <code>trap cleanup INT TERM HUP</code> ensures your cleanup runs whether you press Ctrl+C, close the terminal, or stop it from another shell.</p>
<br>
<h2 id="python-signal-handling">Python Signal Handling</h2>
<blockquote>
<p><em>Profanity is the one language all programmers know best.</em> &ndash;anonymous fortune</p></blockquote>
<p>Python has the <code>signal</code> module, which lets you trap signals much like Bash.<br>
Typical use case: catch <code>SIGINT</code> (Ctrl+C) or <code>SIGTERM</code> (kill) to shut down cleanly.</p>
<p>Example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="ch">#!/usr/bin/env python3</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">signal</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">time</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Define handler function</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">handle_signal</span><span class="p">(</span><span class="n">signum</span><span class="p">,</span> <span class="n">frame</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Caught signal </span><span class="si">{</span><span class="n">signum</span><span class="si">}</span><span class="s2">, cleaning up...&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># do cleanup here</span>
</span></span><span class="line"><span class="cl">    <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Register handlers</span>
</span></span><span class="line"><span class="cl"><span class="n">signal</span><span class="o">.</span><span class="n">signal</span><span class="p">(</span><span class="n">signal</span><span class="o">.</span><span class="n">SIGINT</span><span class="p">,</span> <span class="n">handle_signal</span><span class="p">)</span>   <span class="c1"># Ctrl+C</span>
</span></span><span class="line"><span class="cl"><span class="n">signal</span><span class="o">.</span><span class="n">signal</span><span class="p">(</span><span class="n">signal</span><span class="o">.</span><span class="n">SIGTERM</span><span class="p">,</span> <span class="n">handle_signal</span><span class="p">)</span>  <span class="c1"># kill &lt;pid&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Running. Press Ctrl+C or send SIGTERM to stop.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Simulate work</span>
</span></span><span class="line"><span class="cl"><span class="k">while</span> <span class="kc">True</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span></code></pre></div><ul>
<li><code>SIGINT</code> → user pressed <strong>Ctrl+C</strong></li>
<li><code>SIGTERM</code> → process asked to stop politely (<code>kill &lt;pid&gt;</code>)</li>
</ul>
<span class="tag blue">Pro-Tip:</span>
<ul>
<li>Not every signal is catchable in Python. For example, <code>SIGKILL</code> and <code>SIGSTOP</code> can never be trapped — the kernel enforces those.</li>
<li>Only the <strong>main thread</strong> can set handlers, and signals are always delivered to that thread.</li>
<li>On Windows, only a limited set of signals work (<code>SIGINT</code>, <code>SIGBREAK</code>, and a fake <code>SIGTERM</code>).</li>
<li>In async code, you can use <code>loop.add_signal_handler(signal.SIGTERM, callback)</code> to integrate with <code>asyncio</code>.</li>
</ul>
<h2 id="links-and-stuff">Links and Stuff</h2>
<p><strong>Overview topics</strong></p>
<ul>
<li><a href="https://en.wikipedia.org/wiki/Signal_(IPC)">Signal (IPC)</a></li>
<li><a href="https://en.wikipedia.org/wiki/Inter-process_communication">Inter-process communication</a></li>
<li><a href="https://man7.org/linux/man-pages/man7/signal.7.html"><code>signal(7)</code> — overview of signals</a></li>
<li><a href="https://man7.org/linux/man-pages/man7/signal-safety.7.html"><code>signal-safety(7)</code> — what’s safe inside handlers</a></li>
</ul>
<p><strong>References</strong></p>
<ul>
<li><a href="https://man7.org/linux/man-pages/man1/kill.1.html"><code>kill(1)</code> — man7</a> — manual page for the <code>kill</code> command</li>
<li><a href="https://man7.org/linux/man-pages/man1/pgrep.1.html"><code>pgrep(1)</code>/<code>pkill(1)</code> — man7</a> — search or signal processes by name/regex</li>
<li><a href="https://man7.org/linux/man-pages/man1/killall.1.html"><code>killall(1)</code> — man7</a> — signal all processes matching a name (<em>note portability differences</em>)</li>
<li><a href="https://codebrowser.dev/linux/linux/kernel/signal.c.html">Linux kernel <code>signal.c</code></a> — kernel source handling signals &amp; job control</li>
<li><a href="https://docs.python.org/3/library/signal.html">Python <code>signal</code> module docs</a> — official Python reference</li>
<li><a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html"><code>systemd.service(5)</code></a> — details on <code>ExecReload=</code> and <code>ReloadSignal=</code> in systemd units</li>
<li><a href="https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/signal.h.html">POSIX <code>signal.h</code></a> — portable signal definitions</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>That’s it — a quick and dirty guide to signals in Unix-land.<br>
Signals are one of those Unix fundamentals that everybody bumps into, but they can still trip you up if you don’t know the details.</p>
<p>Next time you press <code>Ctrl+C</code>, background a process, or reload a daemon, remember: you’re working with a mechanism that’s been around since the 1970s and is still quietly running the show today.</p>
<p>📬 Got a favorite signal trick or a war story about a <code>kill -9</code> gone wrong? Send it my way: <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Daemonology: A Cross-Platform Guide</title>
      <link>https://adminjitsu.com/posts/daemonology/</link>
      <pubDate>Mon, 01 Sep 2025 08:21:04 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/daemonology/</guid>
      <description>Learn to create and manage background services on Linux (systemd) and macOS (launchd). Step-by-step examples for scheduled jobs, long-running daemons, and replacing cron. Includes copy-paste templates and troubleshooting tips.</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<figure style="text-align:center; margin: 1em auto;">
  <img src="sunnydale-high-library.jpg" 
       alt="The library set from Buffy the Vampire Slayer, dramatic lighting" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    The Sunnydale High Library seems like a good place to start this one<br>
      Still from Buffy the Vampire Slayer (© 20th Century Fox Television). Used here under fair use for commentary.
  </figcaption>
</figure>
<br>
<p>Welcome to <strong>Daemonology</strong> — the (sometimes) dark art of getting programs to behave, run on schedule, and stick around without babysitting. Whether you’re on Linux with <code>systemd</code> or macOS with <code>launchd</code>, the idea is simple: if you want the operating system to manage your script—starting it, stopping it, logging it, or running it on a schedule—you define it as a service.</p>
<blockquote>
<p><em>In Unix, daemons are background processes that run independently of any user session, usually starting at boot and quietly providing essential services like scheduling, networking, or logging. They don’t interact directly with the user but instead handle requests or monitor resources, often ending in a d (e.g. sshd, cron). Think of them as the invisible caretakers that keep the system’s gears turning.</em></p></blockquote>
<p>People have debated the move from <code>init.d</code>/<code>cron</code> to <code>systemd</code> and <code>launchd</code> a lot over the years — if you want the lore, start here: <a href="https://en.wikipedia.org/wiki/Systemd#Adoption_and_controversy">systemd controversy</a>. But if you’re running a modern Linux (or anything Apple), chances are you’re already in the world of <code>systemd</code> or <code>launchd</code>. These tools give you structured configs, unified logging, restart policies, and scheduling baked right in. The downisde is that they can sometimes be verbose and confusing.</p>
<p>Here’s a practical starter kit to tame them: bare-bones configs, restart policies, and cheatsheet.</p>
<br>
<h2 id="terminology">Terminology</h2>
<blockquote>
<p><em>&ldquo;Never try to fool children, they expect nothing, and therefore see everything.”</em> — Harry Houdini</p></blockquote>
<p>Before we dive into skeletons and configs, let’s set the stage. There are a few moving pieces here, and the terms get overloaded. A little clarity up front makes the rest fall into place.</p>
<hr>
<h3 id="the-old-classic-way-initd--cron">The Old, Classic Way (init.d + cron)</h3>
<p>For decades, Unix-like systems handled background jobs in two different ways:</p>
<ul>
<li><strong>System V init.d scripts</strong><br>
Long-running services (daemons) like <code>sshd</code> or <code>apache2</code> were defined in scripts that lived in <code>/etc/init.d/</code>.<br>
They were shell scripts with <code>start</code>, <code>stop</code>, and <code>restart</code> functions, called by the system at boot.</li>
</ul>
<p>or</p>
<ul>
<li>
<p><strong>BSD init + rc scripts</strong><br>
On BSD systems (and early macOS/Darwin), initialization was driven by a simpler BSD-style init.<br>
A master script (<code>/etc/rc</code>) or per-service scripts in <code>/etc/rc.d/</code> launched daemons at startup, with settings controlled by <code>rc.conf</code>.</p>
</li>
<li>
<p><strong>cron jobs</strong><br>
Scheduled one-shot scripts ran from <code>crontab -e</code>.<br>
You wrote lines like <code>0 2 * * * /usr/local/bin/backup.sh</code>, and cron would invoke them at the right time.<br>
Output usually vanished into the void unless you redirected it somewhere.</p>
</li>
</ul>
<p>It worked, but it meant services and scheduled jobs were managed by different tools, with different conventions and almost no unified logging.</p>
<hr>
<h3 id="the-new-way-systemd--launchd">The New Way (systemd + launchd)</h3>
<p>In an effort to unify the init and scheduling system, modern systems settled on a more structured approach:</p>
<ul>
<li>On <strong>Linux</strong>, <code>systemd</code> runs the show.</li>
<li>On <strong>macOS</strong>, Apple’s <code>launchd</code> plays a similar role.</li>
</ul>
<p>Like the old init process, they are still the first userspace process launched by the kernel (<code>PID 1</code>), responsible for starting everything else. The difference is that they replace the runlevel scripts with declarative unit definitions and a dependency graph. They also integrate scheduling for periodic jobs, so one system manages both daemons and one-shot tasks.</p>
<p>Both treat <em>everything</em> as a <strong>unit of work</strong> described by a small config file. Whether you want a daemon that runs forever, or a script that fires at intervals, you now describe it the same way — in a <code>.service</code> (systemd) or <code>.plist</code> (launchd). Scheduling is built-in (<code>.timer</code> in systemd, <code>StartInterval</code> in launchd), so you don’t need a separate cron subsystem.</p>
<hr>
<h3 id="services-vs-ad-hoc-scripts">Services vs Ad-hoc Scripts</h3>
<p>Let&rsquo;s clarify some key terminology to avoid confusion.</p>
<h4 id="ad-hoc-scripts">Ad-hoc Scripts</h4>
<p>These are scripts you run manually as needed—no system management required. You can run them in the background during use, but they won&rsquo;t persist after reboot or restart automatically if they crash.</p>
<h4 id="managed-services">Managed Services</h4>
<p><strong>Every program you want the system to manage needs a definition file:</strong></p>
<ul>
<li><strong>Linux (systemd):</strong> <code>.service</code> unit files</li>
<li><strong>macOS (launchd):</strong> <code>.plist</code> files</li>
</ul>
<p>These definition files are simple wrappers that tell the system:</p>
<ul>
<li>Which command or script to run</li>
<li>What user to run it as</li>
<li>The working directory</li>
<li>How to handle failures</li>
<li>Where to send logs</li>
</ul>
<h4 id="two-types-of-managed-services">Two Types of Managed Services</h4>
<p><strong>Scheduled Jobs</strong> — <em>Run automatically on a schedule</em>
Perfect for tasks like backup scripts that tar up your home directory.</p>
<ul>
<li><strong>systemd:</strong> Create a <code>.service</code> file + a <code>.timer</code> unit that points to it</li>
<li><strong>launchd:</strong> Add scheduling keys like <code>StartInterval</code> or <code>StartCalendarInterval</code> to your <code>.plist</code></li>
</ul>
<p><strong>Daemons</strong> — <em>Long-running services that listen for requests</em>
Examples include Python web servers or shell loops writing heartbeats.</p>
<ul>
<li><strong>systemd:</strong> Just a <code>.service</code> file (no timer needed)</li>
<li><strong>launchd:</strong> Just a <code>.plist</code> file (no scheduling keys needed)</li>
</ul>
<p>The service stays alive on its own, while the init system handles the lifecycle—tracking the PID, restarting if needed, and capturing logs.</p>
<hr>
<p><strong>Quick Reference:</strong></p>
<ul>
<li><strong>systemd:</strong> <code>.service</code> files for everything, add <code>.timer</code> for scheduled jobs</li>
<li><strong>launchd:</strong> <code>.plist</code> files for everything, add scheduling keys for scheduled jobs</li>
<li><strong>Daemons:</strong> Only need the basic service definition—they manage their own runtime</li>
</ul>
<hr>
<figure style="text-align:center; margin: 1em auto;">
  <img src="orange-wizard.png" 
       alt="a pixellated video game wizard with glowing eyes in robes and a hat reading a book" 
       style="display:block; margin:0 auto; width:min(100%, 200px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    feel like a wizard yet?
  </figcaption>
</figure>
<h3 id="script-patterns-in-practice">Script Patterns in Practice</h3>
<p>Before we look at service definitions, it helps to see the kinds of scripts you might be managing. These are <strong>minimal examples</strong> that illustrate the difference between ad-hoc jobs and programs that behave like daemons. They aren&rsquo;t production-ready (real daemons usually need logging, signal handling, and proper backgrounding), but they capture the basic patterns.</p>
<p><strong>Run-once script</strong> — an ad-hoc job you&rsquo;d pair with a timer:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>tar czf ~/backups/home-<span class="k">$(</span>date +%F<span class="k">)</span>.tgz ~/Documents
</span></span></code></pre></div><p>This script runs once and exits. To have it run automatically, you&rsquo;d wrap it in a <code>.service</code>/<code>.plist</code> and add a <code>.timer</code> or scheduling key.</p>
<p><strong>Looping daemon</strong> — a service that drives itself:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="k">while</span> true<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  date &gt;&gt; /var/log/heartbeat.log
</span></span><span class="line"><span class="cl">  sleep <span class="m">60</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span></code></pre></div><p>The program never exits—it controls its own rhythm with <code>sleep</code>, waking up to do work periodically. Wrapped in a <code>.service</code>/<code>.plist</code>, the init system ensures it stays alive and restarts if it crashes.</p>
<p>This pattern is less visible than listener daemons, but it shows up in places like watchdogs, heartbeat loggers, <code>systemd-timesyncd</code>, <code>cron</code>/<code>anacron</code>, and plenty of user scripts that need to wake up every so often.</p>
<p><strong>Listener daemon</strong> — a service that waits for external input:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">socket</span>
</span></span><span class="line"><span class="cl"><span class="n">s</span> <span class="o">=</span> <span class="n">socket</span><span class="o">.</span><span class="n">socket</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">s</span><span class="o">.</span><span class="n">bind</span><span class="p">((</span><span class="s2">&#34;0.0.0.0&#34;</span><span class="p">,</span> <span class="mi">8080</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="n">s</span><span class="o">.</span><span class="n">listen</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">while</span> <span class="kc">True</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span><span class="p">,</span> <span class="n">addr</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">accept</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="sa">b</span><span class="s2">&#34;Hello, world!</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</span></span></code></pre></div><p>Unlike the looping example, this doesn&rsquo;t &ldquo;wake itself up.&rdquo; Instead, it sits idle until something connects—the program is event-driven, with the outside world driving the loop. With systemd or launchd managing it, you get lifecycle management, automated logging, and restarts on crash.</p>
<p>This is the pattern that a lot of well known servers like nginx, apache2, sshd and postgresql are based on. They listen for requests and respond to them.</p>
<br>
<h2 id="linux-systemd-skeletons">Linux systemd Skeletons</h2>
<figure style="float:left; margin:0 1rem 1rem 0; width:clamp(260px, 45%, 550px);">
  <img src="skeleton-dance.png" 
       alt="3 pixel art, video game style skeletons dancing" 
       style="display:block; width:100%; height:auto;">
  <figcaption style="font-size:85%; color:#666; line-height:1.4; margin-top:0.4em; text-align:center">
    templates / skeletons are really useful
  </figcaption>
</figure>
<br>
<p>Unit files are just plain text, but placement and naming matter.</p>
<ul>
<li><strong>System-wide units</strong> live under <code>/etc/systemd/system/</code> (root-owned, affect all users).</li>
<li><strong>User units</strong> live under <code>~/.config/systemd/user/</code> (run as your user, no root required).</li>
<li>Files must end in <code>.service</code> or <code>.timer</code>. The <em>basename</em> ties them together: <code>mybackup.timer</code> will trigger <code>mybackup.service</code>.</li>
</ul>
<div style="clear:both"></div>
<p>Below is a minimal example: a <code>.service</code> that runs a backup script, and a <code>.timer</code> that fires it every night at 2 AM. Yes, comments starting with <code>#</code> are allowed in unit files — you can omit these lines but using them liberally helps future you. Either way, feel free to copy any of the following examples as a starting point and modify them to suit your needs</p>
<h3 id="example-mybackupservice"><strong>Example: mybackup.service</strong></h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># ==========================================================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># mybackup.service — Example systemd service</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Save to: ~/.config/systemd/user/mybackup.service</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ==========================================================</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">Run home directory backup</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ↑ Human-friendly description (shows up in `systemctl status`).</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Optional ordering/dependencies: After=network.target ensures</span>
</span></span><span class="line"><span class="cl"><span class="c1"># the service only runs once networking is available. This primarily matters on boot so</span>
</span></span><span class="line"><span class="cl"><span class="c1"># you may or may not need it depending on the service</span>
</span></span><span class="line"><span class="cl"><span class="na">After</span><span class="o">=</span><span class="s">network.target</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># The actual command to run. Always use the absolute path.</span>
</span></span><span class="line"><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/usr/local/bin/backup.sh</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Optional: directory to treat as &#34;current working directory&#34;.</span>
</span></span><span class="line"><span class="cl"><span class="c1"># If omitted, defaults to /.</span>
</span></span><span class="line"><span class="cl"><span class="na">WorkingDirectory</span><span class="o">=</span><span class="s">/usr/local/bin</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># User account to run as. For user services, this is implied,</span>
</span></span><span class="line"><span class="cl"><span class="c1"># but for system-wide services it’s important to set explicitly.</span>
</span></span><span class="line"><span class="cl"><span class="na">User</span><span class="o">=</span><span class="s">forfaxx</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Restart behavior:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># - no (default) → do not restart</span>
</span></span><span class="line"><span class="cl"><span class="c1"># - always → restart unconditionally</span>
</span></span><span class="line"><span class="cl"><span class="c1"># - on-failure → restart if the process exits with an error</span>
</span></span><span class="line"><span class="cl"><span class="na">Restart</span><span class="o">=</span><span class="s">on-failure</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Example of passing environment variables into the service:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Environment=&#34;BACKUP_DIR=/mnt/backups&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Install]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># This section defines what targets this unit hooks into when enabled.</span>
</span></span><span class="line"><span class="cl"><span class="c1"># For simple run-once jobs, this usually isn’t needed unless you want it</span>
</span></span><span class="line"><span class="cl"><span class="c1"># to also start at boot. For timer-driven jobs, the timer unit handles it.</span>
</span></span><span class="line"><span class="cl"><span class="na">WantedBy</span><span class="o">=</span><span class="s">default.target</span>
</span></span></code></pre></div><p><strong>mybackup.timer</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># ==========================================================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># mybackup.timer — Example systemd timer</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Save to: ~/.config/systemd/user/mybackup.timer</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ==========================================================</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">Run home directory backup daily at 2am</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ↑ Shown in `systemctl status` and logs.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Timer]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Calendar syntax:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   Day-Month-Year Hour:Minute:Second</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   * means &#34;any&#34;. So this is &#34;every day at 02:00&#34;.</span>
</span></span><span class="line"><span class="cl"><span class="na">OnCalendar</span><span class="o">=</span><span class="s">*-*-* 02:00:00</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Optional: spread jobs randomly by up to this amount of time.</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Useful when many machines might start the same timer simultaneously.</span>
</span></span><span class="line"><span class="cl"><span class="na">RandomizedDelaySec</span><span class="o">=</span><span class="s">15m</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># If true, systemd will run the job immediately on startup if it</span>
</span></span><span class="line"><span class="cl"><span class="c1"># was missed while the machine was off or asleep.</span>
</span></span><span class="line"><span class="cl"><span class="na">Persistent</span><span class="o">=</span><span class="s">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Install]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Tie this timer to the global &#34;timers.target&#34; so it runs when enabled.</span>
</span></span><span class="line"><span class="cl"><span class="na">WantedBy</span><span class="o">=</span><span class="s">timers.target</span>
</span></span></code></pre></div><h3 id="usage">Usage</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Reload so systemd notices new units</span>
</span></span><span class="line"><span class="cl">systemctl --user daemon-reload
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Enable and start the timer (this implicitly links to the service)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Note: --user is crucial here - without it, you&#39;d be managing system services as root</span>
</span></span><span class="line"><span class="cl">systemctl --user <span class="nb">enable</span> --now mybackup.timer
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check status: shows last and next run</span>
</span></span><span class="line"><span class="cl">systemctl --user status mybackup.timer
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># See logs from the last run</span>
</span></span><span class="line"><span class="cl">journalctl --user -u mybackup.service -e
</span></span></code></pre></div><hr>
<h3 id="systemd-timer-scheduling-options">systemd <code>.timer</code> scheduling options</h3>
<p>Timers in systemd can trigger units based on <strong>absolute calendar time</strong>, <strong>relative/monotonic delays</strong>, or a mix of both. You define them in a <code>foo.timer</code> file, usually paired with a <code>foo.service</code>. The calendar parser understands rich <a href="https://www.w3.org/TR/NOTE-datetime?utm_source=chatgpt.com">ISO-8601–style dates and times</a>, making it far more expressive than cron.</p>
<p><strong>Calendar-based</strong><br>
These options use calendar expressions to run jobs at exact times or repeating schedules. The syntax is flexible: you can specify wildcards (<code>*</code>), named days, or shorthand like <code>daily</code>.</p>
<ul>
<li>
<p><code>OnCalendar=</code> → ISO-8601–style calendar expressions. Examples:</p>
<ul>
<li>
<p><code>OnCalendar=*-*-* 09:00</code> → every day at 9 AM</p>
</li>
<li>
<p><code>OnCalendar=Tue,Thu *-*-* 18:00</code> → every Tuesday and Thursday at 6 PM</p>
</li>
<li>
<p><code>OnCalendar=Mon..Fri *-*-* 08:30</code> → weekdays at 8:30 AM</p>
</li>
<li>
<p>Shorthands: <code>hourly</code>, <code>daily</code>, <code>weekly</code>, <code>monthly</code>, <code>yearly</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl">    <span class="c1"># myweekly.timer</span>
</span></span><span class="line"><span class="cl">  <span class="k">[Unit]</span>
</span></span><span class="line"><span class="cl">  <span class="na">Description</span><span class="o">=</span><span class="s">Run weekly cleanup</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">[Timer]</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># shorthand: every Monday at 00:00</span>
</span></span><span class="line"><span class="cl">  <span class="na">OnCalendar</span><span class="o">=</span><span class="s">weekly</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># optional: jitter and catch-up</span>
</span></span><span class="line"><span class="cl">  <span class="na">RandomizedDelaySec</span><span class="o">=</span><span class="s">1h
</span></span></span><span class="line"><span class="cl"><span class="s">  Persistent=true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">[Install]</span>
</span></span><span class="line"><span class="cl">  <span class="na">WantedBy</span><span class="o">=</span><span class="s">timers.target</span>
</span></span></code></pre></div></li>
</ul>
<p><strong>Shorthand mappings</strong><br>
Systemd provides a few convenient aliases for common schedules. They expand into explicit calendar expressions:</p>
<table>
  <thead>
      <tr>
          <th>Shorthand</th>
          <th>Equivalent expression</th>
          <th>Meaning</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>hourly</code></td>
          <td><code>*-*-* *:00:00</code></td>
          <td>every hour on the hour</td>
      </tr>
      <tr>
          <td><code>daily</code></td>
          <td><code>*-*-* 00:00:00</code></td>
          <td>every day at midnight</td>
      </tr>
      <tr>
          <td><code>weekly</code></td>
          <td><code>Mon *-*-* 00:00:00</code></td>
          <td>every Monday at midnight</td>
      </tr>
      <tr>
          <td><code>monthly</code></td>
          <td><code>*-*-01 00:00:00</code></td>
          <td>first day of each month at midnight</td>
      </tr>
      <tr>
          <td><code>yearly</code> / <code>annually</code></td>
          <td><code>*-01-01 00:00:00</code></td>
          <td>every January 1st at midnight</td>
      </tr>
  </tbody>
</table>
<p>👉 Use the shorthands for simplicity, or write explicit forms (<code>Fri *-*-* 18:00</code>) if you need more control.</p>
</li>
</ul>
<p><strong>Monotonic (relative) timers</strong><br>
These options measure time relative to certain events (boot, activation, or last run) and are useful for delays and repeating intervals.</p>
<ul>
<li>
<p><code>OnActiveSec=</code> → time since the timer was activated<br>
i.e. <code>OnActiveSec=5min</code> (run 5 minutes after enabling)</p>
</li>
<li>
<p><code>OnBootSec=</code> → time since system boot<br>
i.e. <code>OnBootSec=10min</code> (run 10 minutes after boot)</p>
</li>
<li>
<p><code>OnStartupSec=</code> → time since the systemd manager itself started</p>
</li>
<li>
<p><code>OnUnitActiveSec=</code> → time since the unit was last active<br>
i.e. <code>OnUnitActiveSec=1h</code> (rerun one hour after the last run)</p>
</li>
<li>
<p><code>OnUnitInactiveSec=</code> → time since the unit was last inactive<br>
i.e. <code>OnUnitInactiveSec=30s</code> (rerun if idle for 30 seconds)</p>
</li>
</ul>
<p><strong>Other controls (modifiers)</strong><br>
These don’t trigger timers on their own; they adjust how the above schedules behave.</p>
<ul>
<li>
<p><code>AccuracySec=</code> → how precise the firing time must be (default 1 min). Larger values allow batching.<br>
i.e. <code>AccuracySec=5min</code></p>
</li>
<li>
<p><code>RandomizedDelaySec=</code> → adds jitter to spread out jobs.<br>
i.e. <code>RandomizedDelaySec=30s</code> (fires randomly within a 30s window)</p>
</li>
<li>
<p><code>Persistent=</code> → catch up on missed runs after downtime.<br>
i.e. a nightly backup still runs on next boot if the system was off at midnight</p>
</li>
</ul>
<p>👉 In practice: <code>.timer</code> units combine <strong>calendar scheduling</strong>, <strong>relative delays</strong>, and <strong>resiliency features</strong> like persistence and jitter. They’re effectively a superset of <code>cron</code>, <code>anacron</code>, and manual <code>sleep</code> loops.</p>
<hr>
<h3 id="docs">Docs</h3>
<p>Canonical references for systemd units, services, and timers (freedesktop.org):</p>
<ul>
<li><a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html"><code>systemd.unit(5)</code></a> — general unit file structure</li>
<li><a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html"><code>systemd.service(5)</code></a> — service-specific directives</li>
<li><a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html"><code>systemd.timer(5)</code></a> — timer options (<code>OnCalendar</code>, <code>Persistent</code>, etc.)</li>
</ul>
<figure style="text-align:center; margin: 1em auto;">
  <img src="red-book.png" 
       alt="a pixelated video game style wizard in red robes, holding a book. playfun tone" 
       style="display:block; margin:0 auto; width:min(100%, 350px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    a little knowledge can be a dangerous thing
  </figcaption>
</figure>
<hr>
<h2 id="macos-launchd-skeletons">macOS launchd Skeletons</h2>
<p>On macOS, every managed job is described by an XML <strong><code>.plist</code> file</strong> (property list).</p>
<ul>
<li><strong>Per-user jobs</strong> → <code>~/Library/LaunchAgents/</code> (run as your login user).</li>
<li><strong>System-wide daemons</strong> → <code>/Library/LaunchDaemons/</code> (run as root by default, but you should set a dedicated service user <code>&lt;UserName&gt;</code>).</li>
<li>Filenames must end in <code>.plist</code>, and each must have a unique <code>Label</code>.</li>
<li>Comments use XML syntax: <code>&lt;!-- ... --&gt;</code>.</li>
</ul>
<p>Unlike <code>cron</code>, scheduling is optional. If you omit it, the job won’t run automatically — you’ll trigger it manually with <code>launchctl start</code>. For daemons, you use <code>KeepAlive</code> instead of a schedule.</p>
<hr>
<h3 id="example-1-scheduled-job-cron-style-replacement"><strong>Example 1: Scheduled Job (cron-style replacement)</strong></h3>
<p>This runs a backup script at 2:00 AM every day.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="c">&lt;!-- ==========================================================
</span></span></span><span class="line"><span class="cl"><span class="c">     com.user.backup.plist — Scheduled job example
</span></span></span><span class="line"><span class="cl"><span class="c">     Save to: ~/Library/LaunchAgents/com.user.backup.plist
</span></span></span><span class="line"><span class="cl"><span class="c">     ========================================================== --&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cp">&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;</span>
</span></span><span class="line"><span class="cl"><span class="cp">&lt;!DOCTYPE plist PUBLIC &#34;-//Apple//DTD PLIST 1.0//EN&#34;
</span></span></span><span class="line"><span class="cl"><span class="cp">  &#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd&#34;&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;plist</span> <span class="na">version=</span><span class="s">&#34;1.0&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;dict&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c">&lt;!-- Unique label for the job. Reverse-domain style is recommended. --&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>Label<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;string&gt;</span>com.user.backup<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c">&lt;!-- Absolute path to the script or binary to run. --&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>ProgramArguments<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;array&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;string&gt;</span>/usr/local/bin/backup.sh<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;/array&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c">&lt;!-- Redirect stdout/stderr so output isn’t lost. --&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>StandardOutPath<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;string&gt;</span>/tmp/com.user.backup.out<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>StandardErrorPath<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;string&gt;</span>/tmp/com.user.backup.err<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c">&lt;!-- Optional environment variables --&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="c">&lt;!--
</span></span></span><span class="line"><span class="cl"><span class="c">  &lt;key&gt;EnvironmentVariables&lt;/key&gt;
</span></span></span><span class="line"><span class="cl"><span class="c">  &lt;dict&gt;
</span></span></span><span class="line"><span class="cl"><span class="c">    &lt;key&gt;BACKUP_DIR&lt;/key&gt;
</span></span></span><span class="line"><span class="cl"><span class="c">    &lt;string&gt;/Volumes/Backups&lt;/string&gt;
</span></span></span><span class="line"><span class="cl"><span class="c">  &lt;/dict&gt;
</span></span></span><span class="line"><span class="cl"><span class="c">  --&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c">&lt;!-- Schedule: run at 2:00 AM every day --&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>StartCalendarInterval<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;dict&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;key&gt;</span>Hour<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>2<span class="nt">&lt;/integer&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;key&gt;</span>Minute<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>0<span class="nt">&lt;/integer&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;/dict&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c">&lt;!-- RunAtLoad would also trigger the job once when loaded --&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>RunAtLoad<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;false/&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/dict&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/plist&gt;</span>
</span></span></code></pre></div><hr>
<h3 id="example-2-daemon-always-on-service"><strong>Example 2: Daemon (always-on service)</strong></h3>
<p>This wraps a program like <code>nginx</code> or <code>postgres</code> — something that should stay alive forever.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="c">&lt;!-- ==========================================================
</span></span></span><span class="line"><span class="cl"><span class="c">     com.user.daemon.plist — Always-on daemon example
</span></span></span><span class="line"><span class="cl"><span class="c">     Save to: /Library/LaunchDaemons/com.user.daemon.plist
</span></span></span><span class="line"><span class="cl"><span class="c">     (requires root privileges to install)
</span></span></span><span class="line"><span class="cl"><span class="c">     ========================================================== --&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cp">&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;</span>
</span></span><span class="line"><span class="cl"><span class="cp">&lt;!DOCTYPE plist PUBLIC &#34;-//Apple//DTD PLIST 1.0//EN&#34;
</span></span></span><span class="line"><span class="cl"><span class="cp">  &#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd&#34;&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;plist</span> <span class="na">version=</span><span class="s">&#34;1.0&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;dict&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c">&lt;!-- Unique label for the daemon --&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>Label<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;string&gt;</span>com.user.daemon<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c">&lt;!-- Absolute path to the daemon binary --&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>ProgramArguments<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;array&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;string&gt;</span>/usr/local/sbin/nginx<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="c">&lt;!-- Example arguments --&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;string&gt;</span>-g<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;string&gt;</span>daemon off;<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;/array&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c">&lt;!-- KeepAlive tells launchd to restart it if it crashes --&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>KeepAlive<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;true/&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c">&lt;!-- Run as a dedicated service account instead of root.
</span></span></span><span class="line"><span class="cl"><span class="c">       Best practice: PostgreSQL runs as &#34;postgres&#34;, nginx often as &#34;www&#34;. --&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>UserName<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;string&gt;</span>www<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c">&lt;!-- Optional logging --&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>StandardOutPath<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;string&gt;</span>/var/log/com.user.daemon.out<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>StandardErrorPath<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;string&gt;</span>/var/log/com.user.daemon.err<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/dict&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/plist&gt;</span>
</span></span></code></pre></div><hr>
<h3 id="event-triggered-jobs-no-schedule">Event-Triggered Jobs (no schedule)</h3>
<p>You don’t need a timer for everything—launchd can trigger on file changes or open a socket for you.</p>
<p><strong>Run when files change (WatchPaths)</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="c">&lt;!-- ~/Library/LaunchAgents/com.user.ingest.plist --&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;plist</span> <span class="na">version=</span><span class="s">&#34;1.0&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;dict&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>Label<span class="nt">&lt;/key&gt;</span> <span class="nt">&lt;string&gt;</span>com.user.ingest<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>ProgramArguments<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;array&gt;&lt;string&gt;</span>/usr/local/bin/ingest.sh<span class="nt">&lt;/string&gt;&lt;/array&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c">&lt;!-- Fire when anything under ~/inbox changes --&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>WatchPaths<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;array&gt;&lt;string&gt;</span>~/inbox<span class="nt">&lt;/string&gt;&lt;/array&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>StandardOutPath<span class="nt">&lt;/key&gt;</span> <span class="nt">&lt;string&gt;</span>~/Library/Logs/com.user.ingest.out<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>StandardErrorPath<span class="nt">&lt;/key&gt;</span> <span class="nt">&lt;string&gt;</span>~/Library/Logs/com.user.ingest.err<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/dict&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/plist&gt;</span>
</span></span></code></pre></div><h3 id="usage-1">Usage</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Modern approach (macOS 10.10+)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Load/enable the job</span>
</span></span><span class="line"><span class="cl">launchctl bootstrap gui/<span class="k">$(</span>id -u<span class="k">)</span> ~/Library/LaunchAgents/com.user.backup.plist
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Unload/disable it  </span>
</span></span><span class="line"><span class="cl">launchctl bootout gui/<span class="k">$(</span>id -u<span class="k">)</span> ~/Library/LaunchAgents/com.user.backup.plist
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check if it&#39;s active</span>
</span></span><span class="line"><span class="cl">launchctl list <span class="p">|</span> grep com.user.backup
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Manually trigger (this part stays the same)</span>
</span></span><span class="line"><span class="cl">launchctl start com.user.backup
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Legacy commands (still work but deprecated)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># launchctl load ~/Library/LaunchAgents/com.user.backup.plist</span>
</span></span><span class="line"><span class="cl"><span class="c1"># launchctl unload ~/Library/LaunchAgents/com.user.backup.plist</span>
</span></span></code></pre></div><p><span class="tag yellow">Note</span> <strong>Modern vs Legacy Syntax</strong></p>
<p>Apple deprecated the legacy <code>launchctl load</code>/<code>unload</code> commands in macOS 10.10, replacing them with <code>bootstrap</code>/<code>bootout</code>. The old commands still work for now, but the modern syntax is more explicit and avoids ambiguity.</p>
<p>The key difference is <strong>domains</strong>: modern commands require you to specify <em>where</em> the job should be loaded.</p>
<ul>
<li>
<p>For personal jobs in <code>~/Library/LaunchAgents/</code>, use:<br>
<code>launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/myjob.plist</code><br>
This targets your <strong>GUI user session</strong> (<code>$(id -u)</code> expands to your numeric UID).</p>
</li>
<li>
<p>For system daemons in <code>/Library/LaunchDaemons/</code>, use:<br>
<code>launchctl bootstrap system /Library/LaunchDaemons/mydaemon.plist</code></p>
</li>
</ul>
<p>Apple introduced this change to solve issues with Fast User Switching and multiple user sessions, where the older <code>load</code>/<code>unload</code> commands could be ambiguous.</p>
<p>Most existing tutorials still use the legacy syntax because it’s simpler and widely understood, but the <strong>modern domain-based approach is clearer and gives precise control over job placement</strong>.</p>
<hr>
<h3 id="choosing-a-label">Choosing a Label</h3>
<p>Every launchd job needs a unique <code>&lt;Label&gt;</code>. Apple convention is to use <strong>reverse-DNS style names</strong>, like:</p>
<ul>
<li><code>com.apple.sshd</code></li>
<li><code>org.postgresql.server</code></li>
<li><code>net.nginx.web</code></li>
</ul>
<p>For your own jobs, you can safely pick something like:</p>
<ul>
<li><code>com.yourname.backup</code></li>
<li><code>net.darkstar.heartbeat</code></li>
</ul>
<p>The format doesn’t change how launchd behaves — it’s purely an identifier — but the reverse-DNS pattern avoids collisions. Two rules of thumb:</p>
<ul>
<li><strong>Unique per system</strong>: If two jobs share a label, launchd gets confused.</li>
<li><strong>Readable</strong>: Make it obvious what the job does when you run <code>launchctl list</code>.</li>
</ul>
<p>If in doubt, prefix with your username or domain, then the purpose. For example, instead of just <code>backup</code>, use something like <code>com.forfaxx.backup</code> to avoid clashing with system jobs.</p>
<hr>
<h3 id="launchd-scheduling-options-macos">launchd scheduling options (macOS)</h3>
<p>Jobs in macOS are managed by <strong>launchd</strong>, defined in <code>.plist</code> XML files and loaded with <code>launchctl</code>. Instead of a single flexible expression like <code>OnCalendar=</code>, launchd breaks scheduling into separate keys. These can be combined, but each key is very explicit — no wildcards or ranges like you’d find in cron.</p>
<p><strong>Calendar-based</strong><br>
<code>StartCalendarInterval</code> is the closest thing to cron. It accepts a dictionary (or an array of dictionaries) with any combination of these keys: <code>Minute</code>, <code>Hour</code>, <code>Day</code>, <code>Weekday</code>, and <code>Month</code>. You must spell out exact values, and if you want multiple schedules you add multiple dictionaries.</p>
<p>👉 <strong>Weekday mapping</strong>: <code>0 = Sunday</code>, <code>1 = Monday</code>, … <code>6 = Saturday</code></p>
<ul>
<li>
<p><strong>Every day at 9 AM</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="nt">&lt;dict&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>Hour<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>9<span class="nt">&lt;/integer&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>Minute<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>0<span class="nt">&lt;/integer&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/dict&gt;</span>
</span></span></code></pre></div></li>
<li>
<p><strong>Every Tuesday and Thursday at 6 PM</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="nt">&lt;array&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;dict&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;key&gt;</span>Weekday<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>2<span class="nt">&lt;/integer&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;key&gt;</span>Hour<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>18<span class="nt">&lt;/integer&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;key&gt;</span>Minute<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>0<span class="nt">&lt;/integer&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;/dict&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;dict&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;key&gt;</span>Weekday<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>4<span class="nt">&lt;/integer&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;key&gt;</span>Hour<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>18<span class="nt">&lt;/integer&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;key&gt;</span>Minute<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>0<span class="nt">&lt;/integer&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;/dict&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/array&gt;</span>
</span></span></code></pre></div></li>
<li>
<p><strong>Every weekday (Mon–Fri) at 8:30 AM</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="nt">&lt;array&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;dict&gt;&lt;key&gt;</span>Weekday<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>1<span class="nt">&lt;/integer&gt;&lt;key&gt;</span>Hour<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>8<span class="nt">&lt;/integer&gt;&lt;key&gt;</span>Minute<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>30<span class="nt">&lt;/integer&gt;&lt;/dict&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;dict&gt;&lt;key&gt;</span>Weekday<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>2<span class="nt">&lt;/integer&gt;&lt;key&gt;</span>Hour<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>8<span class="nt">&lt;/integer&gt;&lt;key&gt;</span>Minute<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>30<span class="nt">&lt;/integer&gt;&lt;/dict&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;dict&gt;&lt;key&gt;</span>Weekday<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>3<span class="nt">&lt;/integer&gt;&lt;key&gt;</span>Hour<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>8<span class="nt">&lt;/integer&gt;&lt;key&gt;</span>Minute<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>30<span class="nt">&lt;/integer&gt;&lt;/dict&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;dict&gt;&lt;key&gt;</span>Weekday<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>4<span class="nt">&lt;/integer&gt;&lt;key&gt;</span>Hour<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>8<span class="nt">&lt;/integer&gt;&lt;key&gt;</span>Minute<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>30<span class="nt">&lt;/integer&gt;&lt;/dict&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;dict&gt;&lt;key&gt;</span>Weekday<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>5<span class="nt">&lt;/integer&gt;&lt;key&gt;</span>Hour<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>8<span class="nt">&lt;/integer&gt;&lt;key&gt;</span>Minute<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>30<span class="nt">&lt;/integer&gt;&lt;/dict&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/array&gt;</span>
</span></span></code></pre></div></li>
<li>
<p><strong>Every 2 weeks</strong><br>
There’s no built-in “every 2 weeks” in launchd. You either:</p>
<ul>
<li>Use <code>StartInterval=1209600</code> (14 days in seconds), which repeats from when the job was loaded — but it drifts if the system is asleep or rebooted.</li>
<li>Or explicitly enumerate the calendar dates you want, using multiple <code>StartCalendarInterval</code> dictionaries. This is verbose, but guarantees alignment with real calendar weeks.</li>
</ul>
</li>
</ul>
<p><strong>Interval-based</strong><br>
<code>StartInterval</code> runs a job every <em>N</em> seconds, regardless of the calendar. Think of it as a built-in <code>sleep</code> loop.</p>
<ul>
<li>Example:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="nt">&lt;key&gt;</span>StartInterval<span class="nt">&lt;/key&gt;&lt;integer&gt;</span>3600<span class="nt">&lt;/integer&gt;</span>
</span></span></code></pre></div>→ run every hour (from job load time)</li>
</ul>
<p><strong>Other controls (modifiers)</strong><br>
These don’t schedule jobs directly, but change how and when they launch.</p>
<ul>
<li><code>RunAtLoad</code> → run once immediately when the job is loaded</li>
<li><code>StartOnMount</code> → trigger when a filesystem is mounted</li>
<li><code>KeepAlive</code> → restart automatically; can be <code>true</code> or a dictionary with conditions (restart if a path exists, a network comes up, or the process exits unexpectedly)</li>
</ul>
<p>👉 Launchd’s style is <strong>explicit and dictionary-driven</strong>: you define exact times or raw intervals, then add flags to control behavior. It’s less expressive than systemd’s <code>OnCalendar</code> (no ranges or modulo like “every 2 weeks”), but tightly integrated into macOS and good enough for most practical scheduling needs.</p>
<hr>
<h3 id="troubleshooting-launchd-jobs">Troubleshooting launchd jobs</h3>
<p>If your job isn’t firing:</p>
<ul>
<li>Make sure the <code>.plist</code> is in the right folder with the right ownership:
<ul>
<li><code>~/Library/LaunchAgents/</code> → per-user jobs (owned by you)</li>
<li><code>/Library/LaunchDaemons/</code> → system-wide jobs (owned by root, mode 644)</li>
</ul>
</li>
<li>Validate the file with <code>plutil -lint myjob.plist</code> to catch XML errors.</li>
<li>Check whether launchd actually loaded it:<br>
<code>launchctl print gui/$(id -u) | grep MyJobLabel</code></li>
<li>Look at logs for errors or crashes:<br>
<code>log show --style syslog --predicate 'process == &quot;myjob&quot;' --last 1h</code></li>
</ul>
<p>👉 Most failures come down to wrong location, wrong ownership, or a typo in the plist.</p>
<hr>
<br>
<h3 id="key-distinctions">Key Distinctions</h3>
<ul>
<li><code>StartInterval</code> / <code>StartCalendarInterval</code> → for <strong>cron-style jobs</strong> (run once and exit).</li>
<li><code>KeepAlive</code> → for <strong>daemons</strong> (stay alive forever, restart if needed).</li>
<li>If you omit both, the job won’t run automatically — you’ll need to start it with <code>launchctl</code>. (see Sockets and WatchPaths for exceptions to this rule) 🔗<a href="https://www.manpagez.com/man/5/launchd.plist/">launchd.plist</a></li>
<li>In <code>/Library/LaunchDaemons/</code>, always set <code>&lt;UserName&gt;</code> to a dedicated account instead of running everything as root.</li>
</ul>
<hr>
<h3 id="docs-1">Docs</h3>
<p>Canonical references for scheduling on macOS with launchd:</p>
<ul>
<li><a href="https://www.manpagez.com/man/5/launchd.plist/"><code>launchd.plist(5)</code></a> — property list format reference</li>
<li><a href="https://www.manpagez.com/man/1/launchctl/"><code>launchctl(1)</code></a> — managing jobs from the command line</li>
<li><a href="https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/ScheduledJobs.html#//apple_ref/doc/uid/10000172i-CH1-SW2">Apple Developer: Scheduling Timed Jobs with launchd</a> — official guide to launchd daemons, agents, and timed jobs</li>
</ul>
<figure style="text-align:center; margin: 1em auto;">
  <img src="purple-book.png" 
       alt="a creepy wizard in purple robes with glowing eyes, holding a book with a skull on it. arcade game style" 
       style="display:block; margin:0 auto; width:min(100%, 320px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    I've been known to RTFM 
  </figcaption>
</figure>
<hr>
<h2 id="tips--tricks">Tips &amp; Tricks</h2>
<p>Here are a few practical reminders and tools that save headaches when working with systemd and launchd.</p>
<table>
  <thead>
      <tr>
          <th>Topic</th>
          <th>Linux (systemd)</th>
          <th>macOS (launchd)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Unit placement</strong></td>
          <td><code>/etc/systemd/system/</code> (system-wide)<br><code>~/.config/systemd/user/</code> (per-user)</td>
          <td><code>/Library/LaunchDaemons/</code> (system-wide, run as root or <code>&lt;UserName&gt;</code>)<br><code>~/Library/LaunchAgents/</code> (per-user)</td>
      </tr>
      <tr>
          <td><strong>Unit naming</strong></td>
          <td>Basename ties <code>.service</code> to <code>.timer</code> (e.g. <code>backup.service</code> + <code>backup.timer</code>).</td>
          <td><code>&lt;Label&gt;</code> must be unique. Use reverse-DNS style (<code>com.user.job</code>) to avoid collisions.</td>
      </tr>
      <tr>
          <td><strong>Validation</strong></td>
          <td><code>systemd-analyze verify /path/to/unit</code></td>
          <td><code>plutil -lint com.user.job.plist</code></td>
      </tr>
      <tr>
          <td><strong>Check what’s loaded</strong></td>
          <td><code>systemctl list-units --type=service --user</code><br><code>systemctl list-timers --user</code></td>
          <td><code>launchctl list</code></td>
      </tr>
      <tr>
          <td><strong>Logs / debugging</strong></td>
          <td><code>journalctl -u myjob.service -e</code><br><code>journalctl --user -xe</code></td>
          <td><code>log show --predicate 'process == &quot;backup.sh&quot;' --last 1h</code><br>or check <code>StandardOutPath</code> / <code>StandardErrorPath</code></td>
      </tr>
      <tr>
          <td><strong>Force a run</strong></td>
          <td><code>systemctl start myjob.service</code></td>
          <td><code>launchctl start com.user.job</code></td>
      </tr>
      <tr>
          <td><strong>Enable at boot</strong></td>
          <td><code>systemctl enable myjob.service</code><br><code>systemctl enable myjob.timer</code></td>
          <td>Jobs in LaunchAgents/LaunchDaemons load automatically; use <code>launchctl bootstrap</code> for fine-grained control.</td>
      </tr>
  </tbody>
</table>
<br>
<p><span class="tag green">Pro-Tip:</span> <strong>Best practices:</strong></p>
<ul>
<li><strong>Don’t run everything as root.</strong> Use <code>User=</code> in systemd or <code>&lt;UserName&gt;</code> in launchd to assign a dedicated account for daemons.</li>
<li><strong>Validate first.</strong> Both <code>systemd-analyze verify</code> and <code>plutil -lint</code> catch typos before you’re scratching your head at runtime.</li>
<li><strong>Check logs early.</strong> <code>journalctl</code> and the unified macOS log are your best friends when things don’t behave.</li>
<li><strong>Start manually once.</strong> Always <code>systemctl start</code> or <code>launchctl start</code> a job to make sure it works interactively before scheduling it forever.</li>
</ul>
<br>
<h2 id="links-and-stuff">Links and Stuff</h2>
<p>Here are the canonical docs and references worth bookmarking:</p>
<ul>
<li>
<p><strong>systemd:</strong></p>
<ul>
<li><a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html">systemd.unit(5)</a></li>
<li><a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html">systemd.service(5)</a></li>
<li><a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html">systemd.timer(5)</a></li>
<li><a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.time.html">systemd.time(7)</a> — time spans &amp; calendar expressions</li>
</ul>
</li>
<li>
<p><strong>launchd:</strong></p>
<ul>
<li><a href="https://www.manpagez.com/man/5/launchd.plist/">launchd.plist(5)</a></li>
<li><a href="https://www.manpagez.com/man/1/launchctl/">launchctl(1)</a></li>
<li>Apple’s old but still useful <a href="https://developer.apple.com/library/archive/technotes/tn2083/_index.html">Technical Note TN2083</a></li>
<li><a href="https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/ScheduledJobs.html#//apple_ref/doc/uid/10000172i-CH1-SW2">Apple Developer: Scheduling Timed Jobs with launchd</a></li>
</ul>
</li>
<li>
<p><strong>GUI helpers (macOS):</strong><br>
I have used these before:</p>
<ul>
<li><a href="https://www.soma-zone.com/LaunchControl/">LaunchControl</a> — full-featured, actively maintained GUI for launchd</li>
<li><a href="https://www.peterborgapps.com/lingon/">Lingon</a> — simpler, lightweight tool for launchd jobs</li>
</ul>
</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>That’s it — the basics of <strong>Daemonology</strong> in Unix-land. We’ve covered how to wrap a script into a managed job, how timers and intervals take the place of cron, and how daemons live under the supervision of systemd or launchd.</p>
<p>Next time you need a script to stick around, restart itself, or wake up on a schedule — you’ll know how to bind it to the system and let the OS do the babysitting.</p>
<p>Got any tips or tricks I missed? Corrections? I&rsquo;d love to hear about them! <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Hugo Boilerplate</title>
      <link>https://adminjitsu.com/posts/hugo-boilerplate/</link>
      <pubDate>Fri, 29 Aug 2025 18:57:12 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/hugo-boilerplate/</guid>
      <description>A pragmatic workflow for writing in Hugo: BOILERPLATE.txt snippets, espanso macros, page bundles, alt-text habits, and when to reach for inline HTML—minus the platform fight.</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<figure style="text-align:center; margin:1em auto;">
  <img src="hugo-logo-wide.svg" 
       alt="Wide horizontal logo of the Hugo static site generator" 
       style="display:block; margin:0 auto; width:min(100%,400px); height:auto;">
  <figcaption style="font-size:85%; font-weight:normal; color:#666; line-height:1.4; margin-top:0.4em;">
    Hugo has such a cheerful, simple logo
  </figcaption>
</figure>
<br>
<p>After setting up Hugo, in a previous post <a href="/posts/my-friend-hugo/">My Friend Hugo</a>,
I kept going — experimenting with the actual <em>writing workflow</em>.
Over time I’ve built a set of practices that make writing easier,
faster, and more consistent: from my <code>boilerplate.txt</code> snippets and
Espanso macros to how I think about alt tags and tactical inline HTML.</p>
<p>You&rsquo;re welcome, both of you!</p>
<p style="text-align:left;">
  <a href="/tags/hugo" class="button">🧾 View Other Hugo Posts</a>
</p>
<h2 id="boilerplate-and-snippets">Boilerplate and Snippets</h2>
<p>Markdown is easy, with a simple syntax that abstracts away complexity and lets you focus on writing. If I had to write this site in pure HTML I’d never attempt long-form content. That simplicity is a big selling point for Hugo as a static site generator — but there are still some sticky bits. Certain Markdown extensions, custom shortcodes, or inline HTML are fiddly to remember.</p>
<p>So like many before me, I keep a crib sheet in a file called <strong>BOILERPLATE.txt</strong>.<br>
It’s just a plain text file, but it has become my go-to reference: copy-and-paste snippets for the patterns I use over and over. Figure blocks, float-left images, GitHub buttons — all captured once, used consistently forever.</p>
<p><span class="tag green">Pro-Tip:</span> Keeping snippets literal in a text file avoids “magic.” You always know exactly what will get pasted in.</p>
<hr>
<h3 id="scratch-folder">Scratch folder</h3>
<p>A best practice I’ve settled on (and would repeat if I started fresh) is keeping a <code>scratch/</code> directory at the root of my Hugo site. It’s ignored by Hugo at build time, so it won’t leak unless I write about it, but it’s a perfect place for experiments: HTML previews of fonts, screenshots, notes, and of course <strong>BOILERPLATE.txt</strong> itself.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">┌──<span class="o">[</span> forfaxx@shinobi <span class="o">]</span>:~/codelab/adminjitsu/scratch  <span class="o">(</span>main*<span class="o">)</span>
</span></span><span class="line"><span class="cl">└─$ ls -l 
</span></span><span class="line"><span class="cl">-rw-r--r--  adminjitsu-fonts.html
</span></span><span class="line"><span class="cl">-rw-r--r--  adminjitsu_palette_overview.png
</span></span><span class="line"><span class="cl">drwxr-xr-x  bin
</span></span><span class="line"><span class="cl">-rw-r--r--  BOILERPLATE.txt
</span></span><span class="line"><span class="cl">-rw-r--r--  POST-IDEAS.txt
</span></span><span class="line"><span class="cl">-rw-r--r--  SCRATCHPAD.txt
</span></span><span class="line"><span class="cl">-rw-r--r--  Tags and Categories.txt
</span></span><span class="line"><span class="cl">drwxr-xr-x  WIP archive
</span></span></code></pre></div><p>Inside <code>bin/</code> I keep little helper tools I’ve built for the site — see <a href="/posts/my-friend-hugo/">My Friend Hugo</a> for details on scripts like <code>metaclean</code> and <code>tag-audit</code>. I also symlink a couple of general-purpose commands from my dotfiles so I don’t duplicate code between projects. Having them <em>right next to the Hugo content</em> keeps everything tidy and self-contained.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">┌──<span class="o">[</span> forfaxx@shinobi <span class="o">]</span>:~/codelab/adminjitsu/scratch/bin  <span class="o">(</span>main*<span class="o">)</span>
</span></span><span class="line"><span class="cl">└─$ ls -l 
</span></span><span class="line"><span class="cl">drwxr-xr-x  covers
</span></span><span class="line"><span class="cl">-rw-r--r--  feather-demo.html
</span></span><span class="line"><span class="cl">-rw-r--r--  feather-icon-demo.html
</span></span><span class="line"><span class="cl">-rw-r--r--  feather-picker.html
</span></span><span class="line"><span class="cl">-rw-r--r--  feather-picker.html.old
</span></span><span class="line"><span class="cl">-rw-r--r--  font-preview.html
</span></span><span class="line"><span class="cl">-rw-r--r--  font-preview2.html
</span></span><span class="line"><span class="cl">-rw-r--r--  font-preview3-roboto.html
</span></span><span class="line"><span class="cl">-rw-r--r--  font-preview4.html
</span></span><span class="line"><span class="cl">-rwxr-xr-x  gen-bg.py
</span></span><span class="line"><span class="cl">-rwxr-xr-x  generate-bg-wave.py
</span></span><span class="line"><span class="cl">-rwxr-xr-x  generate-bg.py
</span></span><span class="line"><span class="cl">-rwxr-xr-x  pal-preview.py
</span></span><span class="line"><span class="cl">-rwxr-xr-x  tag-audit.py
</span></span><span class="line"><span class="cl">-rwxr-xr-x  tag-scan.py
</span></span><span class="line"><span class="cl">lrwxrwxrwx  metaclean -&gt; ~/codelab/dotfiles/tools/metaclean/metaclean.py
</span></span><span class="line"><span class="cl">lrwxrwxrwx  sync-adminjitsu.sh -&gt; ~/codelab/dotfiles/bin/sync-adminjitsu.sh
</span></span></code></pre></div><br>
<p>Finally, I keep a <a href="/posts/ninjas/">Ninja Style-Dojo</a> — a more complete collection of snippets and conventions, with live HTML previews. I publish it on my site for fun, but it could just as easily stay a draft and serve as a private reference. Either way, it’s a handy place to store the raw Markdown and see the rendered results side-by-side in a browser — something I’d definitely set up again on a new site.</p>
<h3 id="why-it-works-for-me">Why it works for me</h3>
<ul>
<li><strong>Consistency</strong>: BOILERPLATE.txt guarantees my figures, captions, and buttons always look the same.</li>
<li><strong>Speed</strong>: Instead of hunting through old posts, I paste from my crib sheet or trigger an Espanso macro.</li>
<li><strong>Safety</strong>: Scratch/ is local-only and never published — so I can stash messy drafts, half-baked snippets, and test assets without worry.</li>
<li><strong>Organization</strong>: Keeping helper code and snippets inside the project keeps the mental overhead low — everything I need to write and polish a post lives right beside it.</li>
</ul>
<br>
<h2 id="espanso-macros">Espanso Macros</h2>
<p>Espanso is my free, cross-platform tool of choice for snippets that I can never quite keep in human RAM. (See <a href="/posts/espanso-and-friends/">Espanso and Friends</a> for more detail.) Typing something like <code>:figcenter</code> and having it expand into the following skeleton makes life so much easier</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">figure</span> <span class="na">style</span><span class="o">=</span><span class="s">&#34;text-align:center; margin: 1em auto;&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;&#34;</span> 
</span></span><span class="line"><span class="cl">       <span class="na">title</span><span class="o">=</span><span class="s">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">       <span class="na">alt</span><span class="o">=</span><span class="s">&#34;&#34;</span> 
</span></span><span class="line"><span class="cl">       <span class="na">style</span><span class="o">=</span><span class="s">&#34;display:block; margin:0 auto; width:min(100%, 600px); height:auto;&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">figcaption</span> <span class="na">style</span><span class="o">=</span><span class="s">&#34;font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">  <span class="p">&lt;/</span><span class="nt">figcaption</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">figure</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>Filling that out after triggering a macro is <em>far</em> quicker than retyping or hunting through past posts. My rule of thumb: whenever I catch myself digging through <strong>BOILERPLATE.txt</strong> or searching old posts too often, that’s my cue to promote it into an Espanso snippet.</p>
<p>I have a few like <code>:mailto</code>, <code>:btn-github</code>, <code>:btn-tag</code> and <code>:author</code> and espanso makes them much easier to use consistently. For an initial rough-draft I sometimes use <code>:lorem</code> to drop a placeholder block like the following:</p>
<p><em>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</em></p>
<p>I’ve used TextExpander on macOS for years, and it’s definitely the more polished program. But Espanso works, it’s cross-platform, and it’s free. Best of all, it doesn’t force you to tie a literal keylogger to a cloud service — a baffling change that pushed many of us to look elsewhere.</p>
<br>
<h2 id="page-bundles-are-where-its-at">Page Bundles are Where It’s At</h2>
<p>This wasn’t obvious to me at first, but the best way I’ve found to structure content in Hugo is by using <strong>page bundles</strong>. Instead of scattering your Markdown, images, and other assets across different directories, a page bundle keeps everything for a single post together in one folder.</p>
<p>In Hugo, that usually means a directory with an <code>index.md</code> and all its supporting files side-by-side:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">content/
</span></span><span class="line"><span class="cl">└── posts/
</span></span><span class="line"><span class="cl">    └── hugo-boilerplate/
</span></span><span class="line"><span class="cl">        ├── index.md
</span></span><span class="line"><span class="cl">        ├── hugo-logo-wide.svg
</span></span><span class="line"><span class="cl">        ├── screenshot.png
</span></span><span class="line"><span class="cl">        └── diagram.jpg
</span></span></code></pre></div><p>The magic: when you reference an image in your Markdown, Hugo automatically resolves relative paths from the bundle folder. That means you don’t need to manage long paths like <code>/images/2025/08/foo.png</code> — you just drop the asset into the bundle and link it as <code>![alt](screenshot.png)</code>.</p>
<p><span class="tag green">Pro-Tip:</span> Page bundles make it natural to keep figures, screenshots, and diagrams right alongside the post they belong to. When you move or archive the post, all its assets come with it.</p>
<p>I wish I’d embraced this earlier — it makes writing image-heavy posts far more portable and self-contained.</p>
<p>I still drop the occasional file in <code>/static</code> when there’s a reason, but page bundles keep the majority of posts tidy and script-friendly.</p>
<hr>
<h3 id="leaf-vs-branch-bundles">Leaf vs. Branch Bundles</h3>
<p>This was also confusing at first, but now it’s clear:</p>
<ul>
<li><strong>Leaf bundle</strong> → a folder with <code>index.md</code>. Perfect for a single post and its images.</li>
<li><strong>Branch bundle</strong> → a folder with <code>_index.md</code>. Used when the directory itself is a section and you want to organize child pages inside it.</li>
</ul>
<p>For most posts, leaf bundles are the sweet spot: everything neatly wrapped up with its assets. But branch bundles shine when you want to customize sections or taxonomy pages.</p>
<hr>
<h3 id="customizing-a-tag-page">Customizing a Tag Page</h3>
<p>One practical example of a branch bundle is a <strong>taxonomy page</strong>. Hugo automatically generates list pages for each taxonomy (like tags or categories) — see the <a href="https://gohugo.io/content-management/taxonomies/">taxonomy docs</a> for the full details. These pages are useful by default, but you can take them further by customizing them with an <code>_index.md</code>.</p>
<p>For example, my Hugo tag has its own logo and introduction at the top <a href="/tags/hugo/">as seen here</a></p>
<p>To make this work, I created an <code>_index.md</code> file in <code>content/tags/hugo/</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">title: &#34;Hugo&#34;
</span></span><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">br</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">p</span> <span class="na">align</span><span class="o">=</span><span class="s">&#34;center&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;hugo-logo-wide.svg&#34;</span> 
</span></span><span class="line"><span class="cl">       <span class="na">alt</span><span class="o">=</span><span class="s">&#34;Hugo static site generator logo&#34;</span> 
</span></span><span class="line"><span class="cl">       <span class="na">style</span><span class="o">=</span><span class="s">&#34;max-width:320px; margin-bottom:0.6em;&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">br</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Posts about Hugo**</span> — workflows, scripts, and experiments with the PaperMod theme.  
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="ge">*Here’s where I collect all my Hugo-related posts. From publishing scripts to styling tricks, these are my experiments in making Hugo work like a Unix-first tool.*</span>
</span></span></code></pre></div><p><span class="tag green">Pro-Tip:</span> Any taxonomy term (tags, categories, series, etc.) can be customized this way. Just create a folder under <code>content/&lt;taxonomy&gt;/&lt;term&gt;/</code> and drop in an <code>_index.md</code>. Hugo merges your intro with the generated list of posts.</p>
<br>
<h2 id="the-art-of-alt-text">The Art of Alt Text</h2>
<p>Markdown images are dead simple: copy the file into your page bundle and drop in</p>
<p><code>![image](image.jpg)</code></p>
<p>That’s fine for quick drafts, but limited. I’ve standardized on using <strong>HTML5 <code>&lt;figure&gt;</code> blocks</strong>. They’re more verbose, but Espanso and BOILERPLATE.txt make them trivial to insert. One big advantage is how naturally they support <strong>alt text</strong> and captions.</p>
<figure style="float:left; margin:0 1rem 1rem 0; width:clamp(260px, 45%, 200px);">
  <img src="blue-ninja2.png" 
       title="a title should appear when you hover over an image in your browser"
       alt="A little 8-bit arcade-style ninja in blue, mid-kick. Playful tone." 
       style="display:block; width:100%; height:auto;">
  <figcaption style="font-size:85%; color:#666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">figure</span> <span class="na">style</span><span class="o">=</span><span class="s">&#34;float:left; margin:0 1rem 1rem 0; width:clamp(260px, 45%, 200px);&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;blue-ninja2.png&#34;</span> 
</span></span><span class="line"><span class="cl">       <span class="na">title</span><span class="o">=</span><span class="s">&#34;a title should appear when you hover over an image in your browser&#34;</span>
</span></span><span class="line"><span class="cl">       <span class="na">alt</span><span class="o">=</span><span class="s">&#34;A little 8-bit arcade-style ninja in blue, mid-kick. Playful tone.&#34;</span> 
</span></span><span class="line"><span class="cl">       <span class="na">style</span><span class="o">=</span><span class="s">&#34;display:block; width:100%; height:auto;&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">figcaption</span> <span class="na">style</span><span class="o">=</span><span class="s">&#34;font-size:85%; color:#666; line-height:1.4; margin-top:0.4em;&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    insert caption here
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">figcaption</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">figure</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>The <code>alt</code> text here is:<br>
<em>&ldquo;A little 8-bit arcade-style ninja in blue, mid-kick. Playful tone.&rdquo;</em></p>
<div style="clear:both"></div>
<p>That’s short, descriptive, and conveys both content <strong>and</strong> feel.</p>
<p><span class="tag green">Pro-tip:</span> “When you float an image (<code>float:left</code> or <code>float:right</code>), add <code>&lt;div style=&quot;clear:both&quot;&gt;&lt;/div&gt;</code> immediately after the last paragraph you want to flow alongside it. That ends the float so the next block of text starts below the image instead of wrapping around it.”</p>
<h3 id="why-alt-text-matters">Why Alt Text Matters</h3>
<ul>
<li><strong>Accessibility</strong> → screen readers literally <em>speak</em> the <code>alt</code> text aloud. For some humans, the alt text <strong>is</strong> the image.</li>
<li><strong>SEO &amp; indexing</strong> → search engines rely on it to understand your images.</li>
<li><strong>Future-you</strong> → when you’re skimming raw Markdown years later, it’s your memory cue for what the image was.</li>
</ul>
<p>💡 Think of alt text as <strong>radio commentary</strong> for your images: short, vivid, and enough for someone who can’t see it to still get the point.</p>
<hr>
<h3 id="how-i-write-mine">How I Write Mine</h3>
<ol>
<li><strong>Concise</strong> → don’t narrate everything, just the essence.</li>
<li><strong>Tone</strong> → if the image is playful, somber, or technical, hint at that.</li>
<li><strong>Context</strong> → what’s relevant in <em>this</em> post, not in general.</li>
</ol>
<hr>
<h3 id="alt-vs-title-vs-figcaption"><code>alt</code> vs. <code>title</code> vs. <code>figcaption</code></h3>
<ul>
<li><code>alt</code> → mandatory description for accessibility &amp; indexing. Invisible unless needed.</li>
<li><code>title</code> → optional tooltip when you hover (not read by all screen readers). Best kept short.</li>
<li><code>figcaption</code> → visible, human-facing caption under the image.</li>
</ul>
<hr>
<h3 id="examples">Examples</h3>
<p><strong>Good:</strong></p>
<ul>
<li><code>alt=&quot;Diagram of a TCP handshake with SYN, SYN-ACK, and ACK arrows&quot;</code></li>
<li><code>alt=&quot;Screenshot of Hugo’s PaperMod theme with sidebar expanded&quot;</code></li>
<li><code>alt=&quot;Cheesy photo of a 90s era hacker in a leather outfit with oversized, early VR goggles&quot;</code></li>
</ul>
<p><strong>Bad:</strong></p>
<ul>
<li><code>alt=&quot;image123.png&quot;</code> (useless)</li>
<li><code>alt=&quot;screenshot&quot;</code> (too vague)</li>
<li><code>alt=&quot;This is a picture showing how computers work and it is very detailed and you should read the whole post to understand it&quot;</code> (too long and redundant)</li>
</ul>
<hr>
<h2 id="writing-workflow">Writing Workflow</h2>
<p>Over the past months I’ve stumbled into a workflow that works well for me. It’s nothing fancy, but it keeps me moving from raw ideas to finished posts without getting stuck in the weeds:</p>
<ul>
<li>
<p><strong>Brainstorm</strong><br>
I start with a physical journal on my desk — just date, time, and whatever’s on my mind. Tasks, quotes, fragments of ideas. If something excites me, it tends to show up on multiple days. Flipping back through recent pages often sparks connections. Sometimes I’ll highlight passages that feel like seeds for posts.</p>
</li>
<li>
<p><strong>Braindump / First Draft</strong><br>
When an idea is ready, I open a new leaf bundle in Hugo and start throwing content at the page. Sections, half-sentences, “insert brilliant analysis here.” Markdown helps because the draft already has structure — headers turn into an outline as I go. The point isn’t polish, just getting a skeleton that makes sense as a document.</p>
</li>
<li>
<p><strong>Second Draft</strong><br>
Here’s where the real work happens. I fill in the placeholders: research, repro steps, screenshots, code snippets. This is also where I fix flow problems and cut the junk. The second draft is “basically done” but still rough — good bones, not yet polished.</p>
</li>
<li>
<p><strong>Final Draft / Polish &amp; Publish</strong><br>
Last step is to read it top to bottom as a reader would. Does it <em>sound right</em>? Are the facts straight? I check typos and grammar (still hunting for the perfect VS Code spellchecker) and make sure my snippets render correctly. When it feels solid, I fire up my publish script and call it a day. This is usually the point where I realize I made a glaring error despite the previous steps and polish some more.</p>
</li>
</ul>
<p>This workflow isn’t revolutionary — brainstorm, draft, revise, polish — but the combination of Hugo + Markdown + my little tooling stack makes it <em>doable</em>. The tech gets out of the way so the writing can happen. That’s already hard enough without having to fight the platform.</p>
<p>Honestly, it feels closer to making a <strong>DIY punk zine</strong> than building a “traditional” website. That reminds me of what I liked about OS X Server’s old Wiki service: it made high-quality technical docs easy to produce without wrestling with the stack. Hugo has the same spirit, but with more power under the hood — full access to HTML, CSS, JS, and theming when I need it.</p>
<h2 id="tactical-inline-html">Tactical Inline HTML</h2>
<p>Markdown gets you 90% of the way, but sometimes you need finer control — a <code>&lt;figure&gt;</code> block, a <code>&lt;div&gt;</code> wrapper, a styled <code>&lt;span&gt;</code>. That’s where inline HTML comes in.</p>
<p>By default Hugo uses the <strong>Goldmark</strong> Markdown processor, which <em>escapes</em> raw HTML unless you tell it otherwise. To enable inline HTML, set this in your site config:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">markup</span><span class="p">.</span><span class="nx">goldmark</span><span class="p">.</span><span class="nx">renderer</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">unsafe</span> <span class="p">=</span> <span class="kc">true</span>
</span></span></code></pre></div><p>Now your raw HTML will render directly in posts.</p>
<h3 id="caveats">Caveats</h3>
<ul>
<li><strong>Trust yourself only</strong>: don’t paste untrusted HTML snippets (security risk).</li>
<li><strong>Keep it minimal</strong>: inline HTML is great for captions, figures, or buttons — but if you find yourself nesting <code>&lt;div&gt;</code>s like it’s 2003, consider moving it into a shortcode or a partial.</li>
<li><strong>Test on mobile</strong>: what looks good in desktop Chrome might break layout in a narrow column.</li>
</ul>
<h3 id="examples-1">Examples</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">span</span> <span class="na">style</span><span class="o">=</span><span class="s">&#34;color:#7D83B9;&#34;</span><span class="p">&gt;</span>inline highlight<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">p</span> <span class="na">style</span><span class="o">=</span><span class="s">&#34;text-align:center;&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;/tags/hugo&#34;</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;button&#34;</span><span class="p">&gt;</span>🧾 View Other Hugo Posts<span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>Inline HTML is a sharp tool: overdo it and your Markdown turns messy, but sprinkled tactically it can save you from endless CSS hacks or theme surgery.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Hugo + Markdown has been a great excuse to tinker, automate, and—most importantly—actually write. That&rsquo;s my story, and I’m sticking to it.</p>
<p>If you’ve read this far, thanks for stopping by. I hope some of these odd little practices spark ideas for your own setup.</p>
<p style="text-align:left;">
  <a href="/tags/hugo" class="button">🧾 View Other Hugo Posts</a>
</p>
<p>Have a tip or trick? Did I miss something? I&rsquo;d love to hear about it!
Email me: <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Oddball Files</title>
      <link>https://adminjitsu.com/posts/oddball-files/</link>
      <pubDate>Thu, 28 Aug 2025 14:05:09 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/oddball-files/</guid>
      <description>Explore the strange and useful world of Unix special files. Learn how /dev/null, /dev/zero, /dev/full, random devices, and the /proc and sysctl interfaces work under the hood, where they live in the kernel source, and how to use them safely in scripts and troubleshooting.</description>
      <content:encoded><![CDATA[<h2 id="everything-is-a-file">Everything is a file</h2>
<p>UNIX is designed around a simple concept: <em>everything is a file.</em>  But the ones I enjoy most are the oddballs — special files that do curious things yet can still be poked, prodded, and <code>cat</code>-ed like any other. Let’s explore a few of them.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="blue-wizard.png" 
       alt="an 8-bit style wizard in blue robes holding a glowing, open book" 
       style="display:block; margin:0 auto; width:min(100%, 384px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    no wizards were harmed in the making of this post
  </figcaption>
</figure>
<h3 id="file-types">File Types</h3>
<blockquote>
<p><em>&ldquo;Some stirring may be necessary to achieve proper consistency.&rdquo;</em><br>
— fortune(6)</p></blockquote>
<p>The phrase <em>everything is a file</em> isn’t just a slogan — it’s how UNIX presents nearly every interface:</p>
<ul>
<li><strong>Regular files</strong> — your documents, configs, logs.</li>
<li><strong>Directories</strong> — just special files mapping names to inodes.</li>
<li><strong>Character devices</strong> — like <code>/dev/null</code> or <code>/dev/tty</code>, providing byte streams to hardware or virtual drivers.</li>
<li><strong>Block devices</strong> — like <code>/dev/sda</code> (a disk), where the kernel maps file I/O into fixed-size blocks.</li>
<li><strong>Sockets</strong> — files that represent network endpoints (check <code>/var/run/</code> on a Unixy system).</li>
<li><strong>Pipes (FIFOs)</strong> — files that connect processes (<code>mkfifo</code> makes one).</li>
<li><strong>Procfs/sysfs entries</strong> — “files” that show you kernel state when you <code>cat</code> them.</li>
</ul>
<p>That’s why tools like <code>cat</code>, <code>echo</code>, or <code>dd</code> work on such a wild variety of things — whether you’re streaming bytes from a terminal, a pseudo-random generator, or the kernel itself.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">└─$ ls -al /dev/null
</span></span><span class="line"><span class="cl">crw-rw-rw- <span class="m">1</span> root root 1,3 Aug <span class="m">16</span> 16:00 /dev/null
</span></span><span class="line"><span class="cl"><span class="c1">#           ^    ^    ^  ^^^</span>
</span></span><span class="line"><span class="cl"><span class="c1">#           |    |    |  major,minor device numbers</span>
</span></span><span class="line"><span class="cl"><span class="c1">#           |    |    group</span>
</span></span><span class="line"><span class="cl"><span class="c1">#           |    owner</span>
</span></span><span class="line"><span class="cl"><span class="c1">#           hardlink count</span>
</span></span><span class="line"><span class="cl"><span class="c1">#character device</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># A block device: your first disk</span>
</span></span><span class="line"><span class="cl">└─$ ls -l /dev/sda
</span></span><span class="line"><span class="cl">brw-rw---- <span class="m">1</span> root disk 8,0 Aug <span class="m">29</span> 10:00 /dev/sda
</span></span><span class="line"><span class="cl"><span class="c1"># b = block device</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># A directory: just another kind of file</span>
</span></span><span class="line"><span class="cl">└─$ ls -ld /etc
</span></span><span class="line"><span class="cl">drwxr-xr-x <span class="m">123</span> root root <span class="m">4096</span> Aug <span class="m">29</span> 09:59 /etc
</span></span><span class="line"><span class="cl"><span class="c1"># d = directory</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># A FIFO (named pipe): file that connects processes</span>
</span></span><span class="line"><span class="cl">└─$ mkfifo mypipe <span class="o">&amp;&amp;</span> ls -l mypipe
</span></span><span class="line"><span class="cl">prw-r--r-- <span class="m">1</span> kevin kevin <span class="m">0</span> Aug <span class="m">29</span> 10:01 mypipe
</span></span><span class="line"><span class="cl"><span class="c1"># p = pipe</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># A socket: file that represents a communication endpoint</span>
</span></span><span class="line"><span class="cl">└─$ ls -l /var/run/docker.sock
</span></span><span class="line"><span class="cl">srw-rw---- <span class="m">1</span> root docker <span class="m">0</span> Aug <span class="m">29</span> 10:00 /var/run/docker.sock
</span></span><span class="line"><span class="cl"><span class="c1"># s = socket</span>
</span></span></code></pre></div><p>Let&rsquo;s explore!</p>
<h2 id="devnull--the-bit-bucket"><code>/dev/null</code> — The Bit Bucket</h2>
<p>A special file that represents nothing. Writing to <code>/dev/null</code> discards data forever; reading it returns immediate EOF.</p>
<p>In the upstream Linux kernel tree (Linus’s repo), <code>/dev/null</code> lives in <strong><code>drivers/char/mem.c</code></strong>. The device’s behavior is wired through the <code>file_operations</code> table:</p>
<blockquote>
<p>Browse: <a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/char/mem.c"><code>drivers/char/mem.c</code> (mainline)</a></p></blockquote>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cm">/* ... inside drivers/char/mem.c ... */</span>
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="kt">ssize_t</span> <span class="nf">read_null</span><span class="p">(</span><span class="k">struct</span> <span class="n">file</span> <span class="o">*</span><span class="n">file</span><span class="p">,</span> <span class="kt">char</span> <span class="n">__user</span> <span class="o">*</span><span class="n">buf</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                         <span class="kt">size_t</span> <span class="n">count</span><span class="p">,</span> <span class="kt">loff_t</span> <span class="o">*</span><span class="n">ppos</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span> <span class="cm">/* EOF immediately */</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="kt">ssize_t</span> <span class="nf">write_null</span><span class="p">(</span><span class="k">struct</span> <span class="n">file</span> <span class="o">*</span><span class="n">file</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="n">__user</span> <span class="o">*</span><span class="n">buf</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                          <span class="kt">size_t</span> <span class="n">count</span><span class="p">,</span> <span class="kt">loff_t</span> <span class="o">*</span><span class="n">ppos</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">count</span><span class="p">;</span> <span class="cm">/* pretend we wrote everything */</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="k">const</span> <span class="k">struct</span> <span class="n">file_operations</span> <span class="n">null_fops</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">read</span>  <span class="o">=</span> <span class="n">read_null</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">write</span> <span class="o">=</span> <span class="n">write_null</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="cm">/* other ops may be present depending on kernel version */</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span></code></pre></div><h3 id="why-it-matters">Why it matters</h3>
<p>Because <code>/dev/null</code> is “just a file,” any tool that can write to a file can be silenced or stress-tested without special flags.</p>
<h3 id="practical-tricks">Practical tricks</h3>
<p><span class="tag blue">Pro-Tip:</span> <strong>Silence everything</strong> (stdout <em>and</em> stderr):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">some-noisy-tool &gt; /dev/null 2&gt;<span class="p">&amp;</span><span class="m">1</span>
</span></span></code></pre></div><p><strong>Measure syscall overhead</strong> (it’ll still touch the kernel fast path):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">strace -c cat /dev/null
</span></span></code></pre></div><p><strong>High-speed “write” benchmark</strong> (no disk I/O happens):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">dd <span class="k">if</span><span class="o">=</span>/dev/zero <span class="nv">of</span><span class="o">=</span>/dev/null <span class="nv">bs</span><span class="o">=</span>64M <span class="nv">count</span><span class="o">=</span><span class="m">128</span> <span class="nv">status</span><span class="o">=</span>progress
</span></span></code></pre></div><p><strong>Drop output in pipelines without branching:</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">make 2&gt;build.err &gt; /dev/null
</span></span></code></pre></div><p><strong>Pretend-success sink in scripts</strong> (command returns 0 if the last stage does):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">generate-stats <span class="p">|</span> tee /dev/null
</span></span></code></pre></div><h3 id="odds--ends">Odds &amp; ends</h3>
<ul>
<li>Historically, <code>/dev/null</code> is a <strong>character device</strong> (major <code>1</code>, minor varies by table) implemented by the kernel’s “memory” driver alongside <code>/dev/zero</code>, <code>/dev/full</code>, and friends.</li>
<li>The exact function names around <code>null_fops</code> may differ slightly across kernel versions (e.g., <code>llseek</code>/<code>splice_write</code> entries); the essence remains: <strong>reads return 0, writes report success</strong>.</li>
</ul>
<blockquote>
<p>Canonical source: <a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/char/mem.c"><code>drivers/char/mem.c</code> in Linus’s tree</a></p></blockquote>
<p><strong>Some detailed explanations I like</strong></p>
<ul>
<li><a href="https://vccolombo.github.io/blog/creating-my-own-dev-null/">Creating My Own /dev/null</a></li>
<li><a href="https://www.xml.com/ldd/chapter/book/ch16.html">Explanation of special devices</a></li>
</ul>
<hr>
<h2 id="devzero--the-infinite-stream-of-zeros"><code>/dev/zero</code> — The Infinite Stream of Zeros</h2>
<p>Need endless null bytes? <code>/dev/zero</code> is your friend. Every read returns <code>\0</code> forever.</p>
<span class="tag orange">Why use it?</span>
<ul>
<li>Create blank disk images or memory files.</li>
<li>Initialize storage with a known pattern.</li>
<li>Quick way to generate padding.</li>
</ul>
<p>Examples:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Create a 100 MB blank file</span>
</span></span><span class="line"><span class="cl">dd <span class="k">if</span><span class="o">=</span>/dev/zero <span class="nv">of</span><span class="o">=</span>blank.img <span class="nv">bs</span><span class="o">=</span>1M <span class="nv">count</span><span class="o">=</span><span class="m">100</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Wipe a disk partition with zeros (careful!)</span>
</span></span><span class="line"><span class="cl">dd <span class="k">if</span><span class="o">=</span>/dev/zero <span class="nv">of</span><span class="o">=</span>/dev/sdX <span class="nv">bs</span><span class="o">=</span>1M <span class="nv">status</span><span class="o">=</span>progress
</span></span></code></pre></div><ul>
<li><code>/dev/zero</code> implementation:<br>
<a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/char/mem.c#n631">drivers/char/mem.c — zero_fops</a></li>
</ul>
<hr>
<h2 id="devfull--the-disk-full-device"><code>/dev/full</code> — The &ldquo;Disk Full&rdquo; Device</h2>
<p>The counterpart to the zero device: <code>/dev/full</code> rejects every write with <code>ENOSPC</code> (“No space left on device”).<br>
Reads return zeros (like <code>/dev/zero</code>), but <strong>writes always fail</strong>.</p>
<span class="tag orange">Why use it?</span>
<ul>
<li>Test how software behaves when disks are full without filling up a disk for real.</li>
<li>Ensure your error handling doesn’t silently corrupt data.</li>
<li>Force applications to trigger out-of-space recovery paths.</li>
</ul>
<p>Examples:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Simulate a write failure</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="nb">test</span> &gt; /dev/full
</span></span><span class="line"><span class="cl"><span class="c1"># bash: echo: write error: No space left on device</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Pipe data into /dev/full to trigger error handling in a script</span>
</span></span><span class="line"><span class="cl">tar cf - /etc <span class="p">|</span> cat &gt; /dev/full
</span></span></code></pre></div><ul>
<li><code>/dev/full</code> implementation:<br>
<a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/char/mem.c#n678">drivers/char/mem.c — full_fops</a></li>
</ul>
<hr>
<h2 id="devrandom-and-devurandom--entropy-on-tap"><code>/dev/random</code> and <code>/dev/urandom</code> — Entropy on Tap</h2>
<p>Both <code>/dev/random</code> and <code>/dev/urandom</code> draw from the kernel’s cryptographic random number generator, which is constantly fed with entropy from <strong>noisy hardware events</strong>: interrupt timing jitter, disk and network latencies, keyboard/mouse input, and—if available—CPU hardware RNG instructions like Intel’s <code>RDRAND</code> or <code>RDSEED</code>. These unpredictable signals are mixed into a pool, then stretched into high-quality random data with a cryptographic PRNG.</p>
<p>There are actually two &ldquo;random&rdquo; devices on Linux:</p>
<ul>
<li><strong><code>/dev/random</code></strong> historically blocks if the kernel’s entropy pool is low. On modern Linux (5.6+), it only blocks until the RNG is fully initialized (very early at boot).</li>
<li><strong><code>/dev/urandom</code></strong> never blocks, stretching whatever entropy is available.</li>
<li>Since Linux 5.6, both use the same ChaCha20-based core, with <code>/dev/random</code> acting mostly as a “blocking front end.”</li>
</ul>
<h3 id="source-code">Source code</h3>
<ul>
<li>Both are implemented in <strong><code>drivers/char/random.c</code></strong> in the mainline tree:<br>
<a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/char/random.c">drivers/char/random.c (mainline)</a></li>
</ul>
<p>Key structures:</p>
<ul>
<li><code>random_fops</code> (for <code>/dev/random</code>)</li>
<li><code>urandom_fops</code> (for <code>/dev/urandom</code>)</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cm">/* snippet from drivers/char/random.c (modern kernels) */</span>
</span></span><span class="line"><span class="cl"><span class="k">const</span> <span class="k">struct</span> <span class="n">file_operations</span> <span class="n">random_fops</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">read_iter</span>   <span class="o">=</span> <span class="n">random_read_iter</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">write_iter</span>  <span class="o">=</span> <span class="n">random_write_iter</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">poll</span>        <span class="o">=</span> <span class="n">random_poll</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">unlocked_ioctl</span> <span class="o">=</span> <span class="n">random_ioctl</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">compat_ioctl</span>   <span class="o">=</span> <span class="n">compat_ptr_ioctl</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">fasync</span>      <span class="o">=</span> <span class="n">random_fasync</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">llseek</span>      <span class="o">=</span> <span class="n">noop_llseek</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">splice_read</span> <span class="o">=</span> <span class="n">copy_splice_read</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">splice_write</span><span class="o">=</span> <span class="n">iter_file_splice_write</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">const</span> <span class="k">struct</span> <span class="n">file_operations</span> <span class="n">urandom_fops</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">read_iter</span>   <span class="o">=</span> <span class="n">urandom_read_iter</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">write_iter</span>  <span class="o">=</span> <span class="n">random_write_iter</span><span class="p">,</span> <span class="cm">/* same writer path */</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">unlocked_ioctl</span> <span class="o">=</span> <span class="n">random_ioctl</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">fasync</span>      <span class="o">=</span> <span class="n">random_fasync</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">llseek</span>      <span class="o">=</span> <span class="n">noop_llseek</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">splice_read</span> <span class="o">=</span> <span class="n">copy_splice_read</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">splice_write</span><span class="o">=</span> <span class="n">iter_file_splice_write</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span></code></pre></div><p>Behind these fops (file operations) are the kernel’s cryptographic RNG (Random Number Generator) functions, feeding data from entropy pools mixed with device interrupts, timings, and other noise sources.</p>
<span class="tag orange">Why use it?</span>
<ul>
<li>Secure key generation (<code>ssh-keygen</code>, <code>gpg</code>, etc).</li>
<li>Random test data (<code>head -c 16 /dev/urandom</code>).</li>
<li>Checking system entropy levels via <code>/proc/sys/kernel/random/entropy_avail</code>.</li>
</ul>
<p>Examples:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Generate a 16-character password</span>
</span></span><span class="line"><span class="cl">tr -dc <span class="s1">&#39;A-Za-z0-9&#39;</span> &lt; /dev/urandom <span class="p">|</span> head -c <span class="m">16</span> <span class="p">;</span> <span class="nb">echo</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check entropy pool size</span>
</span></span><span class="line"><span class="cl">cat /proc/sys/kernel/random/entropy_avail
</span></span><span class="line"><span class="cl"><span class="c1"># Note: on modern kernels this usually sits at 256 by design; it doesn’t mean “low entropy.”</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Grab 32 random bytes as hex</span>
</span></span><span class="line"><span class="cl">od -An -tx1 -N32 /dev/random
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 20 random digits (0–9)</span>
</span></span><span class="line"><span class="cl">tr -dc <span class="s1">&#39;0-9&#39;</span> &lt; /dev/urandom <span class="p">|</span> head -c <span class="m">20</span> <span class="p">;</span> <span class="nb">echo</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 20 random letters (A–Z, a–z)</span>
</span></span><span class="line"><span class="cl">tr -dc <span class="s1">&#39;A-Za-z&#39;</span> &lt; /dev/urandom <span class="p">|</span> head -c <span class="m">20</span> <span class="p">;</span> <span class="nb">echo</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># simulate a 6-sided die roll</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="k">$((</span> <span class="o">(</span> <span class="k">$(</span>od -An -N2 -i /dev/urandom<span class="k">)</span> <span class="o">%</span> <span class="m">6</span> <span class="o">)</span> <span class="o">+</span> <span class="m">1</span> <span class="k">))</span>
</span></span></code></pre></div><hr>
<figure style="text-align:center; margin: 1em auto;">
  <img src="lightning-wizard.png" 
       alt="an 8-bit, arcade sprite style wizard in black robes, casting a lightning bolt. playful tone" 
       style="display:block; margin:0 auto; width:min(100%, 384px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
<h2 id="other-oddballs-in-dev">Other Oddballs in <code>/dev</code></h2>
<p>Not everything in <code>/dev</code> is as widely recognized as <code>/dev/null</code> or <code>/dev/urandom</code>. Many entries serve specialized purposes: some expose useful system interfaces, others exist mainly for testing or debugging, and a few can be hazardous if misused.</p>
<hr>
<h3 id="devtty--your-terminal"><code>/dev/tty</code> — Your Terminal</h3>
<p>Always points to <em>your</em> controlling terminal.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Hello from script land&#34;</span> &gt; /dev/tty
</span></span></code></pre></div><p>Handy when stdout is redirected and you still want to talk to the user.</p>
<hr>
<h3 id="devpts--pseudo-terminals"><code>/dev/pts/*</code> — Pseudo Terminals</h3>
<p>Created dynamically for each terminal session (SSH, tmux, etc.).</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Wake up, Neo...&#34;</span> &gt; /dev/pts/2
</span></span></code></pre></div><p>Yes, you can write text directly into someone’s session—<strong>if you have permission</strong>.<br>
Pseudo-terminal devices are owned by the session user and typically mode <code>rw--w----</code> (<code>0620</code>), group <code>tty</code>. That means only the owner (or root) can write to it.</p>
<hr>
<h3 id="devconsole--the-big-screen"><code>/dev/console</code> — The Big Screen</h3>
<p>The system console. Kernel boot logs and critical errors go here.<br>
You can also send your own (assuming you are root):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;System maintenance in 5 minutes&#34;</span> <span class="p">|</span> sudo tee /dev/console
</span></span></code></pre></div><p>An example of this in the wild can be seen when scheduling a reboot with<br>
<code>sudo shutdown -r +10</code>. Under the hood, <code>shutdown</code> calls <strong>wall(1)</strong> (or an equivalent) to broadcast a warning message. That message is written both to <code>/dev/console</code> and to all connected TTYs.</p>
<p>This behavior is a legacy of the multi-user era, when it was essential to notify other logged-in users before rebooting or performing disruptive maintenance. Even today, it’s a practical demonstration of how the console and TTY devices are just files that can be written to.</p>
<hr>
<h3 id="devkmsg--talk-to-the-kernel-log"><code>/dev/kmsg</code> — Talk to the Kernel Log</h3>
<p>Write straight into <code>dmesg</code> from userspace:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Injected log message from userspace&#34;</span> <span class="p">|</span> sudo tee /dev/kmsg
</span></span></code></pre></div><p>Great for debugging custom scripts or testing log monitoring (as root) although <a href="https://man7.org/linux/man-pages/man1/logger.1.html">logger(1)</a> is the more portable and cleaner alternative.</p>
<hr>
<h3 id="devmem-and-devkmem--raw-memory"><code>/dev/mem</code> and <code>/dev/kmem</code> — Raw Memory</h3>
<p>Give direct access to physical and kernel virtual memory.<br>
<span class="inline-highlight">Mostly disabled</span> on modern systems for security. Historically used for debuggers, kernel hacking… and crashing your system.</p>
<blockquote>
<p>⚠️ Danger: one wrong write and the kernel panics.</p></blockquote>
<hr>
<h3 id="devloop--loopback-block-devices"><code>/dev/loop*</code> — Loopback Block Devices</h3>
<p>Loop devices let you treat an ordinary file as if it were a real disk.<br>
Anything you write into the mounted filesystem is actually stored <em>inside that file</em>, with the kernel handling the translation.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Create a 10MB blank file</span>
</span></span><span class="line"><span class="cl">dd <span class="k">if</span><span class="o">=</span>/dev/zero <span class="nv">of</span><span class="o">=</span>disk.img <span class="nv">bs</span><span class="o">=</span>1M <span class="nv">count</span><span class="o">=</span><span class="m">10</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Map to a loop device and make an ext4 filesystem</span>
</span></span><span class="line"><span class="cl">sudo losetup /dev/loop0 disk.img
</span></span><span class="line"><span class="cl">sudo mkfs.ext4 /dev/loop0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Mount, write some data, then unmount</span>
</span></span><span class="line"><span class="cl">sudo mount /dev/loop0 /mnt
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;hello loopback&#34;</span> <span class="p">|</span> sudo tee /mnt/hello.txt
</span></span><span class="line"><span class="cl">sudo umount /mnt
</span></span><span class="line"><span class="cl">sudo losetup -d /dev/loop0
</span></span></code></pre></div><p>At this point, <code>disk.img</code> is an ext4 filesystem in a file.<br>
If you peek inside with <code>hexdump</code> or <code>strings</code>, you can see its structure:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Look for ext4 magic numbers</span>
</span></span><span class="line"><span class="cl">hexdump -C disk.img <span class="p">|</span> grep <span class="s1">&#39;53 ef&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Typical output:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 00000400  53 ef 01 00 ...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Or scan for human-readable bits</span>
</span></span><span class="line"><span class="cl">strings disk.img <span class="p">|</span> head
</span></span><span class="line"><span class="cl"><span class="c1"># Output might show journal headers, superblock info, etc.</span>
</span></span></code></pre></div><p>Reattach it to a loop device later and your <code>hello.txt</code> will still be there.<br>
This is the same trick used for ISO files, qcow2 VM images, and container layers.</p>
<hr>
<h3 id="devnettun--virtual-networking"><code>/dev/net/tun</code> — Virtual Networking</h3>
<p>The TUN/TAP <em>clone device</em>. Userland programs open <code>/dev/net/tun</code> and use an <code>ioctl(TUNSETIFF)</code> to request either:</p>
<ul>
<li><strong>TUN</strong> (layer-3, IP packets)</li>
<li><strong>TAP</strong> (layer-2, Ethernet frames)</li>
</ul>
<p>Commonly used by VPNs like WireGuard and OpenVPN, and by virtualization tools that need virtual NICs.<br>
<em>(Docker’s default networking uses veth pairs and bridges, not TUN/TAP.)</em></p>
<hr>
<h3 id="devshm--shared-memory"><code>/dev/shm</code> — Shared Memory</h3>
<p>Technically a tmpfs mounted under <code>/dev</code>. It’s RAM-backed storage, cleared on reboot.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;fast scratch data&#34;</span> &gt; /dev/shm/test.txt
</span></span><span class="line"><span class="cl">cat /dev/shm/test.txt
</span></span></code></pre></div><blockquote>
<p><span class="tag blue">Pro-Tip:</span> <code>/dev/shm</code> is like a built-in RAM disk for temp files.<br>
Many tools honor the <code>TMPDIR</code> variable, so you can speed them up by pointing it here:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Run tests with fast temp storage in RAM</span>
</span></span><span class="line"><span class="cl"><span class="nv">TMPDIR</span><span class="o">=</span>/dev/shm pytest -q
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Or give SQLite an all-RAM temp area</span>
</span></span><span class="line"><span class="cl"><span class="nv">SQLITE_TMPDIR</span><span class="o">=</span>/dev/shm sqlite3 mydb.sqlite
</span></span></code></pre></div><p>Great for test runs, builds, and scratch data that doesn’t need to survive a reboot.</p></blockquote>
<hr>
<figure style="text-align:center; margin: 1em auto;">
  <img src="fireball-wizard.png" 
       alt="an 8-bit, arcade sprite style wizard in red casting a fireball. playful tone" 
       style="display:block; margin:0 auto; width:min(100%, 400px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
<h2 id="proc--the-kernels-diary"><code>/proc</code> — The Kernel’s Diary</h2>
<p>Unlike <code>/dev</code>, which is about devices, <code>/proc</code> is a <strong>virtual filesystem</strong> exposing kernel internals.<br>
Files here are interfaces into live kernel state, assembled dynamically whenever you query them.</p>
<p>Every process gets its own subdirectory (<code>/proc/&lt;pid&gt;</code>), plus global kernel stats.</p>
<p>The <code>/proc</code> filesystem implementation lives in the Linux kernel tree under:</p>
<p>🔗 <a href="https://elixir.bootlin.com/linux/latest/source/fs/proc">fs/proc/ — Linux source (Elixir cross-referencer)</a></p>
<hr>
<h3 id="common-tricks">Common tricks</h3>
<span class="tag orange">Why use it?</span>
<ul>
<li>Inspect process details without <code>ps</code> or <code>top</code>.</li>
<li>Read kernel parameters on the fly.</li>
<li>Script system monitoring without extra tools.</li>
</ul>
<p>Examples:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Show command line of your shell, where $$ is a special variable that expands to the current shell&#39;s process ID (PID)</span>
</span></span><span class="line"><span class="cl">cat /proc/<span class="nv">$$</span>/cmdline
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show process status (fields like State, Threads, Memory), where 12345 is any running PID</span>
</span></span><span class="line"><span class="cl">cat /proc/12345/status <span class="p">|</span> head
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># System uptime (in seconds)</span>
</span></span><span class="line"><span class="cl">awk <span class="s1">&#39;{print $1}&#39;</span> /proc/uptime
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Kernel version</span>
</span></span><span class="line"><span class="cl">cat /proc/version
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># List all loaded modules</span>
</span></span><span class="line"><span class="cl">cat /proc/modules
</span></span></code></pre></div><hr>
<h3 id="tweakable-knobs">Tweakable knobs</h3>
<p>Many settings in <code>/proc/sys</code> are writable, same as <code>sysctl</code>.<br>
This is the interface Linux exposes for runtime performance tuning, though most users never need to touch it — the kernel ships with sensible defaults (often tuned differently between server and desktop distributions).</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Enable IPv4 forwarding (required for routers, VPNs, containers)</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="m">1</span> <span class="p">|</span> sudo tee /proc/sys/net/ipv4/ip_forward
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check current swappiness (0–100, default ~60)</span>
</span></span><span class="line"><span class="cl">cat /proc/sys/vm/swappiness
</span></span><span class="line"><span class="cl"><span class="c1"># Lower = keep apps in RAM longer, swap only when memory is tight</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Higher = free RAM sooner, keep larger disk cache</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Set swappiness to 10 (more &#34;desktop-friendly&#34; behavior)</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="m">10</span> <span class="p">|</span> sudo tee /proc/sys/vm/swappiness
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Adjust maximum number of open file descriptors</span>
</span></span><span class="line"><span class="cl">cat /proc/sys/fs/file-max
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="m">2097152</span> <span class="p">|</span> sudo tee /proc/sys/fs/file-max
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Tune TCP keepalive time (seconds of idle before probes are sent)</span>
</span></span><span class="line"><span class="cl">cat /proc/sys/net/ipv4/tcp_keepalive_time
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="m">300</span> <span class="p">|</span> sudo tee /proc/sys/net/ipv4/tcp_keepalive_time
</span></span></code></pre></div><h3 id="-where-to-find-them-all">📚 Where to find them all</h3>
<p>The full catalog of tunables lives under <code>/proc/sys</code>, grouped by subsystem (<code>vm/</code>, <code>net/</code>, <code>kernel/</code>, <code>fs/</code>, …).</p>
<p>Canonical documentation lives in the kernel source tree:</p>
<ul>
<li><a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/admin-guide/sysctl">Documentation/admin-guide/sysctl/</a></li>
<li>Each subsystem has its own file, e.g.
<ul>
<li><a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/admin-guide/sysctl/vm.rst">Documentation/admin-guide/sysctl/vm.rst</a></li>
<li><a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/admin-guide/sysctl/net.rst">Documentation/admin-guide/sysctl/net.rst</a></li>
</ul>
</li>
</ul>
<blockquote>
<p>⚠️ Changes made directly in <code>/proc/sys</code> last only until reboot.<br>
To make them permanent, set them via <code>sysctl</code> (e.g. <code>sysctl -w vm.swappiness=10</code>) and add entries in <code>/etc/sysctl.conf</code> or drop-in files under <code>/etc/sysctl.d/</code>.</p></blockquote>
<hr>
<h3 id="oddities-worth-knowing">Oddities worth knowing</h3>
<p>Some <code>/proc</code> files are especially handy to peek at directly:</p>
<ul>
<li>
<p><strong><code>/proc/cpuinfo</code></strong> — CPU model, flags, and per-core info.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">grep <span class="s1">&#39;model name&#39;</span> /proc/cpuinfo <span class="p">|</span> uniq
</span></span></code></pre></div><p><em>(What <code>lscpu</code> uses under the hood.)</em></p>
</li>
<li>
<p><strong><code>/proc/meminfo</code></strong> — granular memory stats (RAM, swap, buffers, caches).</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">head -5 /proc/meminfo
</span></span></code></pre></div><p><em>(Basis for the <code>free</code> command.)</em></p>
</li>
<li>
<p><strong><code>/proc/loadavg</code></strong> — load averages as shown by <code>uptime</code>. First three fields are 1, 5, and 15-minute averages.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat /proc/loadavg
</span></span></code></pre></div></li>
<li>
<p><strong><code>/proc/filesystems</code></strong> — list of supported filesystem types.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat /proc/filesystems <span class="p">|</span> column
</span></span></code></pre></div><p><em>(Prefixed with <code>nodev</code> if no block device is required.)</em></p>
</li>
<li>
<p><strong><code>/proc/sysrq-trigger</code></strong> — write magic letters here to invoke SysRq kernel actions.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> b <span class="p">|</span> sudo tee /proc/sysrq-trigger   <span class="c1"># reboot immediately</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> m <span class="p">|</span> sudo tee /proc/sysrq-trigger   <span class="c1"># dump memory info to dmesg</span>
</span></span></code></pre></div><p>⚠️ Dangerous if you don’t know the key sequences — it’s a raw escape hatch.</p>
</li>
</ul>
<hr>
<h3 id="source-code-1">Source code</h3>
<p>Implemented in the kernel’s <strong><code>fs/proc/</code></strong> directory:</p>
<ul>
<li><a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/proc/base.c">fs/proc/base.c</a> — per-process files like <code>/proc/&lt;pid&gt;/status</code>.</li>
<li><a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/proc/proc_sysctl.c">fs/proc/proc_sysctl.c</a> — the <code>/proc/sys</code> interface.</li>
<li><a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/filesystems/proc.rst">Documentation/filesystems/proc.rst</a> — canonical docs in-tree.</li>
</ul>
<hr>
<br>
<h2 id="bonus-round-bsdmacos-sysctl">Bonus Round: BSD/macOS <code>sysctl</code></h2>
<blockquote>
<p><em>&ldquo;It&rsquo;s more fun to be a pirate than to join the navy.&rdquo;</em><br>
— Steve Jobs</p></blockquote>
<p>While Linux uses <code>/proc</code>, the BSD family (FreeBSD, NetBSD, OpenBSD) and Darwin/macOS expose kernel state through a <strong><code>sysctl</code> tree</strong>.</p>
<p>At the user level you call the <code>sysctl(3)</code> function, whose prototype is:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">sysctl</span><span class="p">(</span><span class="kt">int</span> <span class="o">*</span><span class="n">name</span><span class="p">,</span> <span class="n">u_int</span> <span class="n">namelen</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">           <span class="kt">void</span> <span class="o">*</span><span class="n">oldp</span><span class="p">,</span> <span class="kt">size_t</span> <span class="o">*</span><span class="n">oldlenp</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">           <span class="kt">void</span> <span class="o">*</span><span class="n">newp</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">newlen</span><span class="p">);</span>
</span></span></code></pre></div><ul>
<li><code>name</code> is an integer array describing the MIB path (e.g. <code>{ CTL_HW, HW_NCPU }</code>).</li>
<li><code>oldp</code> / <code>oldlenp</code> point to a buffer for the current value.</li>
<li><code>newp</code> / <code>newlen</code> optionally provide a new value to set.</li>
</ul>
<p>Inside the kernel, nodes are registered in a tree of <strong><code>struct sysctl_oid</code></strong> objects (FreeBSD/macOS). Each OID describes a tunable or info node (name, type, handler function) that supplies the value or applies a change.</p>
<hr>
<h3 id="example-bsdmacos-sysctl-calls-in-c">Example: BSD/macOS sysctl calls in C</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">mib</span><span class="p">[</span><span class="mi">2</span><span class="p">];</span>
</span></span><span class="line"><span class="cl"><span class="kt">size_t</span> <span class="n">len</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">ncpu</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">mib</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="n">CTL_HW</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="n">mib</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="n">HW_NCPU</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="n">len</span> <span class="o">=</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">ncpu</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nf">sysctl</span><span class="p">(</span><span class="n">mib</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">ncpu</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">len</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">perror</span><span class="p">(</span><span class="s">&#34;sysctl&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">printf</span><span class="p">(</span><span class="s">&#34;CPUs: %d</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">ncpu</span><span class="p">);</span>
</span></span></code></pre></div><p>That’s the C version of what <code>sysctl hw.ncpu</code> does on the shell.</p>
<p>On BSD/macOS you can also use the convenience wrapper <code>sysctlbyname(3)</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">ncpu</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kt">size_t</span> <span class="n">len</span> <span class="o">=</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">ncpu</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nf">sysctlbyname</span><span class="p">(</span><span class="s">&#34;hw.ncpu&#34;</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">ncpu</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">len</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">perror</span><span class="p">(</span><span class="s">&#34;sysctlbyname&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">printf</span><span class="p">(</span><span class="s">&#34;CPUs: %d</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">ncpu</span><span class="p">);</span>
</span></span></code></pre></div><hr>
<h3 id="practical-macos-examples-cli">Practical macOS examples (CLI)</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Show number of CPU cores</span>
</span></span><span class="line"><span class="cl">sysctl hw.ncpu
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show total RAM (in bytes)</span>
</span></span><span class="line"><span class="cl">sysctl hw.memsize
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Kernel version and build string</span>
</span></span><span class="line"><span class="cl">sysctl kern.version
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Boot time (seconds since epoch + human-readable date)</span>
</span></span><span class="line"><span class="cl">sysctl kern.boottime
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Current max files limit</span>
</span></span><span class="line"><span class="cl">sysctl kern.maxfiles
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Raise max files (temporary, until reboot)</span>
</span></span><span class="line"><span class="cl">sudo sysctl -w kern.maxfiles<span class="o">=</span><span class="m">65536</span>
</span></span></code></pre></div><p>💡 On macOS, many performance tunables live under <code>kern.*</code>, <code>hw.*</code>, and <code>net.*</code>. Like Linux <code>/proc/sys</code>, changes with <code>sysctl -w</code> are not persistent — they reset on reboot unless set via <code>launchd</code> plist or sysctl config mechanisms.</p>
<p>Unlike on Linux, <code>/etc/sysctl.conf</code> is ignored. Use a LaunchDaemon or a startup script instead:</p>
<p>For reference:</p>
<ul>
<li>📖 <a href="https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/sysctl.3.html"><code>sysctl(3)</code> man page</a></li>
<li>🗂️ <a href="https://github.com/apple-oss-distributions/xnu/tree/main/bsd">Darwin/XNU source tree</a> — <code>kern_sysctl.c</code> and headers like <code>&lt;sys/sysctl.h&gt;</code>, <code>&lt;netinet/in.h&gt;</code>, <code>&lt;mach/vm_param.h&gt;</code> define the MIBs.</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>I hope you enjoyed this quick tour of some of the more fascinating bits of the Unix filesystem. It’s fun to think about the oddballs all together rather than as isolated footnotes. Unix has a lot of special files—but the beauty is that they’re still just files.</p>
<p>Have a favorite weird Unix virtual file? Something I left out? I’d love to <strong>hear</strong> about it!<br>
Email me: <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>SSH-Fu</title>
      <link>https://adminjitsu.com/posts/ssh-fu/</link>
      <pubDate>Thu, 28 Aug 2025 06:44:27 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/ssh-fu/</guid>
      <description>A companion to the SSH Setup Guide, this post explores the fun side of SSH: running commands remotely, multiplexing, jump hosts, port forwarding, SOCKS proxies, SSHFS, file transfers, and debugging.</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<p>SSH isn’t just about logging in remotely — once you’ve got keys, configs, and an agent in place, it opens the door to a whole arsenal of powerful tricks.</p>
<p>You can run commands remotely, mount filesystems, tunnel services, and even bend network paths to your will.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="data.jpg" 
       alt="Commander Data from Star Trek in front of a display with the caption: 'It is complicated'" 
       style="display:block; margin:0 auto; width:min(100%, 500px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    SSH can be mind-bending at times — but only because it’s incredibly powerful<br>
    Image © Paramount / CBS
  </figcaption>
</figure>
<p>This guide is about the <strong>fun stuff</strong> — the SSH-Fu that turns everyday tasks into smooth one-liners and clever shortcuts. Nothing arcane, just the practical tricks that make life as an admin easier (and maybe a little cooler).</p>
<p>We’ll cover:</p>
<ul>
<li>Running commands and scripts over SSH.</li>
<li>Multiplexing and jump hosts.</li>
<li>Local, remote, and dynamic tunnels.</li>
<li>SSHFS mounts and fstab auto-mounts.</li>
<li>File transfers and syncs.</li>
<li>Debugging connections like a pro.</li>
</ul>
<p>Think of this as an “advanced field manual” — fast, useful, and battle-tested.</p>
<p>If you need help setting up SSH you might want to take a look at my companion post:</p>
<p style="text-align:left;">
  <a href='/posts/ssh-setup/' class="button">SSH Setup Guide</a>
</p>
<h2 id="running-commands-over-ssh-and-handling-quotes">Running Commands Over SSH (and Handling Quotes)</h2>
<p>Sometimes you don’t need a full shell — you just want to run one command remotely.<br>
This is one of the most useful (and often confusing) SSH tricks, especially with quoting.</p>
<hr>
<h3 id="the-basics">The Basics</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh new-server <span class="s1">&#39;uptime&#39;</span>
</span></span></code></pre></div><ul>
<li>Opens a connection, runs <code>uptime</code>, then exits.</li>
<li>Anything in quotes is executed on the <strong>remote host</strong>.</li>
</ul>
<hr>
<h3 id="with-arguments">With Arguments</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh new-server <span class="s1">&#39;ls -lah /var/log&#39;</span>
</span></span></code></pre></div><p>Notice the single quotes — they make sure the <strong>remote shell</strong> interprets the flags, not your local one.</p>
<hr>
<h3 id="mixing-local-and-remote-expansion">Mixing Local and Remote Expansion</h3>
<ul>
<li>
<p><strong>Good:</strong> let remote expand:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh new-server <span class="s1">&#39;echo $HOME&#39;</span>
</span></span></code></pre></div><p>→ prints the remote user’s home directory.</p>
</li>
<li>
<p><strong>Bad:</strong> if you forget quotes:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh new-server <span class="nb">echo</span> <span class="nv">$HOME</span>
</span></span></code></pre></div><p>→ expands <code>$HOME</code> locally, and passes the value to the remote.</p>
</li>
</ul>
<hr>
<h3 id="escaping-quotes">Escaping Quotes</h3>
<p>If your command needs quotes <em>inside</em> it, escape them:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh new-server <span class="s2">&#34;echo &#39;Hello from </span><span class="k">$(</span>hostname<span class="k">)</span><span class="s2">&#39;&#34;</span>
</span></span></code></pre></div><ul>
<li>Outer <code>&quot;</code> let you put <code>'</code> inside safely.</li>
<li><code>$(hostname)</code> is expanded on the <strong>remote host</strong>.</li>
</ul>
<hr>
<h3 id="running-a-script-inline">Running a Script Inline</h3>
<p>You can pipe a local script to run on the remote side:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat script.sh <span class="p">|</span> ssh new-server <span class="s1">&#39;bash -s -- arg1 arg2&#39;</span>
</span></span></code></pre></div><ul>
<li><code>bash -s --</code> tells the remote bash to read from stdin.</li>
<li><code>arg1 arg2</code> are passed as <code>$1</code> <code>$2</code> to the script.</li>
</ul>
<hr>
<h3 id="copy-and-run-one-liner-deployments">Copy-and-Run (one-liner deployments)</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh new-server <span class="s1">&#39;mkdir -p ~/deploy &amp;&amp; tar xzf - -C ~/deploy&#39;</span> &lt; site.tar.gz
</span></span></code></pre></div><ul>
<li><code>&lt; site.tar.gz</code> feeds your local tarball into the remote tar command.</li>
<li>Great for quick deployments without scp/rsync.</li>
</ul>
<hr>
<h3 id="pro-tip-debug-your-quoting">Pro Tip: Debug Your Quoting</h3>
<p>If something looks wrong, <code>echo</code> the command string first:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh new-server <span class="s1">&#39;echo &#34;Hello, world&#34;&#39;</span>
</span></span></code></pre></div><p>Or crank up verbosity:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh -v new-server <span class="s1">&#39;echo &#34;debugging quotes&#34;&#39;</span>
</span></span></code></pre></div><hr>
<p><strong>Bottom line:</strong></p>
<ul>
<li>Wrap remote commands in quotes.</li>
<li>Single quotes <code>' '</code> prevent local expansion.</li>
<li>Double quotes <code>&quot; &quot;</code> allow mixing variables carefully.</li>
<li>If in doubt, escape inner quotes or just write a quick script and pipe it over.</li>
</ul>
<br>
<h2 id="sshfs-mount-remote-files-like-theyre-local">SSHFS: Mount Remote Files Like They’re Local</h2>
<p>Sometimes you don’t want to <code>scp</code> files back and forth — you just want to <em>work in place</em> as if the remote filesystem were part of your machine.<br>
That’s what <strong>SSHFS</strong> (SSH Filesystem) gives you: a way to mount a remote directory over SSH.</p>
<hr>
<h3 id="mount-a-remote-directory">Mount a Remote Directory</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sshfs forfaxx@new-server.darkstar.home:/var/www ~/mnt/www
</span></span></code></pre></div><p>Now <code>~/mnt/www</code> on your machine shows the contents of <code>/var/www</code> on <em>new-server</em>.<br>
You can <code>ls</code>, <code>cd</code>, <code>vim</code>, or even use GUI apps on those files like they were local.</p>
<hr>
<h3 id="unmount-it">Unmount It</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">fusermount -u ~/mnt/www      <span class="c1"># Linux</span>
</span></span><span class="line"><span class="cl">umount ~/mnt/www             <span class="c1"># macOS / BSD</span>
</span></span></code></pre></div><hr>
<h3 id="why-use-it">Why Use It?</h3>
<ul>
<li>Edit remote config files with your local editor.</li>
<li>Drag-and-drop files in your GUI file manager.</li>
<li>Script against a remote directory without copying.</li>
</ul>
<hr>
<h3 id="performance-notes">Performance Notes</h3>
<ul>
<li>It’s built on FUSE, so performance depends on network latency.</li>
<li>Great for light editing, browsing logs, or quick fixes.</li>
<li>Not ideal for heavy I/O workloads (databases, compiles, large builds).</li>
</ul>
<hr>
<h3 id="auto-mount-with-etcfstab">Auto-Mount with <code>/etc/fstab</code></h3>
<p>You can make an SSHFS mount persistent by adding an entry in <code>/etc/fstab</code>.<br>
This way it mounts automatically at boot (or on demand with <code>mount</code>).</p>
<p>Example line:</p>
<pre tabindex="0"><code class="language-fstab" data-lang="fstab">forfaxx@new-server.darkstar.home:/var/www  /mnt/www  fuse.sshfs  defaults,_netdev,IdentityFile=/home/forfaxx/.ssh/id_ed25519,allow_other,uid=1000,gid=1000  0  0
</code></pre><p><strong>Breakdown:</strong></p>
<ul>
<li><code>forfaxx@new-server.darkstar.home:/var/www</code> → remote path.</li>
<li><code>/mnt/www</code> → local mount point (must exist).</li>
<li><code>fuse.sshfs</code> → tells fstab to use SSHFS via FUSE.</li>
<li><code>defaults,_netdev</code> → wait for network before mounting.</li>
<li><code>IdentityFile=…</code> → path to your private key.</li>
<li><code>allow_other</code> → let other local users access it (optional).</li>
<li><code>uid=1000,gid=1000</code> → map ownership to your local user/group.</li>
</ul>
<hr>
<h3 id="mountunmount">Mount/Unmount</h3>
<p>After adding it, you can run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo mount /mnt/www
</span></span><span class="line"><span class="cl">sudo umount /mnt/www
</span></span></code></pre></div><hr>
<p><span class="tag green">Pro-Tip:</span> test the sshfs command manually first to confirm it works before editing <code>/etc/fstab</code>. A typo there can slow down boot.</p>
<p><span class="tag green">Pro-Tip:</span> Combine <code>sshfs</code> with your <code>~/.ssh/config</code> shortcuts, and you can mount complex paths with one short command.</p>
<br>
<h2 id="ssh-tips--tricks">SSH Tips &amp; Tricks</h2>
<p>Once the basics are set — keys, config, agent — you can unlock a bunch of quality-of-life and power-user moves. These are the ones worth knowing.</p>
<hr>
<h3 id="multiplexing-instant-ssh">Multiplexing: Instant SSH</h3>
<p>Instead of re-negotiating crypto every time, reuse a single connection.</p>
<p>In <code>~/.ssh/config</code>:</p>
<pre tabindex="0"><code class="language-sshconfig" data-lang="sshconfig">Host *
  ControlMaster auto
  ControlPath ~/.ssh/cm-%r@%h:%p
  ControlPersist 10m
</code></pre><p>Now:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh new-server        <span class="c1"># open one shell</span>
</span></span><span class="line"><span class="cl">scp file new-server:/tmp/   <span class="c1"># reuses same connection, no delay</span>
</span></span></code></pre></div><p><span class="tag green">Pro-Tip:</span> If you hit “path too long for UNIX socket,” use <code>%C</code> (a hash of the host/port) instead:</p>
<pre tabindex="0"><code>

---
### Jump Hosts: Hop Cleanly  

Sometimes the machine you need isn’t directly reachable — it sits on a private subnet — but there’s a “middleman” you *can* reach, often called a **bastion host**. Instead of logging into the bastion and then hopping again manually, `ProxyJump` lets SSH handle it in one clean step.  

It feels a little cinematic — like Elliot in *Mr. Robot* or “Zero Cool” in *Hackers*, bouncing through a box to slip deeper into the network. It’s not an everyday trick, but worth keeping in your back pocket for those tricky environments.  

```bash
ssh -J bastion new-server
</code></pre><p>Config version:</p>
<pre tabindex="0"><code class="language-sshconfig" data-lang="sshconfig">Host new-server
  HostName new-server.darkstar.home
  User forfaxx
  ProxyJump bastion
</code></pre><p>And yes, you can chain multiple hops if you are so inclined:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh -J bastion1,bastion2 target
</span></span></code></pre></div><hr>
<h3 id="local--remote-port-forwards">Local &amp; Remote Port Forwards</h3>
<p>I have used SSH tunnels many times and it still hurts my brain a little.</p>
<p>Think of an SSH tunnel as a <strong>teleport pad for TCP</strong>: you open a port on one side, step in locally, and come out on the other side.<br>
The confusing part is remembering <em>which side you step into</em>.<br>
Rule of thumb: <strong>you always connect to the side you wrote first</strong>.</p>
<hr>
<p><strong>Local forward (<code>-L</code>) — step in <em>here</em>, come out <em>there</em></strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh -L 8443:127.0.0.1:443 new-server
</span></span></code></pre></div><ul>
<li>You connect to <strong>localhost:8443 on your machine</strong>.</li>
<li>SSH carries it to <code>127.0.0.1:443</code> on <em>new-server</em>.</li>
<li>Example: open <code>https://localhost:8443</code> in your browser → you’re really talking to <em>new-server</em>’s HTTPS service.</li>
</ul>
<hr>
<p><strong>Remote forward (<code>-R</code>) — step in <em>there</em>, come out <em>here</em></strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh -R 9000:127.0.0.1:3000 new-server
</span></span></code></pre></div><ul>
<li>On <em>new-server</em>, connect to <strong>localhost:9000</strong>.</li>
<li>SSH carries it back to <code>127.0.0.1:3000</code> on <em>your machine</em>.</li>
<li>Example: from <em>new-server</em>, <code>curl http://127.0.0.1:9000</code> → hits your app running locally on port 3000.</li>
</ul>
<span class="tag green">Why use this?</span>
<p>Because sometimes the door you need is locked:</p>
<ul>
<li>A service only listens on loopback.</li>
<li>A firewall blocks it.</li>
<li>You want traffic encrypted over SSH.</li>
</ul>
<p>Port forwarding is your keyhole.</p>
<hr>
<h3 id="socks-proxy-poor-mans-vpn">SOCKS Proxy: Poor Man’s VPN</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh -D <span class="m">1080</span> new-server
</span></span></code></pre></div><p>Then point your browser’s SOCKS proxy to <code>localhost:1080</code>.</p>
<p><span class="tag green">Pro-Tip:</span> Pair <code>ssh -D</code> with Firefox’s “Proxy DNS when using SOCKS v5” setting to tunnel DNS lookups too.</p>
<hr>
<h3 id="file-transfers-over-ssh">File Transfers Over SSH</h3>
<p>Copying files is one of the most common SSH tasks. Here are the main tools and when to reach for them:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Copy local → remote (quick &amp; dirty)</span>
</span></span><span class="line"><span class="cl">scp -r ./site new-server:~/deploy/
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Copy remote → local</span>
</span></span><span class="line"><span class="cl">scp new-server:/var/log/nginx/error.log ./error.log
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Efficient sync (resume, skip unchanged files)</span>
</span></span><span class="line"><span class="cl">rsync -avz --progress -e ssh ./public/ new-server:~/www/public/
</span></span></code></pre></div><ul>
<li><strong><code>scp</code></strong> → simple, built-in, works everywhere. Great for one-offs.</li>
<li><strong><code>rsync</code></strong> → faster for large trees, resumes interrupted transfers, only sends differences.</li>
</ul>
<p><span class="tag green">Pro-Tip:</span> This feels like a cheat-code but with the <code>-3</code> switch, you can copy files between two remote machines using yours as a sort of traffic cop to facilitate, all without logging in anywhere.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Copy from hostA → hostB via your local machine</span>
</span></span><span class="line"><span class="cl">scp -3 user@hostA:/var/log/syslog user@hostB:/tmp/syslog
</span></span></code></pre></div><ul>
<li>
<p>Data flows through your client (not directly host-to-host).</p>
</li>
<li>
<p>Still encrypted end-to-end (since both legs use SSH).</p>
</li>
<li>
<p>Useful when hosts can’t talk to each other directly, but you can reach both.</p>
</li>
</ul>
<p><span class="tag orange">Note:</span> This moves files via your local bandwidth. If the file is huge, your network is the bottleneck. But for config snippets, logs, or small dumps, it’s insanely handy.</p>
<hr>
<h3 id="using-sftp-interactive-or-scripted">Using <code>sftp</code> (interactive or scripted)</h3>
<p>For more controlled transfers, SSH comes with <code>sftp</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sftp new-server
</span></span><span class="line"><span class="cl">sftp&gt; put site.tar.gz /tmp/
</span></span><span class="line"><span class="cl">sftp&gt; get /var/log/nginx/access.log ./access.log
</span></span><span class="line"><span class="cl">sftp&gt; <span class="nb">exit</span>
</span></span></code></pre></div><ul>
<li>Looks/feels like FTP, but runs over SSH.</li>
<li>Supports tab completion, <code>ls</code>, <code>cd</code>, and <code>pwd</code>.</li>
<li>Batch mode makes it scriptable:</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sftp -b - new-server <span class="s">&lt;&lt;&#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">put site.tar.gz /tmp/
</span></span></span><span class="line"><span class="cl"><span class="s">get /var/log/nginx/error.log ./error.log
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span></code></pre></div><p><span class="tag green">Pro-Tip:</span> For recurring jobs, use <code>rsync</code> or scripted <code>sftp</code> — they’re more efficient and reliable than <code>scp</code>, which OpenSSH now treats as legacy.</p>
<hr>
<h3 id="quick-host-hygiene">Quick Host Hygiene</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Check fingerprint of a key</span>
</span></span><span class="line"><span class="cl">ssh-keygen -lf ~/.ssh/id_ed25519.pub
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Derive pubkey from private key</span>
</span></span><span class="line"><span class="cl">ssh-keygen -y -f ~/.ssh/id_ed25519
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Remove stale known_hosts entry</span>
</span></span><span class="line"><span class="cl">ssh-keygen -R new-server.darkstar.home
</span></span></code></pre></div><hr>
<h3 id="debug-like-a-pro">Debug Like a Pro</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh -v new-server
</span></span></code></pre></div><p>or crank it up:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh -vvv new-server
</span></span></code></pre></div><p>Tells you exactly why a connection/auth is failing.</p>
<h3 id="using--t-to-run-remote-programs-like-theyre-local">Using <code>-t</code> to Run Remote Programs Like They&rsquo;re Local</h3>
<p>Normally when you use <code>ssh user@host 'command'</code>, SSH treats it like a non-interactive one-shot. That works great for <code>ls</code> or <code>uptime</code>, but it falls apart if the program expects a <strong>terminal</strong> — like <code>vim</code>, <code>top</code>, or <code>htop</code>.</p>
<p>That’s where the <code>-t</code> flag comes in: it forces SSH to allocate a pseudo-TTY so the remote program thinks it’s running in a real terminal. Suddenly, you can run interactive apps on the remote side as if they were local.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Edit a config file remotely</span>
</span></span><span class="line"><span class="cl">ssh -t new-server <span class="s1">&#39;vim /etc/nginx/nginx.conf&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run interactive process monitors</span>
</span></span><span class="line"><span class="cl">ssh -t new-server htop
</span></span><span class="line"><span class="cl">ssh -t new-server <span class="s1">&#39;sudo journalctl -f&#39;</span>
</span></span></code></pre></div><p>If you need to hop through <code>sudo</code> or other programs that strip the TTY, you can double up:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh -tt new-server <span class="s1">&#39;sudo vim /etc/hosts&#39;</span>
</span></span></code></pre></div><p>It’s a tiny switch, but it unlocks a ton of everyday tricks. No more half-baked redirections — just straight into your remote Vim, top, or less session like you’re sitting at the machine.</p>
<p><span class="tag green">Pro-Tip:</span> Combine <code>-t</code> with <code>ProxyJump</code> or multiplexing and you can pop open an editor on a deep-in-the-network host in a single smooth one-liner.</p>
<h2 id="using-ssh-in-scripts-safe-non-interactive-patterns">Using SSH in Scripts (Safe, Non-Interactive Patterns)</h2>
<p>Once your logins are safe and seamless (keys + agent), SSH becomes a rock-solid <strong>automation primitive</strong>. A few rules I&rsquo;ve found to make it reliable in scripts:</p>
<ul>
<li><strong>Never prompt</strong>: force non-interactive behavior.</li>
<li><strong>Pin host keys</strong>: avoid TOFU surprises in CI.</li>
<li><strong>Propagate failures</strong>: treat remote errors as errors.</li>
</ul>
<h3 id="recommended-flags">Recommended flags</h3>
<ul>
<li><code>-o BatchMode=yes</code> → fail immediately if auth would prompt.</li>
<li><code>-o StrictHostKeyChecking=yes</code> → refuse unknown/changed hosts.</li>
<li><code>-o ConnectTimeout=5</code> → don’t hang forever.</li>
<li>(Optional CI) <code>-o UserKnownHostsFile=/path/known_hosts</code> → use a pinned file.</li>
</ul>
<h3 id="one-liner-pattern">One-liner pattern</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh -o <span class="nv">BatchMode</span><span class="o">=</span>yes -o <span class="nv">StrictHostKeyChecking</span><span class="o">=</span>yes new-server <span class="s1">&#39;sudo systemctl restart nginx&#39;</span>
</span></span></code></pre></div><h3 id="multi-line-remote-script-here-doc">Multi-line remote script (here-doc)</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh -o <span class="nv">BatchMode</span><span class="o">=</span>yes new-server <span class="s1">&#39;bash -s&#39;</span> <span class="s">&lt;&lt;&#39;REMOTE&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">set -euo pipefail
</span></span></span><span class="line"><span class="cl"><span class="s">echo &#34;Deploying…&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">mkdir -p ~/app
</span></span></span><span class="line"><span class="cl"><span class="s">tar xzf - -C ~/app
</span></span></span><span class="line"><span class="cl"><span class="s">systemctl --user restart app.service
</span></span></span><span class="line"><span class="cl"><span class="s">REMOTE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># (feed tar on stdin if needed: tar czf - . | ssh … &#39;bash -s&#39;)</span>
</span></span></code></pre></div><h3 id="copy-and-run-fast-deploy-without-scp">Copy-and-run (fast “deploy without scp”)</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">tar czf - . <span class="p">|</span> ssh -o <span class="nv">BatchMode</span><span class="o">=</span>yes new-server <span class="s1">&#39;mkdir -p ~/deploy &amp;&amp; tar xzf - -C ~/deploy &amp;&amp; ~/deploy/install.sh&#39;</span>
</span></span></code></pre></div><h3 id="iperfer-one-command-iperf3-test-over-ssh">iperfer: One-Command iperf3 test over SSH</h3>
<p>A concrete example of the <strong>copy-and-run pattern</strong> is my tool <code>iperfer</code>: it uses SSH to spin up an <code>iperf3</code> server remotely, runs the client locally, then tears everything down in one go.</p>
<p style="text-align:left;">
  <a href='/posts/iperfer/' class="button">🚀 iperfer: one-command iperf3 over SSH</a>
</p>
<hr>
<h2 id="conclusion">Conclusion</h2>
<p>SSH is more than <code>ssh user@host</code>. With a few well-placed tricks, you’ll connect faster, hop cleanly through networks, tunnel like a pro, and stop tripping over key headaches.</p>
<p>Got a sweet SSH tip, trick, or hack to share?<br>
I’d love to hear it: <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
<p style="text-align:left;">
  <a href='/posts/ssh-setup/' class="button">SSH Setup Guide</a>
</p>
]]></content:encoded>
    </item>
    <item>
      <title>SSH Setup Guide</title>
      <link>https://adminjitsu.com/posts/ssh-setup/</link>
      <pubDate>Thu, 28 Aug 2025 00:08:20 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/ssh-setup/</guid>
      <description>Step-by-step guide to setting up SSH the right way: secure server settings, generating and installing keys, reciprocal trust, client configs, and making SSH agents painless.</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<p>SSH is one of those indispensible tools that quietly becomes the backbone of your entire setup.
It’s how you log in, move files, hop between machines, and even tunnel services. Getting it set up correctly is important especially since you tend not to do these steps very often.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="pershing.jpg" 
       alt="an M26 Pershing Tank on display from a dramatic angle" 
       style="display:block; margin:0 auto; width:min(100%, 500px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Set up corectly, SSH is like a tank—tough, rugged, and dependable
  </figcaption>
</figure>
<p>But here’s the thing: SSH is deceptively simple on the surface—type <code>ssh user@host</code> and you’re in—yet maddeningly complex once you start juggling multiple machines, keys, configs, and workflows. New users often stumble on permissions, while seasoned pros sometimes end up with messy key sprawl and brittle configs. Either way, pain awaits without a little discipline.</p>
<p>In this guide, I want to capture a clean, repeatable workflow for standing up a new machine with reciprocal SSH trust, a well-structured config, and a few tips and tricks that make everyday use faster, safer, and less painful. Nothing groundbreaking—just the kind of practical guide I wish I’d had when I was first wiring machines together.</p>
<p>We’ll cover:</p>
<ul>
<li>Generating modern SSH keys the right way.</li>
<li>Copying them safely between machines (both directions).</li>
<li>Setting up a maintainable ~/.ssh/config.</li>
<li>Using SSH agent and keychains effectively.</li>
</ul>
<p>For multiplexing, tunneling, and other advanced tricks, check out the companion post</p>
<p style="text-align:left;">
  <a href="/posts/ssh-fu" class="button">⚔️ Continue to SSH-Fu</a>
</p>
<h2 id="why-ssh-matters">Why SSH matters</h2>
<p>At its core, SSH (Secure Shell) gives you three superpowers:</p>
<ol>
<li>
<p><strong>Secure Remote Access</strong><br>
Encrypted sessions mean you can log in to a remote system without worrying about someone sniffing your password or keystrokes. This replaced insecure tools like Telnet and rlogin.</p>
</li>
<li>
<p><strong>File Transfer</strong><br>
With <code>scp</code> and <code>sftp</code>, SSH is a safe way to move files. Add <code>rsync</code> over SSH and the ability to mount paths with <code>sshfs</code>, and you’ve got a flexible toolkit for syncs, backups, and deployments.</p>
</li>
<li>
<p><strong>Tunnels and Proxies</strong><br>
SSH can forward ports, set up SOCKS proxies, and even “jump” through one machine to reach another. This makes it the Swiss Army knife of your network.</p>
</li>
</ol>
<h2 id="configuring-sshd_config">Configuring sshd_config</h2>
<p>On most systems the <strong>server daemon</strong> is <code>sshd</code>, and its main config lives in <code>/etc/ssh/sshd_config</code>. There are a lot of options but the ones we are concerned with are as follows. On the machine you will be connecting to (we&rsquo;ll call it new-server), make sure you have the following set:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># Only allow modern protocol
</span></span><span class="line"><span class="cl">Protocol 2
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># Enable public key auth
</span></span><span class="line"><span class="cl">PubkeyAuthentication yes
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># Disable password logins once keys are working
</span></span><span class="line"><span class="cl">PasswordAuthentication no
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># Prevent root from password logins (or at all)
</span></span><span class="line"><span class="cl">PermitRootLogin prohibit-password
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># Security hygiene
</span></span><span class="line"><span class="cl">ChallengeResponseAuthentication no
</span></span><span class="line"><span class="cl">PermitEmptyPasswords no
</span></span><span class="line"><span class="cl">UsePAM yes
</span></span></code></pre></div><p>After making edits, make sure to reload:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemctl reload sshd    <span class="c1"># systemd</span>
</span></span><span class="line"><span class="cl"><span class="c1"># or</span>
</span></span><span class="line"><span class="cl">sudo service ssh reload       <span class="c1"># SysVinit</span>
</span></span></code></pre></div><br>
<p><span class="tag green">Pro-Tip</span> Never flip <code>PasswordAuthentication no</code> until you&rsquo;ve verified that you can log in with a key from another terminal session to avoid getting locked out.</p>
<hr>
<h2 id="generating-and-installing-keys">Generating and Installing Keys</h2>
<p>Now that you have the machine you will be connecting to ready to accept <code>PubkeyAuthentiation</code>, you&rsquo;ll need to generate keys on your client and then copy the public key to new-server. You need to have a working user account on new-server as well but in this scenario we&rsquo;ll assume you are using the same username across your machines.</p>
<p>On your client machine:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh-keygen -t ed25519 -a <span class="m">100</span> -C <span class="s2">&#34;forfaxx@laptop (2025-08)&#34;</span>
</span></span></code></pre></div><p>Copy the public key to the server:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh-copy-id forfaxx@new-server.darkstar.home
</span></span></code></pre></div><p>If <code>ssh-copy-id</code> isn’t available:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat ~/.ssh/id_ed25519.pub <span class="p">|</span> ssh forfaxx@new-server.darkstar.home <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span> <span class="s1">&#39;umask 077; mkdir -p ~/.ssh &amp;&amp; cat &gt;&gt; ~/.ssh/authorized_keys&#39;</span>
</span></span></code></pre></div><p>On the server, fix permissions:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">chmod <span class="m">700</span> ~/.ssh
</span></span><span class="line"><span class="cl">chmod <span class="m">600</span> ~/.ssh/authorized_keys
</span></span></code></pre></div><hr>
<h2 id="client-config-sshconfig">Client Config (<code>~/.ssh/config</code>)</h2>
<p>On your client you&rsquo;ll want to add an entry to <code>.ssh/config</code> to make it easier to connect. Without an entry you need to remember to connect with a command like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh -i ~/.ssh/id_ed25519 forfaxx@new-server.darkstar.home
</span></span></code></pre></div><p>So to make it easier, we&rsquo;ll add an entry to <code>~/.ssh/config</code> (create it if it doesn&rsquo;t exist) and include an entry like the following:</p>
<pre tabindex="0"><code class="language-sshconfig" data-lang="sshconfig">Host new-server
  HostName new-server.darkstar.home
  User forfaxx
  IdentityFile ~/.ssh/id_ed25519
  IdentitiesOnly yes
  ServerAliveInterval 30
</code></pre><p>With that in place you can connect with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh new-server
</span></span></code></pre></div><p>This is where the real quality of life improvements begin — shorter commands, per-host options, and tricks like multiplexing or jump hosts.</p>
<br>
<h2 id="using-an-ssh-agent-stop-skipping-this">Using an SSH Agent (Stop Skipping This)</h2>
<figure style="text-align:center; margin: 1em auto;">
  <img src="tank-armor.jpg" 
       alt="detail of M26 Pershing turret showing battle damage dents in the thick armor" 
       style="display:block; margin:0 auto; width:min(100%, 450px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    With proper setup and best practices your SSH armor will hold up on the battlefield
  </figcaption>
</figure>
<p>If you’ve been running keys <strong>without passphrases</strong> (guilty 🙋), it’s probably because you thought:<br>
<em>“Why bother typing a passphrase every single time I connect? Connecting without a password is terrific!”</em></p>
<p>The problem is:</p>
<ul>
<li>Without a passphrase, your private key file is <strong>just a single factor</strong>.</li>
<li>If a bad guy steals <code>~/.ssh/id_ed25519</code>, they don’t need anything else — they <strong>are</strong> you.</li>
</ul>
<p>So you’re stuck in a dilemma:</p>
<ul>
<li><strong>With a passphrase</strong> → secure, but annoying to type constantly.</li>
<li><strong>Without a passphrase</strong> → convenient, but dangerously insecure.</li>
</ul>
<p>That’s exactly what the <strong>SSH agent</strong> solves.<br>
Think of it as a guard that holds your unlocked key safely <em>in memory</em> for the session. You type the passphrase once, hand it to the guard, and from then on SSH just works — no constant retyping, no unprotected key file.</p>
<p>The best part: you don’t have to sacrifice security for convenience. With an agent, you get both.</p>
<hr>
<h3 id="how-it-works">How It Works</h3>
<ul>
<li>Normally, SSH has to decrypt your private key <strong>every single time</strong> (prompting you for the passphrase).</li>
<li>The agent is a <strong>background process</strong> (<code>ssh-agent</code>) that:
<ol>
<li>Runs once per login/session.</li>
<li>Holds your unlocked key safely in memory.</li>
<li>Answers “signing challenges” on your behalf — without ever exposing the key.</li>
</ol>
</li>
<li>You unlock the key once with <code>ssh-add</code>, then it’s smooth sailing until you log out or reboot.</li>
</ul>
<hr>
<h3 id="one-time-setup-per-session">One-Time Setup (per session)</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 1. Start the agent (if not already running)</span>
</span></span><span class="line"><span class="cl"><span class="nb">eval</span> <span class="s2">&#34;</span><span class="k">$(</span>ssh-agent -s<span class="k">)</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"># 2. Add your key (enter passphrase once)</span>
</span></span><span class="line"><span class="cl">ssh-add ~/.ssh/id_ed25519
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 3. Verify it&#39;s loaded</span>
</span></span><span class="line"><span class="cl">ssh-add -l
</span></span></code></pre></div><p>That’s it. Every <code>ssh</code>, <code>scp</code>, or <code>git</code> command will now silently ask the agent instead of bugging you.</p>
<hr>
<h3 id="how-long-does-it-last">How Long Does It Last?</h3>
<ul>
<li><strong>Until you log out or reboot</strong> (or until the agent is killed).</li>
<li>You can set a timeout if you want the agent to forget automatically:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh-add -t 1h ~/.ssh/id_ed25519
</span></span></code></pre></div></li>
<li>On <strong>macOS</strong>, you can store keys in the Keychain so they come back on login:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh-add --apple-use-keychain ~/.ssh/id_ed25519
</span></span></code></pre></div></li>
</ul>
<hr>
<h3 id="quick-recap">Quick Recap</h3>
<table>
  <thead>
      <tr>
          <th>Step</th>
          <th>When you do it</th>
          <th>What it does</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>eval &quot;$(ssh-agent -s)&quot;</code></td>
          <td>Once per login session</td>
          <td>Starts the guard</td>
      </tr>
      <tr>
          <td><code>ssh-add ~/.ssh/id_ed25519</code></td>
          <td>Once per login session</td>
          <td>Give guard your unlocked key</td>
      </tr>
      <tr>
          <td><code>ssh new-server</code></td>
          <td>Anytime</td>
          <td>Uses the guard, no prompt</td>
      </tr>
  </tbody>
</table>
<p><strong>Mental model:</strong> like making coffee in the morning → unlock once, good all day.</p>
<hr>
<h2 id="make-the-agent-automatic-so-you-dont-have-to-remember">Make the Agent Automatic (so you don’t have to remember)</h2>
<p>Here’s how to set it and forget it on each OS.</p>
<h3 id="macos-seamless">macOS (seamless)</h3>
<p>One-time:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh-add --apple-use-keychain ~/.ssh/id_ed25519
</span></span></code></pre></div><p>In <code>~/.ssh/config</code>:</p>
<pre tabindex="0"><code class="language-sshconfig" data-lang="sshconfig">Host *
  UseKeychain yes
  AddKeysToAgent yes
</code></pre><p><strong>Result:</strong> key unlocks at login, no extra steps.</p>
<hr>
<h3 id="linux-single-line-in-shell-rc">Linux (single line in shell rc)</h3>
<p>Add to <code>~/.bash_profile</code> or <code>~/.zshrc</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Start agent if needed, add key if missing (prompts once per login)</span>
</span></span><span class="line"><span class="cl">ssh-add -l &gt;/dev/null 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="o">||</span> <span class="o">{</span> <span class="nb">eval</span> <span class="s2">&#34;</span><span class="k">$(</span>ssh-agent -s<span class="k">)</span><span class="s2">&#34;</span> &gt;/dev/null<span class="p">;</span> ssh-add ~/.ssh/id_ed25519 &lt;/dev/tty<span class="p">;</span> <span class="o">}</span>
</span></span></code></pre></div><p><strong>Result:</strong> each login → one passphrase prompt, then done.</p>
<hr>
<h3 id="windows-1011">Windows 10/11</h3>
<p>One-time enable:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Set-Service</span> <span class="nb">ssh-agent</span> <span class="n">-StartupType</span> <span class="n">Automatic</span>
</span></span><span class="line"><span class="cl"><span class="nb">Start-Service</span> <span class="nb">ssh-agent</span>
</span></span></code></pre></div><p>In your PowerShell profile (<code>$PROFILE</code>):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="c"># If agent has no identities, add mine (prompts once per login)</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">((</span><span class="nb">ssh-add</span> <span class="n">-l</span><span class="p">)</span> <span class="o">-match</span> <span class="s1">&#39;The agent has no identities&#39;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">ssh-add</span> <span class="s2">&#34;</span><span class="nv">$env:USERPROFILE</span><span class="s2">\.ssh\id_ed25519&#34;</span> <span class="p">|</span> <span class="nb">Out-Null</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><strong>Result:</strong> first terminal after login prompts once, then all SSH/Git just works.</p>
<hr>
<h3 id="handy-commands">Handy Commands</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh-add -l        <span class="c1"># list loaded keys</span>
</span></span><span class="line"><span class="cl">ssh-add -D        <span class="c1"># remove all keys from agent</span>
</span></span><span class="line"><span class="cl">ssh-agent -k      <span class="c1"># stop agent (Linux/macOS)</span>
</span></span></code></pre></div><p><strong>Bottom line:</strong><br>
Keep your keys encrypted with a passphrase.<br>
Let the agent do the remembering.<br>
Unlock once, fight battles all day.</p>
<h2 id="links-and-stuff">Links and stuff</h2>
<ul>
<li><a href="https://www.rfc-editor.org/rfc/rfc4251">RFC 4251</a> - The Secure Shell (SSH) Protocol Architecture</li>
<li><a href="https://man7.org/linux/man-pages/man1/ssh.1.html"><code>ssh(1)</code> man page — man7.org</a></li>
<li><a href="https://man7.org/linux/man-pages/man5/ssh_config.5.html"><code>ssh_config(5)</code> man page — man7.org</a></li>
<li><a href="https://man7.org/linux/man-pages/man5/sshd_config.5.html"><code>sshd_config(5)</code> man page — man7.org</a></li>
<li><a href="https://en.wikipedia.org/wiki/Secure_Shell">Wikipedia: Secure Shell</a></li>
<li><a href="https://www.openssh.com/">OpenSSH Primer</a></li>
<li><a href="https://www.cisa.gov/news-events/cybersecurity-advisories">US-CERT Advisories</a> (search for “SSH”)</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>SSH is a deceptively simple command with a lot of hidden depth.<br>
A little upfront discipline—clean keys, a modular config, and a few tricks—turns it into one of the most powerful, reliable tools in your kit.</p>
<p style="text-align:left;">
  <a href="/posts/ssh-fu" class="button">⚔️ Continue to SSH-Fu</a>
</p>
<p>Got feedback, corrections, or your own SSH war stories?<br>
I’d love to hear them: <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Manpage-fu</title>
      <link>https://adminjitsu.com/posts/manpage-fu/</link>
      <pubDate>Thu, 21 Aug 2025 10:25:00 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/manpage-fu/</guid>
      <description>A hands-on guide to creating man pages for your own scripts: start with a commented template, learn macros, view locally, install on Linux and macOS, bootstrap with help2man, automate with a Makefile, and even build your own cheatsheet manpage.</description>
      <content:encoded><![CDATA[<h2 id="what-is-a-man-page">What is a man page?</h2>
<p>If you’ve ever typed <code>man ls</code> or <code>man ssh</code>, you’ve used one: a <strong>manual page</strong>. These pages are the canonical documentation of Unix—every command, system call, file format, or standard has its place.</p>
<p>They were created in 1971 at Bell Labs by Dennis Ritchie and Ken Thompson, after their manager insisted the system needed proper documentation. At first, this meant printed binders: plain text written in <a href="https://man7.org/linux/man-pages/man7/roff.7.html"><code>roff(7)</code></a> typesetting macros, formatted for the line printer. It worked—but it was clunky. Finding the right entry meant flipping through a binder, and keeping it up to date meant re-printing and re-binding constantly.</p>
<p>The solution was <code>man(1)</code>: a program that could render those same <code>roff</code> sources directly on a terminal screen. Suddenly, documentation was always current, always searchable, and always at your fingertips. Fifty years later, we’re still using it. Talk about an evergreen technology—manpages have outlasted punch cards, paper tape, floppy disks, and a host of other “modern” technologies.</p>
<blockquote>
<p><em>It&rsquo;s documented in The Book, somewhere&hellip;</em> <br>
&ndash; Larry Wall in <a href="mailto:10502@jpl-devvax.JPL.NASA.GOV">10502@jpl-devvax.JPL.NASA.GOV</a></p></blockquote>
<p>A manpage today still comes in two forms: the <strong>source page</strong> (written in <code>roff</code> macros) and the <strong>formatted page</strong> (sometimes called a <em>cat page</em>), which is what <code>man(1)</code> actually displays.</p>
<h2 id="why-is-this-cool">Why is this cool?</h2>
<p>Using <code>man(1)</code> is second nature, but writing your own page can feel intimidating enough that most of us skip it for our own scripts and tools. That’s a shame—because a proper manpage doesn’t just document your program, it <em>legitimizes</em> it. Nothing makes a tool feel more “real” than typing <code>man mytool</code> and seeing it pop up like it was always meant to live in <code>/usr/bin</code>.</p>
<p>In this post I’ll share the concepts that finally made the process click for me—and hopefully for you as well.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="mytool.1.jpg" 
       alt="screenshot of man ./mytool.1" 
       style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
     With a good reference and a simple template, you can give your code the kind of documentation that feels native to Unix itself.
  </figcaption>
</figure>
<hr>
<h2 id="the-quick-and-dirty-howto">The Quick and Dirty Howto</h2>
<p>The manual system is dead simple:</p>
<ul>
<li>View a page:</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">man ls
</span></span><span class="line"><span class="cl">man ./mytool.1   <span class="c1"># view local draft</span>
</span></span></code></pre></div><ul>
<li>Search all pages for a keyword:</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">man -k network
</span></span><span class="line"><span class="cl">apropos network   <span class="c1"># same thing</span>
</span></span></code></pre></div><ul>
<li>Search only in specific sections</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">man -s <span class="m">2</span> -k open  <span class="c1"># search section 2 (system calls) for open</span>
</span></span></code></pre></div><br>
<p><span class="tag blue">Pro-Tip</span> a really cool package worth installing is <code>tldr</code> which serves as a companion to the standard man pages and just shows practical usage examples</p>
<h2 id="gnu-vs-bsd-flavor">GNU vs BSD flavor</h2>
<p>There are small differences depending on your platform. Linux typically ships the GNU man/groff toolchain, while macOS and the BSDs use their BSD versions. The basics are the same, but options and formatting quirks can differ.</p>
<p>The best way to check your system is with the manuals themselves</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">man man
</span></span><span class="line"><span class="cl">man groff
</span></span><span class="line"><span class="cl">man troff 
</span></span></code></pre></div><hr>
<h2 id="gui-manpage-viewers">GUI manpage viewers</h2>
<p>Most of us are used to paging through manpages in a terminal, but there are GUI front-ends that make them easier to browse, search, and bookmark.</p>
<p>On <strong>macOS</strong> you’ve got a few nice tricks:</p>
<ul>
<li>
<p>Built-in <code>x-man-page://</code> scheme:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">open x-man-page://ls
</span></span></code></pre></div><p>Opens a dedicated manpage window with search and scroll.</p>
</li>
<li>
<p>Render as a PDF in Preview:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">man -t ls <span class="p">|</span> open -f -a Preview
</span></span></code></pre></div></li>
<li>
<p><strong>ManOpen</strong> — a classic NeXTStep-era tool with its <code>openman</code> helper.</p>
</li>
<li>
<p><strong>Man Reader</strong> — modern, App Store version with tabs, bookmarks, search.</p>
</li>
<li>
<p><strong>Bwana</strong> — browser-based, supports <code>man:foo</code> URIs.</p>
</li>
</ul>
<p>On <strong>Linux</strong>, you’ll find:</p>
<ul>
<li><code>yelp</code> (GNOME Help Browser)</li>
<li><code>khelpcenter</code> (KDE Help Center)</li>
<li><code>tkman</code> (old-school but still works)</li>
<li>Some distros also support <code>man:foo</code> URIs in Firefox/Konqueror.</li>
</ul>
<p><span class="tag red">Check out</span> my <a href="/posts/manfzf/">manfzf script</a>  for a fuzzy CLI manpage picker.</p>
<br>
<p><span class="tag blue">Pro Tip</span> On both macOS and Linux, you can alias <code>man</code> to your favorite viewer:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">man</span><span class="o">=</span><span class="s1">&#39;openman&#39;</span>          <span class="c1"># macOS with ManOpen</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">man</span><span class="o">=</span><span class="s1">&#39;yelp man:&#39;</span>        <span class="c1"># GNOME</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">man</span><span class="o">=</span><span class="s1">&#39;khelpcenter man:&#39;</span> <span class="c1"># KDE</span>
</span></span></code></pre></div><br>
<h2 id="manpage-navigation-keys-less--man-pager">Manpage navigation keys (less / man pager)</h2>
<p>If you spend enough time in the manual you may find the following hotkeys useful:</p>
<table>
  <thead>
      <tr>
          <th>Key</th>
          <th>Function</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>j</code></td>
          <td>Move forward one line. Prefix with a number to move that many lines (e.g., <code>6j</code> moves forward six lines).</td>
      </tr>
      <tr>
          <td><code>k</code></td>
          <td>Move back one line. Prefix with a number to move that many lines.</td>
      </tr>
      <tr>
          <td><code>g</code></td>
          <td>Jump to the top of the manual.</td>
      </tr>
      <tr>
          <td><code>G</code></td>
          <td>Jump to the end of the manual.</td>
      </tr>
      <tr>
          <td><code>f</code></td>
          <td>Move forward one screen. (Space bar does the same.)</td>
      </tr>
      <tr>
          <td><code>b</code></td>
          <td>Move back one screen.</td>
      </tr>
      <tr>
          <td><code>d</code></td>
          <td>Move forward half a screen.</td>
      </tr>
      <tr>
          <td><code>u</code></td>
          <td>Move back half a screen.</td>
      </tr>
      <tr>
          <td><code>/pattern</code></td>
          <td>Search <strong>forward</strong> for <em>pattern</em>.</td>
      </tr>
      <tr>
          <td><code>?pattern</code></td>
          <td>Search <strong>backward</strong> for <em>pattern</em>.</td>
      </tr>
      <tr>
          <td><code>n</code></td>
          <td>Repeat the last search in the same direction.</td>
      </tr>
      <tr>
          <td><code>N</code></td>
          <td>Repeat the last search in the opposite direction.</td>
      </tr>
      <tr>
          <td><code>q</code></td>
          <td>Quit the manual pager.</td>
      </tr>
  </tbody>
</table>
<p>Want to explore more search tricks like wrap-around, advanced pattern control, or highlighting tweaks? Check out the official <code>less(1)</code> manual: <a href="https://man7.org/linux/man-pages/man1/less.1.html">less — Linux manual page (search &amp; navigation)</a> :contentReference[oaicite:1]{index=1}</p>
<hr>
<h2 id="manual-sections-what-those-numbers-mean">Manual sections (what those numbers mean)</h2>
<p>You’ll often see references with a section number like <code>foo(1)</code> or <code>bar(5)</code>—that’s <strong>command_name(section)</strong>.</p>
<table>
  <thead>
      <tr>
          <th>Section</th>
          <th>Scope</th>
          <th>Examples</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>1</strong></td>
          <td>User commands (executables you run)</td>
          <td><code>ls(1)</code>, <code>grep(1)</code></td>
      </tr>
      <tr>
          <td><strong>2</strong></td>
          <td>System calls (kernel entry points)</td>
          <td><code>open(2)</code>, <code>write(2)</code></td>
      </tr>
      <tr>
          <td><strong>3</strong></td>
          <td>Library functions (libc, etc.)</td>
          <td><code>printf(3)</code>, <code>malloc(3)</code></td>
      </tr>
      <tr>
          <td><strong>4</strong></td>
          <td>Special files (devices, drivers)</td>
          <td><code>null(4)</code>, <code>random(4)</code></td>
      </tr>
      <tr>
          <td><strong>5</strong></td>
          <td>File formats &amp; conventions</td>
          <td><code>passwd(5)</code>, <code>crontab(5)</code></td>
      </tr>
      <tr>
          <td><strong>6</strong></td>
          <td>Games &amp; demos</td>
          <td><code>fortune(6)</code>, <code>nethack(6)</code></td>
      </tr>
      <tr>
          <td><strong>7</strong></td>
          <td>Misc (conventions, protocols, standards)</td>
          <td><code>man(7)</code>, <code>regex(7)</code>, <code>ascii(7)</code></td>
      </tr>
      <tr>
          <td><strong>8</strong></td>
          <td>System administration commands</td>
          <td><code>mount(8)</code>, <code>systemctl(8)</code></td>
      </tr>
  </tbody>
</table>
<h3 id="less-common-extras">Less common extras</h3>
<ul>
<li><strong>9</strong> — Kernel routines (driver devs, low-level stuff)</li>
<li><strong>l</strong> — Local (site-specific)</li>
<li><strong>n</strong> — New (experimental)</li>
<li><strong>o</strong> — Old (obsolete)</li>
<li><strong>p</strong> — Public domain (rare)</li>
</ul>
<p>⚡ Example: <code>printf(1)</code> is the shell command, <code>printf(3)</code> is the C library call. Use the number to disambiguate.</p>
<hr>
<h2 id="anatomy-of-a-manpage">Anatomy of a manpage</h2>
<figure style="text-align:center; margin: 1em auto;">
  <img src="flowers.jpg" 
       alt="serene photo of yellow wildflowers in a field" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    If things start to feel too cryptic, just think of the pretty flowers until it passes
  </figcaption>
</figure>
<p>At its core, a man page is written in <strong>roff</strong>, a plain-text markup language for formatting documents. In practice, almost everyone today uses <strong>groff</strong> (the GNU roff toolchain) to process these files.</p>
<p>Each line that starts with a dot (<code>.</code>) is a directive (called a <em>macro</em>), and everything else is plain text. For example:</p>
<ul>
<li><code>.SH NAME</code> → start a new section header</li>
<li><code>.B</code> → make text bold</li>
<li><code>.TP</code> → set up a hanging indent (used in option lists)</li>
</ul>
<p>When you run <code>man</code>, the source file is passed through <strong>groff</strong> with the <code>-man</code> macro package. That expands those directives into the nicely formatted output you see in your terminal. A raw man page looks cryptic but predictable—it’s typesetting code waiting to be compiled into clean columns.</p>
<h3 id="common-sections-and-macros">Common sections and macros</h3>
<p>A man page is built with <code>roff</code> macros. Each <strong>section header</strong> is declared with the <code>.SH</code> macro, and specific formatting macros handle bold, italics, lists, and examples.</p>
<table>
  <thead>
      <tr>
          <th>Section</th>
          <th>Purpose</th>
          <th>Macro(s)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>NAME</strong></td>
          <td>One-line description (<code>name \- summary</code>). Parsed by <code>whatis(1)</code>.</td>
          <td><code>.SH NAME</code></td>
      </tr>
      <tr>
          <td><strong>SYNOPSIS</strong></td>
          <td>Usage syntax: command, options, arguments. Typically uses bold for commands, italics for args.</td>
          <td><code>.SH SYNOPSIS</code>, <code>.B</code>, <code>.I</code>, <code>.RI</code></td>
      </tr>
      <tr>
          <td><strong>DESCRIPTION</strong></td>
          <td>Longer explanation of what the tool does.</td>
          <td><code>.SH DESCRIPTION</code>, text macros (<code>.PP</code> for paragraphs)</td>
      </tr>
      <tr>
          <td><strong>OPTIONS</strong></td>
          <td>Command-line flags, usually formatted as a tagged list.</td>
          <td><code>.SH OPTIONS</code>, <code>.TP</code> (tagged paragraph)</td>
      </tr>
      <tr>
          <td><strong>EXAMPLES</strong></td>
          <td>Literal usage examples.</td>
          <td><code>.SH EXAMPLES</code>, <code>.EX</code> / <code>.EE</code> (example block)</td>
      </tr>
      <tr>
          <td><strong>FILES</strong></td>
          <td>Config or data files related to the program.</td>
          <td><code>.SH FILES</code>, <code>.I</code> (italic filenames)</td>
      </tr>
      <tr>
          <td><strong>ENVIRONMENT</strong></td>
          <td>Environment variables the program respects.</td>
          <td><code>.SH ENVIRONMENT</code>, <code>.TP</code> for listing vars</td>
      </tr>
      <tr>
          <td><strong>EXIT STATUS</strong></td>
          <td>Exit codes and their meanings.</td>
          <td><code>.SH EXIT STATUS</code>, <code>.TP</code></td>
      </tr>
      <tr>
          <td><strong>DIAGNOSTICS</strong></td>
          <td>Explanation of error messages.</td>
          <td><code>.SH DIAGNOSTICS</code>, <code>.TP</code></td>
      </tr>
      <tr>
          <td><strong>BUGS</strong></td>
          <td>Known bugs, limitations, or witty remarks.</td>
          <td><code>.SH BUGS</code></td>
      </tr>
      <tr>
          <td><strong>SEE ALSO</strong></td>
          <td>Cross-references to related commands/libraries.</td>
          <td><code>.SH SEE ALSO</code>, <code>.BR foo (1)</code> for bold+roman</td>
      </tr>
      <tr>
          <td><strong>AUTHOR</strong></td>
          <td>Who wrote/maintains the program.</td>
          <td><code>.SH AUTHOR</code></td>
      </tr>
      <tr>
          <td><strong>COPYRIGHT</strong></td>
          <td>Licensing information.</td>
          <td><code>.SH COPYRIGHT</code></td>
      </tr>
      <tr>
          <td><strong>HISTORY</strong> (optional)</td>
          <td>Historical notes on command evolution.</td>
          <td><code>.SH HISTORY</code></td>
      </tr>
  </tbody>
</table>
<h3 id="handy-formatting-macros">Handy Formatting Macros</h3>
<ul>
<li><code>.B text</code> — bold (usually commands and flags)</li>
<li><code>.I text</code> — italic (filenames, variables)</li>
<li><code>.RI arg1 arg2</code> — roman + italic (e.g., <code>command arg</code>)</li>
<li><code>.TP</code> — tagged paragraph (great for options lists)</li>
<li><code>.EX</code> / <code>.EE</code> — example block, keeps spacing/formatting</li>
<li><code>.BR foo (1)</code> — bold + roman mix, used in <strong>SEE ALSO</strong> references</li>
</ul>
<p>⚡ <strong>Rule of thumb</strong>: section headers (<code>NAME</code>, <code>SYNOPSIS</code>, <code>OPTIONS</code>, etc.) always get <code>.SH</code>. Within those, you mix <code>.B</code>, <code>.I</code>, <code>.TP</code>, etc. to get the formatting you want.</p>
<hr>
<h2 id="minimal-manpage-template-customize-me">Minimal manpage template (customize me)</h2>
<p>Let&rsquo;s take a look at a minimal, complete manpage with helpful comments as a guide to creating your own</p>
<pre tabindex="0"><code class="language-roff" data-lang="roff">.\&#34; ==========================================================
.\&#34; mytool.1 — Minimal, complete manpage template
.\&#34; How to view locally:
.\&#34;   man ./mytool.1
.\&#34;   groff -man -Tutf8 mytool.1 | less
.\&#34;
.\&#34; Conventions:
.\&#34;   - Edit placeholders in ALL CAPS.
.\&#34;   - Keep lines under ~80 cols if you can.
.\&#34;   - Remove comments (lines starting with .\&#34;) when publishing.
.\&#34; ==========================================================

.TH MYTOOL 1 &#34;Aug 2025&#34; &#34;MyTool 1.0&#34; &#34;User Commands&#34;
.\&#34; .TH = Title Header: NAME SECTION DATE VERSION MANUAL-TITLE

.SH NAME
mytool \- one-line summary of what the tool does
.\&#34; The dash must be escaped: \-   (this line is parsed by whatis(1)/apropos)

.SH SYNOPSIS
.B mytool
.RI [ OPTIONS ] &#34; ARG1 &#34; [ ARG2 ... ]
.\&#34; Use .B for the command, .RI to mix roman/italic (nice for args).
.\&#34; Keep synopsis concise; show common forms, not every permutation.

.SH DESCRIPTION
.B mytool
does X in order to achieve Y (1–3 sentences). State defaults and
side-effects briefly. Longer tutorials go in README or web docs.

.PP
Typical use cases:
.IP \[bu] 2
Do foo to bar quickly.
.IP \[bu] 2
Automate baz with a single command.

.SH OPTIONS
.TP
.B -h, --help
Show help and exit.
.TP
.B -v, --version
Print version and exit.
.TP
.B -o, --output \fIFILE\fR
Write output to \fIFILE\fR (default: stdout).
.TP
.B --dry-run
Print what would happen without making changes.

.SH EXAMPLES
.EX
# basic usage
$ mytool --output result.txt input.dat

# preview actions
$ mytool --dry-run input.dat

# multiple files
$ mytool -o out/ merged*.csv
.EE

.SH FILES
.I ~/.config/mytool/config.yml
Optional configuration file.
.PP
.I /var/log/mytool/mytool.log
Log file (if logging enabled).

.SH ENVIRONMENT
.TP
.B MYTOOL_CONFIG
Path to an alternate config file.
.TP
.B MYTOOL_DEBUG
Set to 1 for verbose diagnostics.

.SH EXIT STATUS
.TP
.B 0
Success.
.TP
.B 1
General error (see DIAGNOSTICS).
.TP
.B 2
Invalid arguments.

.SH DIAGNOSTICS
Common error messages and remedies:
.TP
.B &#34;cannot open file&#34;
Check permissions and that the path exists.
.TP
.B &#34;unknown option&#34;
See
.BR mytool (1)
under OPTIONS.

.SH SEE ALSO
.BR grep (1),
.BR awk (1),
.BR roff (7),
.BR help2man (1)

.SH AUTHOR
Your Name &lt;you@example.com&gt;

.SH COPYRIGHT
Copyright \(co 2025 Your Name.
License: MIT.

.SH VERSION
1.0.0
</code></pre><br>
<figure style="text-align:center; margin: 1em auto;">
  <img src="mytool.1.jpg" 
       alt="screenshot of man ./mytool.1" 
       style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    That creates a manpage with the familiar structure and conventions. 
  </figcaption>
</figure>
<p>That&rsquo;s all there is to it. Again it&rsquo;s just plain text and some macros and escapes to control formatting.</p>
<p><span class="tag green">Download</span> If you would like to download that template as a ready to go stub, you can grab it <a href="mytool.1">Here</a></p>
<hr>
<p>To view it without installing first, you can do the following:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># simplest: point man directly at it</span>
</span></span><span class="line"><span class="cl">man ./test.1
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># or, explicitly render with groff for debugging layout</span>
</span></span><span class="line"><span class="cl">groff -man -Tutf8 ./test.1 <span class="p">|</span> less
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># on macOS, you can preview it in Preview.app as a PDF:</span>
</span></span><span class="line"><span class="cl">man -t ./test.1 <span class="p">|</span> open -f -a Preview
</span></span></code></pre></div><br>
<h2 id="using-help2man-to-automatically-generate-documentation-for-your-scripts">Using <code>help2man</code> to automatically generate documentation for your scripts</h2>
<p><code>help2man</code> is a little utility that auto-generates manpages from the output of your program’s <code>--help</code> and <code>--version</code> flags. Instead of hand-crafting a <code>.1</code> file with <code>groff</code> macros, you can let your script speak for itself, and <code>help2man</code> will translate that into a standard UNIX manual page.</p>
<p>It’s lightweight, doesn’t need you to write <code>groff</code>, and is often bundled in Debian packaging workflows for quick manpages. The downside, I&rsquo;ve found, is that help2man produces relatively ugly output regardless of how good your <code>--help</code> output is. You&rsquo;ll probably find yourself polishing it to make it look nice but it does produce reasonable enough output.</p>
<hr>
<h3 id="requirements">Requirements</h3>
<p>To work with <code>help2man</code>, your script or program must support:</p>
<ul>
<li><code>-h</code> / <code>--help</code> → prints a usage summary and available options</li>
<li><code>-v</code> / <code>--version</code> → prints the program name and version</li>
<li>the script must be executable</li>
</ul>
<p>That’s all <code>help2man</code> needs to create a basic manual page.</p>
<p><span class="tag blue">Pro-Tip:</span> Keep your &ndash;help text tidy and descriptive—help2man copies those lines directly into the man page. If your help is garbage, your man page will be, too.</p>
<hr>
<h3 id="minimal-script-example">Minimal script example</h3>
<p>Here’s a tiny, example Python script that satisfies those conditions:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="ch">#!/usr/bin/env python3</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">NAME</span> <span class="o">=</span> <span class="s2">&#34;myscript&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">VERSION</span> <span class="o">=</span> <span class="s2">&#34;1.0&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="s2">&#34;-h&#34;</span> <span class="ow">in</span> <span class="n">sys</span><span class="o">.</span><span class="n">argv</span> <span class="ow">or</span> <span class="s2">&#34;--help&#34;</span> <span class="ow">in</span> <span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Usage: </span><span class="si">{</span><span class="n">NAME</span><span class="si">}</span><span class="s2"> [options]</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Options:&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;  -h, --help     Show this help message&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;  -v, --version  Show version info&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="s2">&#34;-v&#34;</span> <span class="ow">in</span> <span class="n">sys</span><span class="o">.</span><span class="n">argv</span> <span class="ow">or</span> <span class="s2">&#34;--version&#34;</span> <span class="ow">in</span> <span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">NAME</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">VERSION</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Hello, world!&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">main</span><span class="p">()</span>
</span></span></code></pre></div><p>Make it executable:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">chmod +x myscript
</span></span></code></pre></div><h3 id="generating-the-manpage">Generating the manpage</h3>
<p>Run <code>help2man</code> on your script:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">help2man ./myscript &gt; myscript.1
</span></span></code></pre></div><p>Preview it with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">man ./myscript.1
</span></span></code></pre></div><h3 id="optional-polish">Optional polish</h3>
<p>You can add extra details with flags:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">help2man --name<span class="o">=</span><span class="s2">&#34;Tiny demo script&#34;</span> --section<span class="o">=</span><span class="m">1</span> ./myscript &gt; myscript.1
</span></span></code></pre></div><p>And install it for local use:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo install -m <span class="m">0644</span> myscript.1 /usr/local/share/man/man1/
</span></span><span class="line"><span class="cl">sudo mandb 2&gt;/dev/null <span class="o">||</span> <span class="nb">true</span>
</span></span><span class="line"><span class="cl">man myscript
</span></span></code></pre></div><hr>
<h2 id="-installing-a-manpage-locally">📦 Installing a manpage locally</h2>
<p>After you’ve got a <code>.1</code> file you’re happy with, install it into the system manpath so <code>man mytool</code> works like any built‑in command. Section <strong>1</strong> pages usually live under <code>/usr/local/share/man/man1/</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 1) Copy your page into section 1</span>
</span></span><span class="line"><span class="cl">sudo install -m <span class="m">0644</span> mytool.1 /usr/local/share/man/man1/
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2) Rebuild the whatis/apropos database</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Linux:</span>
</span></span><span class="line"><span class="cl">sudo mandb
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># macOS (BSD mandoc):</span>
</span></span><span class="line"><span class="cl">sudo makewhatis /usr/local/share/man
</span></span><span class="line"><span class="cl"><span class="c1"># (Tip: `man makewhatis` for details)</span>
</span></span></code></pre></div><p>Verify:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Where will man find it?</span>
</span></span><span class="line"><span class="cl">man -w mytool
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Does whatis/apropos see it?</span>
</span></span><span class="line"><span class="cl">whatis mytool  <span class="c1"># or: apropos mytool</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Use it!</span>
</span></span><span class="line"><span class="cl">man mytool
</span></span></code></pre></div><h3 id="userlocal-install-no-sudo">User‑local install (no sudo)</h3>
<p>You can keep manpages in your home directory:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Put it in your personal manpath:</span>
</span></span><span class="line"><span class="cl">install -d <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.local/share/man/man1&#34;</span>
</span></span><span class="line"><span class="cl">install -m <span class="m">0644</span> mytool.1 <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.local/share/man/man1/&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Point man(1) at it for this shell:</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">MANPATH</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.local/share/man:</span><span class="k">$(</span>manpath 2&gt;/dev/null <span class="o">||</span> <span class="nb">echo</span> /usr/share/man:/usr/local/share/man<span class="k">)</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"># Or scope just one lookup:</span>
</span></span><span class="line"><span class="cl">man -M <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.local/share/man&#34;</span> mytool
</span></span></code></pre></div><hr>
<h2 id="troubleshooting-quickies">Troubleshooting quickies</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># See your active manpath (Linux):</span>
</span></span><span class="line"><span class="cl">manpath
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># On macOS, print the search path:</span>
</span></span><span class="line"><span class="cl">man -w
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># If apropos/whatis doesn’t show your entry yet:</span>
</span></span><span class="line"><span class="cl">sudo mandb                         <span class="c1"># Linux</span>
</span></span><span class="line"><span class="cl">sudo makewhatis /usr/local/share/man  <span class="c1"># macOS</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check the NAME line format (parsed by whatis):</span>
</span></span><span class="line"><span class="cl"><span class="c1"># must be: &#34;name \- one line summary&#34; (note the escaped dash \-)</span>
</span></span><span class="line"><span class="cl">grep -n <span class="s1">&#39;^\.SH NAME&#39;</span> -n mytool.1 -n
</span></span></code></pre></div><hr>
<h2 id="links-and-stuff">Links and stuff</h2>
<p><a href="https://en.wikipedia.org/wiki/Man_page">man page on wikipedia</a>
<a href="https://www.gnu.org/software/help2man/">help2man Reference Manual</a></p>
<h2 id="conclusion">Conclusion</h2>
<blockquote>
<p>&ldquo;Whoever undertakes to set himself up as a judge of Truth and Knowledge is
shipwrecked by the laughter of the gods.&rdquo;
&ndash; Albert Einstein</p></blockquote>
<p>That’s it for now. Manpages can feel ancient and mysterious, but they’re still one of the most reliable ways to give your tools polish and permanence. With just a text file and a few macros, you can drop your work into the same lineage as <code>ls(1)</code> and <code>grep(1)</code>.</p>
<p>If you’ve got a script you use every day, give it a manpage. Start small — a <code>NAME</code>, a <code>SYNOPSIS</code>, maybe a couple of <code>OPTIONS</code>. The next time you type <code>man myscript</code>, you’ll see your own work sitting comfortably alongside fifty years of Unix history.</p>
<p>💡 Got a favorite trick, or did this inspire you to write a wild cheatsheet manpage? Did you find yourself thinking of the pretty flowers? I’d love to hear it: <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Tag Audit</title>
      <link>https://adminjitsu.com/posts/tag-audit/</link>
      <pubDate>Tue, 19 Aug 2025 14:22:55 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/tag-audit/</guid>
      <description>Tag Audit is a tiny Python CLI that scans Hugo front matter for tag and category usage. Quickly spot singletons, get totals, and export mappings for cleanup or docs.</description>
      <content:encoded><![CDATA[<h2 id="introduction">Introduction</h2>
<p align="center">
  <img src="hugo-logo-wide.svg" alt="Hugo Logo" style="max-width:420px;">
</p>
<p>If you run a Hugo site, you’ve probably ended up with a messy tag list at some point.<br>
<br>
Duplicate tags, one-off singletons, forgotten categories… it all builds up over time and clutters your taxonomies. Hugo won’t clean them for you.</p>
<p>I wanted a way to <strong>see exactly which tags and categories I’ve used, how often, and where</strong> — so I built a quick little scanner. It&rsquo;s great for spring cleaning your frontmatter—but fast, and without the sneezing.</p>
<br>
<h3 id="elevator-pitch"><em>Elevator pitch:</em></h3>
<p><strong>Tag Audit</strong> is a no-frills Python CLI that:</p>
<ul>
<li>Counts tag and category usage across your content tree</li>
<li>Flags singletons (items used only once)</li>
<li>Shows which posts use which tag or category</li>
<li>Outputs in text, markdown, CSV, or JSON</li>
</ul>
<br>
<h2 id="quickstart">Quickstart</h2>
<p class="github-btn">
  <a href="https://github.com/forfaxx/tag-audit" target="_blank">
    🔗 View tag-audit on GitHub
  </a>
</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Install dep</span>
</span></span><span class="line"><span class="cl">python3 -m pip install pyyaml
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Basic run (from your Hugo repo root)</span>
</span></span><span class="line"><span class="cl">python3 tag-audit.py
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># One file only</span>
</span></span><span class="line"><span class="cl">python3 tag-audit.py --file content/posts/metaclean.md
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show which files use each tag (markdown output)</span>
</span></span><span class="line"><span class="cl">python3 tag-audit.py --by-tag --format markdown
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Top 20 tags/categories by count, with mappings</span>
</span></span><span class="line"><span class="cl">python3 tag-audit.py --top <span class="m">20</span> --by-tag --by-cat
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Ignore drafts, only include items used ≥ 2 times</span>
</span></span><span class="line"><span class="cl">python3 tag-audit.py --ignore-drafts --min-count <span class="m">2</span>
</span></span></code></pre></div><h2 id="features">Features</h2>
<ul>
<li>Scan entire Hugo <code>content/</code> tree or a single file</li>
<li>Totals for tags and categories</li>
<li>Singleton detection (used only once)</li>
<li>Inverse mappings: files grouped by tag or category</li>
<li>Multiple output formats: <strong>text</strong>, <strong>markdown</strong>, <strong>csv</strong>, <strong>json</strong></li>
<li>Filters: <code>--min-count</code>, <code>--top</code>, <code>--ignore-drafts</code>, <code>--ext</code></li>
</ul>
<hr>
<h2 id="usage">Usage</h2>
<p><strong>tag-audit.py</strong> <code>[OPTIONS]</code></p>
<p><strong>Options (common):</strong></p>
<ul>
<li><code>--dir PATH</code> — Path to Hugo content (default: <code>./content</code>)</li>
<li><code>--file FILE</code> — Scan a single file</li>
<li><code>--ignore-drafts</code> — Skip drafts</li>
<li><code>--per-file</code> — Show per-file usage</li>
<li><code>--by-tag</code> — Show files grouped by tag</li>
<li><code>--by-cat</code> — Show files grouped by category</li>
<li><code>--format</code> — <code>text</code> (default), <code>markdown</code>, <code>csv</code>, <code>json</code></li>
<li><code>--sort</code> — Sort by <code>count</code> (default) or <code>alpha</code></li>
<li><code>--min-count N</code> — Only include items with count ≥ N</li>
<li><code>--top N</code> — Limit to top N items</li>
</ul>
<hr>
<h2 id="sample-output">Sample Output</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Tags
</span></span><span class="line"><span class="cl">====
</span></span><span class="line"><span class="cl">count  name
</span></span><span class="line"><span class="cl">-----  ----------------
</span></span><span class="line"><span class="cl">   21  cli
</span></span><span class="line"><span class="cl">   14  python
</span></span><span class="line"><span class="cl">   13  utilities
</span></span><span class="line"><span class="cl">   10  bash
</span></span><span class="line"><span class="cl">    5  unix
</span></span><span class="line"><span class="cl">    4  fun
</span></span><span class="line"><span class="cl">    4  sysadmin
</span></span><span class="line"><span class="cl">    3  cromulent
</span></span><span class="line"><span class="cl">    3  gibberish
</span></span><span class="line"><span class="cl">    3  homelab
</span></span><span class="line"><span class="cl">    3  productivity
</span></span><span class="line"><span class="cl">    3  scripting
</span></span><span class="line"><span class="cl">    2  hugo
</span></span><span class="line"><span class="cl">    2  linux
</span></span><span class="line"><span class="cl">    2  star-trek
</span></span><span class="line"><span class="cl">    1  adminjitsu
</span></span><span class="line"><span class="cl">    1  automation
</span></span><span class="line"><span class="cl">    1  commodore-64
</span></span><span class="line"><span class="cl">    1  zsh
</span></span><span class="line"><span class="cl">    [...]
</span></span><span class="line"><span class="cl">Total tags: 171
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Categories
</span></span><span class="line"><span class="cl">==========
</span></span><span class="line"><span class="cl">count  name
</span></span><span class="line"><span class="cl">-----  ---------------
</span></span><span class="line"><span class="cl">   22  projects
</span></span><span class="line"><span class="cl">   13  tools
</span></span><span class="line"><span class="cl">    3  fun
</span></span><span class="line"><span class="cl">    3  trivia
</span></span><span class="line"><span class="cl">    2  guides
</span></span><span class="line"><span class="cl">    2  workflow
</span></span><span class="line"><span class="cl">    1  retrocomputing
</span></span><span class="line"><span class="cl">    1  sysadmin
</span></span><span class="line"><span class="cl">    1  tips
</span></span><span class="line"><span class="cl">    [...]
</span></span><span class="line"><span class="cl">Total categories: 61
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Singleton tags (used only once)
</span></span><span class="line"><span class="cl">===============================
</span></span><span class="line"><span class="cl">count  name
</span></span><span class="line"><span class="cl">-----  ------------------------
</span></span><span class="line"><span class="cl">    1  adminjitsu
</span></span><span class="line"><span class="cl">    1  automation
</span></span><span class="line"><span class="cl">    1  commodore-64
</span></span><span class="line"><span class="cl">    1  cryptography
</span></span><span class="line"><span class="cl">    1  docker
</span></span><span class="line"><span class="cl">    1  espanso
</span></span><span class="line"><span class="cl">    1  fortune
</span></span><span class="line"><span class="cl">    1  vim
</span></span><span class="line"><span class="cl">    1  zsh
</span></span><span class="line"><span class="cl">    [...]
</span></span><span class="line"><span class="cl">Total singleton tags: 69
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Singleton categories (used only once)
</span></span><span class="line"><span class="cl">=====================================
</span></span><span class="line"><span class="cl">count  name
</span></span><span class="line"><span class="cl">-----  ------------------------------
</span></span><span class="line"><span class="cl">    1  retrocomputing
</span></span><span class="line"><span class="cl">    1  sandbox
</span></span><span class="line"><span class="cl">    1  sysadmin
</span></span><span class="line"><span class="cl">    1  troubleshooting
</span></span><span class="line"><span class="cl">    [...]
</span></span><span class="line"><span class="cl">Total singleton categories: 9
</span></span></code></pre></div><h3 id="files-by-tag-excerpt">Files by Tag (excerpt)</h3>
<h4 id="linux">linux</h4>
<ul>
<li><code>content/posts/kernel-tips.md</code></li>
<li><code>content/posts/bash-wizardry.md</code></li>
</ul>
<h4 id="docker">docker</h4>
<ul>
<li><code>content/posts/compose-cleanup.md</code></li>
<li><code>content/posts/build-secrets.md</code></li>
</ul>
<h4 id="hugo">hugo</h4>
<ul>
<li><code>content/posts/theme-tweaks.md</code></li>
<li><code>content/posts/tag-audit-release.md</code></li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>Whether you’re prepping a cleanup pass or just curious which tags are pulling their weight, <code>tag-audit.py</code> gives you a clear snapshot of your Hugo taxonomy.</p>
<p>PRs, issues, and suggestions welcome!
<a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Self-Reports</title>
      <link>https://adminjitsu.com/posts/self-reports/</link>
      <pubDate>Mon, 18 Aug 2025 16:48:13 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/self-reports/</guid>
      <description>Printers can print test pages showing ink and network settings. Why can’t servers? These small scripts print self-diagnostics with consistent formatting, emoji, and color. Drop them in /usr/local/bin and any box can tell you exactly how it’s doing.</description>
      <content:encoded><![CDATA[<h2 id="what-are-self-reports">What are Self-Reports?</h2>
<p>Printers have it figured out. Push a button or two → get a neat printed summary of ink, alignment, and IP address.<br>
I wanted the same for my machines. Jumping between machines on SSH can get disorienting as you try to remember how each is configured and what is going on with it. I thought about how nice it would be to have printer test-page style reports for my servers.</p>
<p>So I wrote a small set of self-report scripts—Bash and Python tools I keep in <code>/usr/local/bin</code>—that generate clean, colorful status reports. They don’t try to be full monitoring systems; they’re quick checkups I can run over SSH or right after login. It gives me a familiar, nice report from that machine&rsquo;s point of view. Simple scripts—powerful concept.</p>
<h2 id="features">Features</h2>
<p>The family so far:</p>
<ul>
<li><code>disk_report.sh</code> → Disk usage &amp; mount points</li>
<li><code>mem_report.sh</code> → Memory &amp; swap snapshot</li>
<li><code>lan_report.sh</code> → Interfaces, routes, DNS, public IP, open ports</li>
<li><code>pi-health.sh</code> → Hardware report specific to Raspberry Pi and PironMan5 hardware</li>
<li><code>py_report.py</code> → Python environment snapshot (<code>sys.path</code>, pip packages, <code>-m</code> modules, etc.)</li>
</ul>
<br> 
<p>The scripts are designed with consistency in mind. Some highlights:</p>
<ul>
<li>Consistent formatting across reports (emoji + color)</li>
<li>Runs on Linux, macOS, and WSL</li>
<li>Zero dependencies (optional: prettier output with <code>eza</code>)</li>
<li>Drop-in friendly → just copy into <code>/usr/local/bin</code></li>
<li>Focused: each report covers one domain (disk, memory, LAN, etc.)</li>
<li>Safe: read-only reports, no risk of changing config</li>
</ul>
<h2 id="quickstart">QuickStart</h2>
<p>You can grab a copy of my report scripts to use or modify from GitHub:</p>
<p class="github-btn"> <a href="https://github.com/forfaxx/self-reports" target="_blank"> 🔗 View Self-Reports on GitHub </a> </p>
<br> 
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone https://github.com/forfaxx/self-reports.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> self-reports
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">sudo cp *.sh *.py /usr/local/bin/
</span></span><span class="line"><span class="cl">sudo chmod +x /usr/local/bin/*
</span></span></code></pre></div><br> 
<p><span class="tag green">Optional:</span> strip <code>.sh</code> when installing if you prefer shorter command names:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo cp disk_report.sh /usr/local/bin/disk_report
</span></span><span class="line"><span class="cl">sudo cp mem_report.sh  /usr/local/bin/mem_report
</span></span><span class="line"><span class="cl">sudo cp lan_report.sh  /usr/local/bin/lan_report
</span></span><span class="line"><span class="cl">sudo cp pi-health.sh   /usr/local/bin/pi-health
</span></span><span class="line"><span class="cl">sudo cp py_report.py   /usr/local/bin/py_report
</span></span></code></pre></div><p><span class="tag blue">Tip:</span>
You should now be able to run any of the reports. The real power in my experience has been in having these on each of my machines with public key based ssh authentication to make it easy to run them from anywhere:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh user@host mem_report
</span></span></code></pre></div><br> 
<h2 id="-sample-output">🖥️ Sample Output</h2>
<p>See the README.md on GitHub for detailed output examples for each script.</p>
<p>Basically what you get from each is a consistent, on-demand report from that machine&rsquo;s perspective. Read-only and with minimal dependencies so you can run it anywhere, anytime you need to refresh your memory.</p>
<p>Here is a taste of the output for mem_report.sh (I remove the .sh extensions in my examples. it&rsquo;s up to you).</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">🧠 Memory Report <span class="k">for</span>: server.example.com
</span></span><span class="line"><span class="cl">🕒 Date: Mon <span class="m">18</span> Aug 20:44:57 CDT <span class="m">2025</span>
</span></span><span class="line"><span class="cl">🧬 OS: Linux
</span></span><span class="line"><span class="cl">----------------------------------------
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">📊 Memory Summary:
</span></span><span class="line"><span class="cl">               total        used        free      shared  buff/cache   available
</span></span><span class="line"><span class="cl">Mem:           7.9Gi       2.9Gi       425Mi        21Mi       5.1Gi       5.0Gi
</span></span><span class="line"><span class="cl">Swap:           17Gi       1.4Gi        16Gi
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">📈 Load Averages:
</span></span><span class="line"><span class="cl"> 20:44:57 up <span class="m">32</span> days, 23:49,  <span class="m">2</span> users,  load average: 1.30, 1.36, 1.51
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">🔁 Swap Usage:
</span></span><span class="line"><span class="cl">NAME       TYPE      SIZE USED PRIO
</span></span><span class="line"><span class="cl">/dev/zram0 partition   2G 1.4G  <span class="m">100</span>
</span></span><span class="line"><span class="cl">/swapfile  file       16G   0B   <span class="m">10</span>
</span></span><span class="line"><span class="cl">  SwapCached:         <span class="m">6656</span> kB
</span></span><span class="line"><span class="cl">  SwapTotal:      <span class="m">18874336</span> kB
</span></span><span class="line"><span class="cl">  SwapFree:       <span class="m">17425664</span> kB
</span></span><span class="line"><span class="cl">  Zswap:                 <span class="m">0</span> kB
</span></span><span class="line"><span class="cl">  Zswapped:              <span class="m">0</span> kB
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">🌀 ZRAM Status:
</span></span><span class="line"><span class="cl">zram0:
</span></span><span class="line"><span class="cl">  disksize            : <span class="m">2147483648</span>
</span></span><span class="line"><span class="cl">  compr_data_size     : <span class="o">(</span>missing<span class="o">)</span>
</span></span><span class="line"><span class="cl">  mem_used_total      : <span class="o">(</span>missing<span class="o">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">🐘 Top Memory Consumers:
</span></span><span class="line"><span class="cl">    PID    PPID CMD                          %MEM %CPU
</span></span><span class="line"><span class="cl">   <span class="m">1001</span>     <span class="m">999</span> /usr/bin/sync-daemon          4.0 64.8
</span></span><span class="line"><span class="cl">   <span class="m">2048</span>    <span class="m">2000</span> /usr/local/bin/webapp         2.3  0.2
</span></span><span class="line"><span class="cl">   <span class="m">3001</span>    <span class="m">2988</span> homebridge: bridge-addon      2.0  0.0
</span></span><span class="line"><span class="cl">    <span class="m">325</span>       <span class="m">1</span> /lib/systemd/systemd-journa   2.0  0.0
</span></span><span class="line"><span class="cl">   <span class="m">1111</span>     <span class="m">888</span> /usr/bin/db-service           1.7  0.3
</span></span><span class="line"><span class="cl">   <span class="m">4000</span>    <span class="m">3990</span> node app/server.js            1.6  0.4
</span></span><span class="line"><span class="cl">   <span class="m">4020</span>    <span class="m">4010</span> /usr/bin/dns-resolver         1.5  0.5
</span></span><span class="line"><span class="cl">   <span class="m">4500</span>    <span class="m">4400</span> background-service            1.1  0.0
</span></span><span class="line"><span class="cl">   <span class="m">5000</span>       <span class="m">1</span> /usr/bin/containerd-shim      0.7  0.2
</span></span></code></pre></div><h2 id="conclusion">Conclusion</h2>
<p>That’s the whole idea: quick, colorful self-reports you can run on any machine.<br>
Not a monitoring system, not a dashboard—just a friendly check-in, like a printer test page for your servers.</p>
<p>I’ve already found these scripts handy when I ssh into a box I haven’t touched in a while, or when I want to sanity-check a Pi on the shelf. They’re small, portable, and don’t step on anything else.</p>
<p>💡 Got an idea for another kind of report? Found a bug? Or maybe you’ve written your own flavor?<br>
I’d love to see it:</p>
<p>📬 <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a><br>
🔗 <a href="https://github.com/forfaxx/self-reports">Self-Reports GitHub repo</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Breathing New Life Into Vintage Macs</title>
      <link>https://adminjitsu.com/posts/vintage-macs/</link>
      <pubDate>Sat, 16 Aug 2025 18:46:02 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/vintage-macs/</guid>
      <description>Turn old Intel Macs into useful Linux machines. A practical guide to dual-booting Ubuntu with macOS, solving Wi-Fi issues, and more.</description>
      <content:encoded><![CDATA[<h2 id="heading">⌘</h2>
<p> Apple’s Intel-era hardware is still really nice to use — slim cases, good keyboards, decent screens — but the software side ages out more quickly. A 2013 iMac or a 2017 MacBook Pro may be perfectly usable physically, yet Apple’s support window means no new macOS releases, no security updates, and a shrinking list of installable apps.</p>
<p>Rather than throw them away, I’ve started putting Linux on them. It’s a great way to keep solid but “obsolete” Macs relevant — giving them new roles instead of letting them gather dust. My iMac, once rarely touched, is now a genuinely useful part of my daily environment. The MacBook Pro that sat idle after my last upgrade is back in service too, reborn as a powerful Ubuntu machine wrapped in sleek Apple hardware.</p>
<p>And the best part? I didn’t have to give up macOS entirely. By preserving the original partition, I can still boot into my old system when needed. That dual-boot peace of mind is a big selling point.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img
    src="i-still-function.jpg"
    alt="Pile of discarded electronic waste including old computers and monitors"
    style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;"
  >
  <figcaption style="font-size:85%; font-weight:normal; color:#666; line-height:1.4; margin-top:0.4em;">
    Frame from <em>Transformers: The Movie</em> (1986), © Hasbro — used here under fair use as cultural commentary.
  </figcaption>
</figure>
<p>Luckily the process is so much easier than it used to be. No Boot Camp required! Still, I have a few tips that should make the process go more smoothly.</p>
<br>
<h2 id="the-install-process">The Install Process</h2>
<p><span class="tag red">IMPORTANT</span> <em>Make a <strong>Time Machine backup</strong> (or the moral equivalent) before you follow any of my steps.</em></p>
<p>Also, before diving into the install, it’s worth giving your Mac a quick cleanup — wipe the screen, clear the dust, and <a href="https://support.apple.com/en-us/102605">reset the SMC</a> for good measure.</p>
<br>
<p>The procedure I used was to download the latest amd64 image of Ubuntu Desktop (At time of writing I chose Ubuntu 24.04.3 LTS). For Intel macs you want the Intel or AMD 64-bit architecture version. I have tested this with Ubuntu, Mint and Debian.</p>
<ul>
<li>
<p><a href="https://ubuntu.com/download/desktop">https://ubuntu.com/download/desktop</a></p>
</li>
<li>
<p><a href="https://help.ubuntu.com/lts/ubuntu-help/index.html">Ubuntu Desktop Guide</a></p>
</li>
</ul>
<p>Next, you will need a USB thumb drive. An 8gb or larger drive should be plenty. There are different ways to write the image to the flash drive but I used <a href="https://etcher.balena.io/">Balena Etcher</a> which is available for Windows, Mac and Linux. The program is easy to use, just make sure to choose the correct drive (it only displays removable drives on my machines which makes it extra simple). You will then choose the Linux iso image you downloaded and begin the process. It can take a while and I found that letting my Macbook Pro idle interrupted the process. The second time I kept the computer from locking or sleeping until the process finished.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="balena-etcher-flashing.png" 
       alt="Balena etcher flashing an image to a USB drive" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Balena Etcher takes a few minutes to image your flash drive. Be patient and don't let the system idle. 
  </figcaption>
</figure>
<p>You should now have an Ubuntu (or whatever distro you prefer) bootable thumb drive.</p>
<p>For my machines, I wanted to retain my macOS partition as opposed to giving Linux the whole drive. This would allow me to dual-boot if desired. Regardless of your choice, you should backup any files or the OS if you care about them. Time Machine on Mac is a great solution.</p>
<br>
<h2 id="partitioning-an-intel-mac-for-linux">Partitioning an Intel Mac for Linux</h2>
<blockquote>
<p><em>&ldquo;What you don&rsquo;t know can hurt you, only you won&rsquo;t know it.&rdquo;</em></p></blockquote>
<br>
<p>To resize the disks, reboot into Recovery mode by holding Command-R while the system boots (just before or when the startup chime sounds).</p>
<p>Choose Disk Utility and find the drive with your Macintosh HD volume. If you use APFS and FileVault, you will need to mount the encrypted volume by highlighting it and clicking the mount button and entering your password. Once that is done you can select</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="disk-utility-mount.jpeg" 
       alt="screenshot of disk utility" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    If you use apfs you will need to mount the encrypted data partition before you can resize it
  </figcaption>
</figure>
<p><span class="tag orange">Note:</span> On older Intel Macs (pre-2011), Internet Recovery (<code>Command + Option + R</code>) may not be available unless you’ve applied a firmware update. In that case you’ll need a local recovery partition or an installer USB.</p>
<p>In <strong>Disk Utility</strong>, the key is knowing the difference between <strong>containers/volumes</strong> and the <strong>physical drive</strong>.</p>
<hr>
<h3 id="-the-hierarchy-in-disk-utility">🔍 The Hierarchy in Disk Utility</h3>
<ul>
<li><strong>Physical drive</strong> → the actual SSD, usually named something like <em>Apple SSD…</em> at the top of the tree.</li>
<li><strong>APFS container</strong> → a partition on that physical drive which contains APFS volumes.</li>
<li><strong>APFS volumes</strong> → what you see as “Macintosh HD” (and its companions like “Macintosh HD – Data”).</li>
</ul>
<hr>
<h3 id="-the-step-that-matters">⚡ The Step That Matters</h3>
<p>When you want to make space for Linux, you <strong>don’t add a volume</strong> inside the APFS container. Volumes just share the container’s space dynamically, and Linux can’t install into an APFS volume.</p>
<p>Instead, you:</p>
<ol>
<li>Reboot into <strong>Recovery mode</strong> by holding <code>Command + R</code> as the system boots (right before or when the startup chime plays).
<ul>
<li>If <code>Command + R</code> doesn’t work, try <strong>Internet Recovery</strong> with <code>Command + Option + R</code>.</li>
</ul>
</li>
<li>Open <strong>Disk Utility</strong>.</li>
<li>If you use <strong>APFS with FileVault</strong>, mount the encrypted volume:
<ul>
<li>Highlight it, click <strong>Mount</strong>, and enter your password.</li>
</ul>
</li>
<li>Select the <strong>physical disk</strong> (the top entry in the sidebar, e.g. <em>Apple SSD…</em>).
<ul>
<li>Not just “Macintosh HD.”</li>
<li>Not just the APFS container.</li>
</ul>
</li>
<li>Click <strong>Partition</strong> (not “Add Volume”).</li>
<li>Shrink the APFS container and create a new partition for Linux:
<ul>
<li><strong>Free Space</strong> is the cleanest choice (Ubuntu installer will handle formatting).</li>
<li><code>MS-DOS (FAT)</code> also works as a placeholder, since Ubuntu will reformat it anyway.</li>
</ul>
</li>
</ol>
<figure style="text-align:center; margin: 1em auto;">
  <img src="mac-repartition.jpg" 
       alt="detailed screenshot of partition display in disk utility" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    With the encrypted volume mounted in Recovery mode Disk Utility, you are can now add a new partition from free space. <br> Choose MS-Dos or Mac OS Extended for now. Ubuntu will reformat it later. 
  </figcaption>
</figure>
<br>
<p><span class="tag orange">NOTE:</span> If Disk Utility refuses to shrink the APFS container, boot from the installer USB and use <code>diskutil</code> from Terminal instead:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">diskutil apfs resizeContainer disk0s2 200g  
</span></span></code></pre></div><p>(where <code>disk0s2</code> is your APFS container and <code>200g</code> is the new size).</p>
<p><span class="tag orange">NOTE:</span> If you&rsquo;re unsure, 40–60GB (or more) should give you a decent amount of legroom in Linux.</p>
<hr>
<h3 id="-why-that-step-is-crucial">📝 Why That Step is Crucial</h3>
<ul>
<li>If you add an APFS volume → Linux can’t use it.</li>
<li>If you resize the container and carve out a partition → Ubuntu can see that space and format it as ext4.</li>
</ul>
<hr>
<br> 
<h2 id="installing-linux">Installing Linux</h2>
<p>Once you have a partition, you&rsquo;re ready to boot from your installer.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="startup-manager.jpeg" 
       alt="Pile of discarded electronic waste including old computers and monitors" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Your bootable thumb drive should have an orange removable disk icon and the label EFI Boot
  </figcaption>
</figure>
<ul>
<li>Hold the <strong>Option (Alt)</strong> key while booting to enter Startup Manager.
<ul>
<li>On some Macs, the USB installer may appear as <strong>EFI Boot</strong> instead of “Ubuntu.”</li>
</ul>
</li>
<li>Select the Linux USB installer and choose the option to install Linux.
<ul>
<li><a href="https://ubuntu.com/tutorials/install-ubuntu-desktop#4-boot-from-usb-flash-drive">Documentation</a>.</li>
</ul>
</li>
<li>I chose the standard <strong>Interactive Installation</strong> and then selected <strong>Manual Installation</strong> so I could point Ubuntu at the new partition.</li>
</ul>
<figure style="text-align:center; margin: 1em auto;">
  <img src="ubuntu-install.jpeg" 
       alt="Pile of discarded electronic waste including old computers and monitors" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    You should see Grub and the option to Install Ubuntu at this point. 
  </figcaption>
</figure>
<p><span class="tag orange">Note:</span> On some Intel Macs you may need to <strong>disable Secure Boot</strong> in the Ubuntu installer. The Mac firmware itself doesn’t enforce Secure Boot — this is specific to Ubuntu’s shim loader. If the system refuses to boot the USB or drops you back to Startup Manager, toggling Secure Boot in the installer usually fixes it.</p>
<hr>
<h3 id="manual-partitioning">Manual Partitioning</h3>
<p>When you reach the <strong>Installation Type</strong> screen:</p>
<ol>
<li>Select <strong>Something else</strong> (manual partitioning).</li>
<li>Highlight the empty partition you created in Disk Utility.
<ul>
<li>Do <strong>not</strong> touch your macOS/APFS container.</li>
</ul>
</li>
<li>Choose <strong>Change</strong> → set the following:
<ul>
<li><strong>Use as:</strong> <code>Ext4 journaling file system</code></li>
<li><strong>Mount point:</strong> <code>/</code></li>
<li><strong>Format:</strong> checked (so the installer formats the new partition).</li>
</ul>
</li>
<li>Leave your macOS volumes alone — Ubuntu will install side-by-side.</li>
</ol>
<p>The installer will then set up Ubuntu on the Linux partition while preserving macOS. At boot you can hold <strong>Option (Alt)</strong> to pick which OS you want to load.</p>
<p><span class="tag green">Pro-Tip:</span> If you want a nicer boot menu instead of the plain “EFI Boot” entry, check out <a href="https://www.rodsbooks.com/refind/">rEFInd</a>, a lightweight boot manager that makes dual-booting friendlier.</p>
<h2 id="first-boot-hurdles-networking">First Boot Hurdles: Networking</h2>
<p>The biggest issue I encountered doing this was that Wi-Fi wasn&rsquo;t detected in the Ubuntu installer. I found simple enough workarounds and I am pretty sure from my research that I could have used any number of Wi-Fi USB dongles (as a temporary workaround) and they would have been supported and detected instantly. The objective is just to get online long enough to enable Wi-Fi.</p>
<p>On the iMac I was able to use an Ethernet cable so Wi-Fi wasn&rsquo;t a priorty like it might be in another room.</p>
<p>For the Macbook Pro, I found that my Thunderbolt to Ethernet adapter was not detected—Ethernet was out.</p>
<p>The workaround I used was to tether my phone. It&rsquo;s easy to connect your iPhone with a USB cable and connect via tethering. You&rsquo;ll need to unlock your device and trust the computer when prompted and the system should prompt you to activate a wired connection.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="ios-trust.jpeg" 
       alt="The iOS Trust This Computer display" 
       style="display:block; margin:0 auto; width:min(100%, 400px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Make sure to Trust This Computer on your phone, when prompted. Ubuntu will then allow you to use it as a Wired Connection
  </figcaption>
</figure>
<figure style="text-align:center; margin: 1em auto;">
  <img src="ubuntu-tethered-phone.jpeg" 
       alt="Ubuntu setup screen showing an available wired connection after tethering iPhone with USB cable" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Once thethered, you will be able to select 'Use Wired Connection' to connect to the Internet. 
  </figcaption>
</figure>
<figure style="text-align:center; margin: 1em auto;">
  <img src="ubuntu-3rd-party.jpeg" 
       alt="Install 3rd party drivers screen" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
<blockquote>
<p>You can verify which Broadcom chip you have with the following command
<code>lspci -nn | grep -i network</code></p></blockquote>
<p>Go ahead and install the proprietary drivers during install although this didn&rsquo;t contain the Wi-Fi driver. To install that, I had to do the following additional step to install the WiFi drivers:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt update  
</span></span><span class="line"><span class="cl">sudo apt install bcmwl-kernel-source  
</span></span><span class="line"><span class="cl">reboot  
</span></span></code></pre></div><p>After reboot, Wi-Fi was working and I was able to connect.</p>
<p><span class="tag orange">Note:</span> Some newer Broadcom chipsets may need <code>broadcom-sta-dkms</code> instead of <code>bcmwl-kernel-source</code>. If Wi-Fi doesn’t come up after reboot, try:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt install broadcom-sta-dkms  
</span></span></code></pre></div><h2 id="post-install-setup">Post-Install Setup</h2>
<p>Great Success! Ubuntu is now installed and Wi-Fi is working.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="ubuntu-boot.jpeg" 
       alt="Ubuntu booting on Macbook Pro" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Booting into Ubuntu on Mac hardware. nice. 
  </figcaption>
</figure>
<figure style="text-align:center; margin: 1em auto;">
  <img src="ubuntu-on-mac.jpeg" 
       alt="Ubuntu running on a Macbook Pro" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Ubuntu on Macbook Pro
  </figcaption>
</figure>
<p>I&rsquo;m working on documenting the steps to get all of the built in hardware working. The facetime camera doesn&rsquo;t show up by default, for instance. I installed <code>hardinfo</code> and it sees most of it though.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="hardinfo.jpg" 
       alt="hardinfo display showing Apple hardware" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    `hardinfo` shows that almost all of my hardware was detected out of the box. 
  </figcaption>
</figure>
<p><span class="tag green">Pro-Tip:</span> To smooth out hardware quirks:</p>
<ul>
<li><strong>Graphics</strong>: If you have an Nvidia GPU, install the proprietary <code>nvidia-driver</code> package instead of relying on Nouveau.</li>
<li><strong>Fans &amp; thermals</strong>: Macs can run hot under Linux. Install <code>mbpfan</code> to get sane fan control.</li>
<li><strong>Backlight &amp; keyboard</strong>: The <code>pommed</code> utility or kernel backlight drivers may be needed for brightness and keyboard controls.</li>
</ul>
<p>Check out my quick guide that I used with both machines.
<a href="/posts/meet-gir/#zram-for-efficient-swap">ZRAM for Efficient Swap</a></p>
<h2 id="the-project-side">The Project Side</h2>
<blockquote>
<p><em>I have looked in the mirror every morning and asked myself: &ldquo;If today were the
last day of my life, would I want to do what I am about to do today?&rdquo; And
whenever the answer has been &ldquo;No&rdquo; for too many days in a row, I know I need to
change something.</em>
&ndash; Steve Jobs (1955-2011)</p></blockquote>
<br>
<p><strong>So what can you actually <em>do</em> with a Linux-powered Mac?</strong></p>
<p>As mentioned, my iMac has been pulling its weight as a reliable little workstation — and it’s held up better than I expected. The MacBook Pro is newly imaged, but even while drafting this article I found myself tweaking it, customizing it, and genuinely enjoying the experience. Sure, I miss a few macOS niceties, but Ubuntu is very usable day to day.</p>
<p>Most of my favorite apps are cross-platform these days anyway: VS Code, PyCharm, Obsidian, OBS Studio, Audacity, Spotify, Firefox, Chromium. Add in a proper Unix environment and my go-to wallpapers, and the machine felt like home almost instantly.</p>
<p>Compared to my Raspberry Pi server, these Intel Macs bring serious advantages: quiet cooling, rock-solid build quality, and hardware that was premium in its day. That makes them a fantastic middle ground between tiny low-power boards and brand-new desktops.</p>
<p>I can&rsquo;t wait to build more docker containers and a top-notch dev and test laptop!</p>
<h2 id="links-and-stuff">Links and Stuff</h2>
<h3 id="essential-references-used-in-this-article">Essential references used in this article</h3>
<table>
  <thead>
      <tr>
          <th>Resource</th>
          <th>Link</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Mac startup key combinations</td>
          <td><a href="https://support.apple.com/en-us/102603">https://support.apple.com/en-us/102603</a></td>
      </tr>
      <tr>
          <td>Reset the SMC of your Mac</td>
          <td><a href="https://support.apple.com/en-us/102605">https://support.apple.com/en-us/102605</a></td>
      </tr>
      <tr>
          <td>Disk Utility User Guide</td>
          <td><a href="https://support.apple.com/guide/disk-utility/partition-a-physical-disk-dskutl14027/mac">https://support.apple.com/guide/disk-utility/partition-a-physical-disk-dskutl14027/mac</a></td>
      </tr>
      <tr>
          <td>Ubuntu Desktop Download</td>
          <td><a href="https://ubuntu.com/download/desktop">https://ubuntu.com/download/desktop</a></td>
      </tr>
      <tr>
          <td>Ubuntu Desktop Guide</td>
          <td><a href="https://help.ubuntu.com/lts/ubuntu-help/index.html">https://help.ubuntu.com/lts/ubuntu-help/index.html</a></td>
      </tr>
      <tr>
          <td>Ubuntu Installation Tutorial</td>
          <td><a href="https://ubuntu.com/tutorials/install-ubuntu-desktop#4-boot-from-usb-flash-drive">https://ubuntu.com/tutorials/install-ubuntu-desktop#4-boot-from-usb-flash-drive</a></td>
      </tr>
      <tr>
          <td>Balena Etcher</td>
          <td><a href="https://etcher.balena.io/">https://etcher.balena.io/</a></td>
      </tr>
      <tr>
          <td>rEFInd Boot Manager</td>
          <td><a href="https://www.rodsbooks.com/refind/">https://www.rodsbooks.com/refind/</a></td>
      </tr>
  </tbody>
</table>
<hr>
<h3 id="other-resources-that-might-be-useful">Other resources that might be useful</h3>
<table>
  <thead>
      <tr>
          <th>Resource</th>
          <th>Link</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Ubuntu Certified Hardware (searchable DB)</td>
          <td><a href="https://certification.ubuntu.com/">https://certification.ubuntu.com/</a></td>
      </tr>
      <tr>
          <td>Ubuntu Community Help Wiki: MacBookPro</td>
          <td><a href="https://help.ubuntu.com/community/MacBookPro">https://help.ubuntu.com/community/MacBookPro</a></td>
      </tr>
      <tr>
          <td>Ubuntu Wi‑Fi Troubleshooting</td>
          <td><a href="https://help.ubuntu.com/community/WifiDocs/WirelessTroubleShootingGuide">https://help.ubuntu.com/community/WifiDocs/WirelessTroubleShootingGuide</a></td>
      </tr>
      <tr>
          <td>Touchegg (multi‑touch gestures)</td>
          <td><a href="https://github.com/JoseExposito/touchegg">https://github.com/JoseExposito/touchegg</a></td>
      </tr>
      <tr>
          <td>Thunderbolt on Linux (boltd/boltctl)</td>
          <td><a href="https://gitlab.freedesktop.org/bolt/bolt">https://gitlab.freedesktop.org/bolt/bolt</a></td>
      </tr>
      <tr>
          <td>ArchWiki: MacBook Pro</td>
          <td><a href="https://wiki.archlinux.org/title/MacBookPro">https://wiki.archlinux.org/title/MacBookPro</a></td>
      </tr>
      <tr>
          <td>ArchWiki: MacBook</td>
          <td><a href="https://wiki.archlinux.org/title/MacBook">https://wiki.archlinux.org/title/MacBook</a></td>
      </tr>
      <tr>
          <td>ArchWiki: Broadcom wireless</td>
          <td><a href="https://wiki.archlinux.org/title/Broadcom_wireless">https://wiki.archlinux.org/title/Broadcom_wireless</a></td>
      </tr>
      <tr>
          <td>mbpfan (fan control for MacBooks)</td>
          <td><a href="https://github.com/dgraziotin/mbpfan">https://github.com/dgraziotin/mbpfan</a></td>
      </tr>
  </tbody>
</table>
<br>
<hr>
<h2 id="closing-thought">Closing Thought</h2>
<figure style="text-align:center; margin: 1em auto;">
  <img src="Electronic_waste.jpg" 
       alt="Pile of discarded electronic waste including old computers and monitors" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <em>Linux gives old Macs a second chance before they end up here</em><br>
    Pictured: Electronic waste, photograph by <a href="https://commons.wikimedia.org/wiki/File:Electronic_waste.jpg" target="_blank">Katherine Welles (CC BY-SA 3.0)</a>
  </figcaption>
</figure>
<p>Like my older iMac, this MacBook Pro is now running Ubuntu instead of sitting idle. It’s not worth much on eBay, but it’s too valuable to throw away — and with Linux, it still has plenty of life left.</p>
<p>The project is about more than just recycling hardware. It’s about taking machines Apple has orphaned and turning them into relevant, capable systems again.</p>
]]></content:encoded>
    </item>
    <item>
      <title>My Friend Hugo: A Unix-First Blogging Workflow</title>
      <link>https://adminjitsu.com/posts/my-friend-hugo/</link>
      <pubDate>Fri, 15 Aug 2025 16:40:42 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/my-friend-hugo/</guid>
      <description>A hands-on guide to building a Hugo-powered blog with Unix-first workflows: from editing and sanity checks to publishing scripts and responsive CSS tweaks. Featuring Hugo tips, CLI tools, and automation tricks that make blogging fast, lightweight, and fun.</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<figure style="text-align:center; margin:1em auto;">
  <img src="hugo-logo-wide.svg" 
       alt="Wide horizontal logo of the Hugo static site generator" 
       style="display:block; margin:0 auto; width:min(100%,400px); height:auto;">
  <figcaption style="font-size:85%; font-weight:normal; color:#666; line-height:1.4; margin-top:0.4em;">
    another thrilling meta article about Hugo!
  </figcaption>
</figure>
<p>When I decided to start this site, I thought WordPress was the obvious choice. I&rsquo;ve used it before for a few projects but every time I revisit WordPress, I am quickly reminded that I just don&rsquo;t enjoy working in it. There are so many options and plugins and themes that it becomes a bit overwhelming and I just wanted to start writing.</p>
<p>Django was certainly an option—I&rsquo;ve built a number of decent web applications with it over the years and could certainly build a django-based blog but that was also a huge stack of technology to deal with before I could write the first hello world post.</p>
<p>Happily I stumbled upon the Hugo static site generator via a <a href="https://www.youtube.com/results?search_query=hugo+static+site+generator">YouTube rabbit hole</a>. It was markdown-based, fast, simple and scriptable and lends itself to some clever CLI workflows. Exactly what I was going for.</p>
<p style="text-align:left;">
  <a href="/tags/hugo" class="button">🧾 View Other Hugo Posts</a>
</p>
<hr>
<h2 id="first-impressions">First Impressions</h2>
<blockquote>
<p>&ldquo;I changed my headlights the other day. I put in strobe lights instead! Now
when I drive at night, it looks like everyone else is standing still &hellip;&rdquo;
&ndash; Steven Wright</p></blockquote>
<p>Some impressions after 2 months of working with Hugo (especially compared to other CMS or manual web design approaches):</p>
<ul>
<li>Markdown everywhere = good.</li>
<li>GoLang code, optimized for speed and flexible templating. Active development.</li>
<li>Easy to use inline HTML with a setting in config.toml</li>
<li>Just about everything can be configured and overridden if desired</li>
<li>PaperMod theme is clean but takes some tweaking to look “less academic.”</li>
<li>Git feels natural here; the site is a repo. Easy to jump between machines and keep working.</li>
<li>CLI + scripts give me more control than a CMS ever would.</li>
<li>Static site generation so the site is fast and lightweight while still featuring some impressive scripted bits</li>
</ul>
<hr>
<h2 id="editing-options">Editing Options</h2>
<p>I’ve tried a bunch of different tools to write and edit posts:</p>
<ul>
<li><strong>Vim</strong>: unbeatable for quick edits, regex magic, and staying close to the shell.</li>
<li><strong>VS Code</strong>: excellent overall, though it still lacks a truly reliable spellchecker.</li>
<li><strong>Obsidian</strong>: great for note-taking and linking ideas, but with some quirks.</li>
<li><strong>Typora</strong>: clean interface and live preview, though not much beyond Hugo’s built-in preview.</li>
</ul>
<p>These days I bounce between VS Code (for structure) and Vim (for quick fixes), always wishing for a universal spellcheck to catch my numerous typos. Working in Markdown keeps me close to the writing while hiding the verbosity of HTML and CSS. When I need more control, Goldmark’s settings let me drop into raw HTML without friction.</p>
<p>Obsidian is very tempting to use but it&rsquo;s vault structure is dirty with respect to git with lots of things you have to add to <code>.gitignore</code> with varying degrees of difficulty. I originally made a sync script to maintain a mirror between Obsidian and Hugo&rsquo;s production directory in my codelab folder. That worked but I vastly prefer VS Code with its integrated Terminal and Git support as my main editor. The drawbacks with VS Code are a lack of spellchecking and a separate tabbed view for rendered markdown. Not a big deal.</p>
<p>My workflow is simple: one terminal tab runs <code>hugo server -D</code>, while two browser tabs track the local dev server and the public site. Make a change (usually in VSCode), and see it reflected instantly. Everything is tracked in <code>git</code>. When it’s time to publish, one script takes it live.</p>
<hr>
<h2 id="publishing-workflow">Publishing Workflow</h2>
<p>After setup, the main challenge was getting updates live without friction. WordPress gave me gui menus, Hugo gives me a (Go) binary. Perfect excuse to write my own tooling. I was able to quickly identify a manual workflow and then build a nice, robust script to automate it. What a lovely experience.</p>
<p>What I ended up with was the following <strong>publish script</strong> that I run almost daily. It handles cleaning, building, syncing—everything I don’t want to think about (or mess up).</p>
<p class="github-btn">
  <a href="https://github.com/forfaxx/publish-hugo" target="_blank">
    🔗 View publish-hugo on GitHub
  </a>
</p>
<p>Why not Git hooks or CI? Because I like the flexibility of running this script on any machine, without a network dependency.</p>
<p>The script automates the full Hugo publishing cycle with built-in safety checks. It starts by making sure any <code>hugo server</code> process is stopped, then moves into the correct project directory and performs a clean rebuild with <code>hugo --cleanDestinationDir</code>. Before anything goes live, it can snapshot your work in Git:</p>
<ul>
<li>Show a concise diff if there are uncommitted changes.</li>
<li>Prompt you whether to commit and push.</li>
<li>Auto-build the commit message from changed .md files, leaving a traceable record of what was published.</li>
</ul>
<p>From there, <code>publish-hugo.sh</code> (or <code>publish-adminjitsu.sh</code>) adds some quality-of-life touches:</p>
<ul>
<li><strong>Draft detection</strong> – warns you if you’re about to ship drafts (forgot to flip status).</li>
<li><strong>Rsync prompt</strong> – asks before running the destructive <code>rsync --delete</code>.</li>
<li><strong>Dry-run mode</strong> – lets you preview the sync without touching the server.</li>
</ul>
<p>The rsync step itself is straightforward but effective, mirroring your <code>public/</code> folder to the remote web root in one shot. And because it’s a Bash script, the UX has personality: colorful prompts, conversational feedback, and even a celebratory <code>fortune</code> if you have that installed.</p>
<p>In practice, this makes for a deploy process that’s safe, repeatable, and just a little bit fun.</p>
<hr>
<h2 id="pre-press-and-sanity-checks">Pre-Press and Sanity Checks</h2>
<blockquote>
<p>“In theory there is no difference between theory and practice. In practice there is.”
&ndash; Benjamin Brewster</p></blockquote>
<p>Publishing text isn’t just about writing—it’s also about cleaning and organizing. In my hugo directory I created a <code>scratch/</code> folder and <code>scratch/bin/</code> to hold helper scripts and test pages as I create them. It&rsquo;s a good place to keep site tools and I have built a few decent ones already.</p>
<hr>
<h3 id="metaclean">metaclean</h3>
<p>Metaclean is a simple, robust tool for working with images in pre-press workflows. It can scan and strip metadata in a variety of ways, which makes it very scriptable.</p>
<blockquote>
<p><span class="tag red">Check out</span> my 🔗 <a href="/posts/metaclean/">Metaclean</a> post for the script.</p></blockquote>
<hr>
<h3 id="tag-audit">tag-audit</h3>
<p>Tag audit is a simple script that reports on category and tag usage in my posts. It helps me to keep my taxonomies straight and to spot typos and outliers and to refresh my memory without looking at each posts front matter. All in a nice, flexible report.</p>
<blockquote>
<p><span class="tag red">Check out</span> my 🔗 <a href="/posts/tag-audit/">Tag Audit</a> post for the script.</p></blockquote>
<hr>
<h3 id="asset-demos">asset demos</h3>
<p>I find it useful to keep various demo pages in my <code>scratch/bin</code> folder. Hugo ignores arbitrary folders like this and scratch is my convention. I use it to store all sorts of helpers and notes that are strictly relating to the site as opposed to a more general tool.</p>
<blockquote>
<p>Some examples of things that I find useful to keep around:</p>
<ul>
<li>font-previews</li>
<li>BOILERPLATE.txt with useful snippets</li>
<li>POST-IDEAS.txt where I can jot down rough ideas that pop into my head</li>
<li>various little test scripts</li>
<li>a WIP bin where I can keep articles that I&rsquo;m not ready to work on yet</li>
</ul></blockquote>
<p>One tool that is extremely useful is this dynamic javascript powered feather icon picker. Feather icons are SVG (vector based) icons that scale to any size and have a number of styling options. My picker is pretty cool despite being hard as heck to get just right. It allows you to easily browse, search, filter, adjust stroke width, scale, and copy the icon in a ready-to-go <code>&lt;i data-feather=&quot;home&quot;&gt;&lt;/i&gt;</code> block that you can copy and paste, like here:<br>
<i data-feather="home"></i>
<i data-feather="users"></i>
<i data-feather="headphones"></i>
<i data-feather="search" width="30" height="30" stroke-width="1.5" stroke="#4e45f8"></i>
<i data-feather="home" width="30" height="30" stroke-width="1.5" stroke="#fa435f"></i></p>
<p><span class="tag red">Check out</span> 🔗 my <a href="/feather-picker.html">feather-picker.html</a> tool</p>
<br>
<h2 id="tips-and-tricks">Tips and Tricks</h2>
<p>Some CLI tools and habits that keep me sane:</p>
<ul>
<li><code>find</code>, <code>grep</code>, <code>sed</code>, and <code>awk</code> for batch edits.</li>
<li><code>vim -p</code> to open multiple posts side by side.</li>
<li>Running Hugo with <code>--cleanDestinationDir</code> so stale files don’t stick around.</li>
<li>Using shortcodes and custom CSS tweaks from my style sheet.</li>
</ul>
<hr>
<h3 id="problem-solving">Problem solving</h3>
<p>So far, the majority of issues I&rsquo;ve encountered can be resolved by stopping the server, running <code>hugo --cleanDestinationDir</code> and starting the server again. The other is that I forgot to set draft to false in the frontmatter. Virtually every other problem will produce a big, descriptive error on the dev server giving you a chance to fix the problem and saving you from uploading (most) broken code to your webhost. Getting shortcodes to work can be a little tricky but bouncing the server helps with runtime issues.</p>
<p>Git is your friend when it gets confusing:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 1. Show changes to a file vs. last commit</span>
</span></span><span class="line"><span class="cl">git diff HEAD -- path/to/file
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2. Restore file from the last commit (discard current changes)</span>
</span></span><span class="line"><span class="cl">git restore path/to/file
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 3. Restore file to a specific commit (useful when you know the good hash)</span>
</span></span><span class="line"><span class="cl">git checkout &lt;commit-hash&gt; -- path/to/file
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 4. If you want to revert *all* changes since last commit (scorched earth)</span>
</span></span><span class="line"><span class="cl">git reset --hard HEAD
</span></span></code></pre></div><hr>
<h3 id="style-playground">Style playground</h3>
<p>I made this demo page public for fun and interest. Basically I have a style dojo page where I try out each markdown element and made it useful as a glyph board from which I can copy various colors, and feather, unicode, and emoji symbols. It&rsquo;s great having a silly, but useful, testing ground where I can quickly try out CSS changes and see the results in one place. The original markdown also gives me a crib sheet from which I can copy useful snippets when I forget the syntax.</p>
<p>I kind of adore living documentation!</p>
<blockquote>
<p><span class="tag red">Check out</span> the 🔗 <a href="/posts/ninjas/">Ninjas Style Dojo</a> post for the shenanigans.</p></blockquote>
<br>
<hr>
<h3 id="css-for-left-side-floating-table-of-contents">CSS for Left-side floating Table of Contents</h3>
<p>I created <code>z_toc-left.css</code> in <code>assets/css/extended</code> containing the following to create and lock my floating toc to the left side of the content area:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="cl"><span class="c">/* --- FLOATING TOC ON LEFT (FIXED TO CONTENT COLUMN) --- */</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">#</span><span class="nn">toc-sidebar</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">position</span><span class="p">:</span> <span class="kc">fixed</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">top</span><span class="p">:</span> <span class="mi">120</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c">/* 1000px max content width → 500px half width */</span>
</span></span><span class="line"><span class="cl">    <span class="c">/* 320px TOC width + 48px gap */</span>
</span></span><span class="line"><span class="cl">    <span class="k">left</span><span class="p">:</span> <span class="nb">calc</span><span class="p">(</span><span class="mi">50</span><span class="kt">%</span> <span class="o">-</span> <span class="mi">500</span><span class="kt">px</span> <span class="o">-</span> <span class="mi">368</span><span class="kt">px</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">width</span><span class="p">:</span> <span class="mi">320</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">max-width</span><span class="p">:</span> <span class="mi">380</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">max-height</span><span class="p">:</span> <span class="nb">calc</span><span class="p">(</span><span class="mi">100</span><span class="kt">vh</span> <span class="o">-</span> <span class="mi">140</span><span class="kt">px</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">overflow-y</span><span class="p">:</span> <span class="kc">auto</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">background</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">entry</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">border</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">border-radius</span><span class="p">:</span> <span class="mi">8</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">padding</span><span class="p">:</span> <span class="mi">1</span><span class="kt">em</span> <span class="mf">1.5</span><span class="kt">em</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">box-shadow</span><span class="p">:</span> <span class="mi">0</span> <span class="mi">2</span><span class="kt">px</span> <span class="mi">6</span><span class="kt">px</span> <span class="nb">rgba</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">z-index</span><span class="p">:</span> <span class="mi">1000</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">@</span><span class="k">media</span> <span class="o">(</span><span class="nt">max-width</span><span class="o">:</span> <span class="nt">1380px</span><span class="o">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="p">#</span><span class="nn">toc-sidebar</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">position</span><span class="p">:</span> <span class="kc">static</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">width</span><span class="p">:</span> <span class="kc">auto</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">max-height</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">box-shadow</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">margin-bottom</span><span class="p">:</span> <span class="mi">1</span><span class="kt">rem</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">left</span><span class="p">:</span> <span class="kc">unset</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>I have been experimenting with settings as I test on different platforms. So far it works quite well on wide screens, collapsing to an inline menu on narrower windows (like mobile browsers). It&rsquo;s not perfect but it&rsquo;s easy to tweak. Worst case, I just git restore <code>z_toc-left.css</code> and I’m back to a clean setup</p>
<hr>
<p><span class="tag purple">Update</span> It was a bit of a challenge to get my TOC to indent and format headings correctly, so I thought I would share what I figured out. Here’s the CSS + JS combo I landed on to make heading levels show up clearly in the sidebar:</p>
<p>I added the following CSS to <strong><code>assets/css/extended/z_toc-left.css</code></strong>:
(this handles the indentation, bullets, and subtle guide rails once each TOC item has a depth class)</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="cl"><span class="c">/* === BEGIN: Adminjitsu TOC Indent v1 === */</span>
</span></span><span class="line"><span class="cl"><span class="c">/* Make sure list markers/indent can actually show */</span>
</span></span><span class="line"><span class="cl"><span class="p">#</span><span class="nn">toc-sidebar</span> <span class="nt">ol</span><span class="o">,</span>
</span></span><span class="line"><span class="cl"><span class="p">#</span><span class="nn">toc-sidebar</span> <span class="nt">ul</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">list-style-position</span><span class="p">:</span> <span class="kc">outside</span> <span class="cp">!important</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">#</span><span class="nn">toc-sidebar</span> <span class="nt">li</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">list-style</span><span class="p">:</span> <span class="kc">disc</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c">/* H2 (##) */</span>
</span></span><span class="line"><span class="cl"><span class="p">#</span><span class="nn">toc-sidebar</span> <span class="nt">li</span><span class="p">.</span><span class="nc">toc-depth-2</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">margin-left</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">font-weight</span><span class="p">:</span> <span class="mi">600</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c">/* H3 (###) */</span>
</span></span><span class="line"><span class="cl"><span class="p">#</span><span class="nn">toc-sidebar</span> <span class="nt">li</span><span class="p">.</span><span class="nc">toc-depth-3</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">margin-left</span><span class="p">:</span> <span class="mi">1</span><span class="kt">rem</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">list-style-type</span><span class="p">:</span> <span class="kc">circle</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">font-size</span><span class="p">:</span> <span class="mf">0.95</span><span class="kt">em</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">opacity</span><span class="p">:</span> <span class="mf">0.9</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c">/* H4 (####) */</span>
</span></span><span class="line"><span class="cl"><span class="p">#</span><span class="nn">toc-sidebar</span> <span class="nt">li</span><span class="p">.</span><span class="nc">toc-depth-4</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">margin-left</span><span class="p">:</span> <span class="mf">1.6</span><span class="kt">rem</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">list-style-type</span><span class="p">:</span> <span class="kc">square</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">font-size</span><span class="p">:</span> <span class="mf">0.9</span><span class="kt">em</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">opacity</span><span class="p">:</span> <span class="mf">0.8</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c">/* Optional: subtle guide rail for sublevels */</span>
</span></span><span class="line"><span class="cl"><span class="p">#</span><span class="nn">toc-sidebar</span> <span class="nt">li</span><span class="p">.</span><span class="nc">toc-depth-3</span><span class="o">,</span>
</span></span><span class="line"><span class="cl"><span class="p">#</span><span class="nn">toc-sidebar</span> <span class="nt">li</span><span class="p">.</span><span class="nc">toc-depth-4</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">position</span><span class="p">:</span> <span class="kc">relative</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">#</span><span class="nn">toc-sidebar</span> <span class="nt">li</span><span class="p">.</span><span class="nc">toc-depth-3</span><span class="p">::</span><span class="nd">before</span><span class="o">,</span>
</span></span><span class="line"><span class="cl"><span class="p">#</span><span class="nn">toc-sidebar</span> <span class="nt">li</span><span class="p">.</span><span class="nc">toc-depth-4</span><span class="p">::</span><span class="nd">before</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">content</span><span class="p">:</span> <span class="s2">&#34;&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">position</span><span class="p">:</span> <span class="kc">absolute</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">left</span><span class="p">:</span> <span class="mf">-0.6</span><span class="kt">rem</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">top</span><span class="p">:</span> <span class="mf">0.25</span><span class="kt">em</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">bottom</span><span class="p">:</span> <span class="mf">0.25</span><span class="kt">em</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">width</span><span class="p">:</span> <span class="mi">1</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">background</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">tertiary</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="c">/* === END: Adminjitsu TOC Indent v1 === */</span></span></span></code></pre></div>

  </div>
</details>

<p>And added the following script to <strong><code>layouts/partials/extend_footer.html</code></strong>:
(the theme’s TOC doesn’t tag list items by heading level out of the box, so this script inspects the page headings, figures out their depth, and adds a class like <code>toc-depth-2</code> or <code>toc-depth-3</code>. Without this, our CSS wouldn’t know which styles to apply.)</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="c1">// === BEGIN: Adminjitsu TOC Depth Tagger v1 ===
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">TOC_CONTAINER</span> <span class="o">=</span> <span class="s1">&#39;#toc-sidebar&#39;</span><span class="p">;</span> <span class="c1">// your left TOC wrapper
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl">  <span class="kd">function</span> <span class="nx">tagTOCDepth</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">toc</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="nx">TOC_CONTAINER</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">toc</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">links</span> <span class="o">=</span> <span class="nx">toc</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;a[href^=&#34;#&#34;]&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">links</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nx">links</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">a</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="kr">const</span> <span class="nx">id</span> <span class="o">=</span> <span class="nx">a</span><span class="p">.</span><span class="nx">getAttribute</span><span class="p">(</span><span class="s1">&#39;href&#39;</span><span class="p">).</span><span class="nx">slice</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">id</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="kr">const</span> <span class="nx">h</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="nx">id</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">h</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="kr">const</span> <span class="nx">depthMap</span> <span class="o">=</span> <span class="p">{</span> <span class="nx">H2</span><span class="o">:</span> <span class="mi">2</span><span class="p">,</span> <span class="nx">H3</span><span class="o">:</span> <span class="mi">3</span><span class="p">,</span> <span class="nx">H4</span><span class="o">:</span> <span class="mi">4</span><span class="p">,</span> <span class="nx">H5</span><span class="o">:</span> <span class="mi">5</span><span class="p">,</span> <span class="nx">H6</span><span class="o">:</span> <span class="mi">6</span> <span class="p">};</span>
</span></span><span class="line"><span class="cl">      <span class="kr">const</span> <span class="nx">depth</span> <span class="o">=</span> <span class="nx">depthMap</span><span class="p">[</span><span class="nx">h</span><span class="p">.</span><span class="nx">tagName</span><span class="p">]</span> <span class="o">||</span> <span class="mi">2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="kr">const</span> <span class="nx">li</span> <span class="o">=</span> <span class="nx">a</span><span class="p">.</span><span class="nx">closest</span><span class="p">(</span><span class="s1">&#39;li&#39;</span><span class="p">)</span> <span class="o">||</span> <span class="nx">a</span><span class="p">.</span><span class="nx">parentElement</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="nx">li</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">li</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">c</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="k">if</span> <span class="p">(</span><span class="nx">c</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="s1">&#39;toc-depth-&#39;</span><span class="p">))</span> <span class="nx">li</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">remove</span><span class="p">(</span><span class="nx">c</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">});</span>
</span></span><span class="line"><span class="cl">        <span class="nx">li</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="sb">`toc-depth-</span><span class="si">${</span><span class="nx">depth</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;DOMContentLoaded&#39;</span><span class="p">,</span> <span class="nx">tagTOCDepth</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="nb">window</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;load&#39;</span><span class="p">,</span> <span class="nx">tagTOCDepth</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">mo</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">((</span><span class="nx">muts</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">(</span><span class="kr">const</span> <span class="nx">m</span> <span class="k">of</span> <span class="nx">muts</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="nx">m</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="s1">&#39;childList&#39;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">tagTOCDepth</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="cl">  <span class="nx">mo</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">subtree</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="p">})();</span>
</span></span><span class="line"><span class="cl"><span class="c1">// === END: Adminjitsu TOC Depth Tagger v1 ===
</span></span></span></code></pre></div>

  </div>
</details>

<hr>
<h3 id="css-to-increase-content-area-width-on-wide-screens">CSS to increase content area width on wide screens</h3>
<p>I added the following (I&rsquo;m using Paper Mod so check the documentation for your theme) to <code>theme-vars-override.css</code>. Customize the <code>--main-width</code> variable.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="cl"><span class="p">:</span><span class="nd">root</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nv">--code-block-bg</span><span class="p">:</span> <span class="mh">#58585a</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nv">--code-text-color</span><span class="p">:</span> <span class="mh">#f8f8f2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nv">--main-width</span><span class="p">:</span> <span class="mi">1000</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><br>
<hr>
<h3 id="responsive-image-use">Responsive image use</h3>
<p>I had to experiment a bit to find the right, responsive pattern for images. Without it they go full KAIJU mode and spill out of the content area on smaller screens rather than resizing like you probably want. You can see this in action in the following two examples. Either resize your browser down or view on a small screen and the first example behaves while the second tries to destroy Tokyo</p>
<figure class="shadowed" style="text-align:center; margin:1em auto;">
  <img src="kaiju.jpg"
       alt="Godzilla behind the scenes - responsive example"
       style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size:85%; font-weight:normal; color:#666; line-height:1.4; margin-top:0.4em;">
    Responsive: scales with the column on small screens, never exceeds 800&nbsp;px on wide screens.
  </figcaption>
</figure>
<figure style="text-align:center; margin:1em auto;">
  <img src="kaiju.jpg"
       alt="Godzilla behind the scenes - non-responsive example"
       style="display:block; margin:0 auto; max-width:800px;">
  <figcaption style="font-size:85%; font-weight:normal; color:#666; line-height:1.4; margin-top:0.4em;">
    Broken on phones: uses only <code>max-width:800px</code>, so it overflows when the content area is narrower than 800&nbsp;px.
  </figcaption>
</figure>
<p>The key is to use <code>width:min(100%, 800px);</code> or whatever maximum width you desire, in the style line like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">figure</span> <span class="na">style</span><span class="o">=</span><span class="s">&#34;text-align:center; margin: 1em auto;&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;kaiju.jpg&#34;</span>
</span></span><span class="line"><span class="cl">       <span class="na">alt</span><span class="o">=</span><span class="s">&#34;Godzilla behind the scenes&#34;</span>
</span></span><span class="line"><span class="cl">       <span class="na">style</span><span class="o">=</span><span class="s">&#34;display:block; margin:0 auto; width:min(100%, 800px); height:auto;&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">figcaption</span> <span class="na">style</span><span class="o">=</span><span class="s">&#34;font-size:85%; color:#666; line-height:1.4; margin-top:0.4em;&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    Responsive kaiju — scales down neatly on phones, capped at 800px wide.
</span></span><span class="line"><span class="cl">  <span class="p">&lt;/</span><span class="nt">figcaption</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">figure</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>An example of a responsive, plain <code>img</code> tag:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">p</span> <span class="na">align</span><span class="o">=</span><span class="s">&#34;center&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;kaiju.jpg&#34;</span>
</span></span><span class="line"><span class="cl">       <span class="na">alt</span><span class="o">=</span><span class="s">&#34;Godzilla behind the scenes&#34;</span>
</span></span><span class="line"><span class="cl">       <span class="na">style</span><span class="o">=</span><span class="s">&#34;width:min(100%, 800px); height:auto; display:block; margin:0 auto;&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>Plain markdown images <code>![](foo.jpg)</code> in PaperMod (and most Hugo themes) sets the rendered html tag to <code>max-width:100%; height:auto;</code> in CSS so they are responsive out of the box. The downside of course is that you can&rsquo;t tweak captions, widths or add styles.</p>
<p><span class="tag red">Check out</span> my 🔗 <a href="/posts/espanso-and-friends/">Espanso</a> howto for a good way to keep track of boilerplate like proper html 5 figure statements and other long snippets.</p>
<hr>
<h3 id="report-functions">Report Functions</h3>
<p>Add the following to your startup files (e.g., <code>.bashrc</code> or <code>.zshrc</code>) and reload.</p>
<p>This reports on each article with the title and a word and line count. Good for a quick overview</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">article_stats<span class="o">(){</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> <span class="nv">count</span><span class="o">=</span><span class="m">0</span>
</span></span><span class="line"><span class="cl">  <span class="k">while</span> <span class="nv">IFS</span><span class="o">=</span> <span class="nb">read</span> -r -d <span class="s1">&#39;&#39;</span> f<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="nv">title</span><span class="o">=</span><span class="k">$(</span>awk -F<span class="s1">&#39;: &#39;</span> <span class="s1">&#39;/^title:[[:space:]]*/{sub(/^title:[[:space:]]*/,&#34;&#34;); gsub(/^&#34;|&#34;$/, &#34;&#34;, $0); print; exit}&#39;</span> <span class="s2">&#34;</span><span class="nv">$f</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">    <span class="nb">printf</span> <span class="s1">&#39;&gt;&gt;&gt; %s\n&#39;</span> <span class="s2">&#34;</span><span class="nv">$f</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="o">[</span> -n <span class="s2">&#34;</span><span class="nv">$title</span><span class="s2">&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nb">printf</span> <span class="s1">&#39;   title: %s\n&#39;</span> <span class="s2">&#34;</span><span class="nv">$title</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    awk <span class="s1">&#39;BEGIN{w=0;l=0} {l++; w+=NF} END{printf &#34;   words: %d   lines: %d\n&#34;, w, l}&#39;</span> <span class="s2">&#34;</span><span class="nv">$f</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nv">count</span><span class="o">=</span><span class="k">$((</span>count+1<span class="k">))</span>
</span></span><span class="line"><span class="cl">  <span class="k">done</span> &lt; &lt;<span class="o">(</span>find ./content/posts -type f -name index.md -print0<span class="o">)</span>
</span></span><span class="line"><span class="cl">  <span class="nb">printf</span> <span class="s1">&#39;Total articles: %d\n&#39;</span> <span class="s2">&#34;</span><span class="nv">$count</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><p>This alias prints out each article with it&rsquo;s front matter and makes for a handy overview of your site. I know this will scale badly so I&rsquo;ll come up with a better function-based report to replace it eventually. It&rsquo;s useful as is though.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">article_fm</span><span class="o">=</span><span class="s1">&#39;find . -type f -name &#34;index.md&#34; -print0 | \
</span></span></span><span class="line"><span class="cl"><span class="s1">  xargs -0 -n1 sh -c &#39;</span><span class="se">\&#39;</span><span class="s1">&#39;echo &#34;&gt;&gt;&gt; $1&#34;; awk &#34;/^---/{if (inblock){inblock=0; print \&#34;----------------\&#34;; exit} else {inblock=1; next}} inblock&#34; &#34;$1&#34;; echo&#39;</span><span class="se">\&#39;</span><span class="s1">&#39; sh&#39;</span>
</span></span></code></pre></div><hr>
<h2 id="links--references">Links &amp; References</h2>
<h3 id="html--css-references">HTML &amp; CSS References</h3>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference">MDN HTML Reference</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference">MDN CSS Reference</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Guides">MDN HTML Guides</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics">MDN CSS Styling Basics</a></li>
<li><a href="https://www.reddit.com/r/learnprogramming/comments/gvmw5j/is_mdn_enough_for_high_quality_htmlcss/">Discussion: Is MDN enough for high-quality HTML/CSS? (Reddit)</a></li>
</ul>
<h3 id="hugo-resources">Hugo Resources</h3>
<ul>
<li><a href="https://gohugo.io/">Hugo Official Site</a></li>
<li><a href="https://github.com/adityatelange/hugo-PaperMod">PaperMod theme</a></li>
<li><a href="https://www.freecodecamp.org/news/your-first-hugo-blog-a-practical-guide/">Your First Hugo Blog – FreeCodeCamp</a></li>
<li><a href="https://www.pakstech.com/series/blog-with-hugo/">Blog With Hugo (Series) – PäksTech</a></li>
<li><a href="https://www.reddit.com/r/gohugo/comments/1kfwo55/best_blog_sites_youve_seen_built_with_hugo/">Best Blog Sites Built with Hugo (Reddit thread)</a></li>
<li>Example Hugo blogs:
<ul>
<li><a href="https://matteocervelli.com">matteocervelli.com</a></li>
<li><a href="https://icloudnative.io">icloudnative.io</a></li>
<li><a href="https://radu.link/posts/">radu.link</a></li>
</ul>
</li>
</ul>
<hr>
<h2 id="conclusion">Conclusion</h2>
<p>Hugo doesn’t feel like a CMS. It feels like a tool that gets out of the way once you wire it into your workflow. With a couple of scripts and sanity checks, publishing feels less like blogging and more like running <code>make deploy &amp;&amp; go get coffee</code>.</p>
<p style="text-align:left;">
  <a href="/tags/hugo" class="button">🧾 View Other Hugo Posts</a>
</p>
<p>Have a favorite Hugo trick? Or just a good spellchecker for VS Code?<br>
📬 <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a> — always happy to swap notes.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Metaclean</title>
      <link>https://adminjitsu.com/posts/metaclean/</link>
      <pubDate>Wed, 13 Aug 2025 06:01:26 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/metaclean/</guid>
      <description>Metaclean is a tiny, pipe-friendly CLI tool for scanning and removing image metadata with safe defaults. Perfect for preparing photos for blogs, social media, or public archives without leaking location or camera metadata.</description>
      <content:encoded><![CDATA[<h2 id="introduction">Introduction</h2>
<p>Every photo you take contains more than meets the eye.<br>
<br>
Hidden inside are <strong>EXIF metadata</strong> tags — GPS coordinates, camera serial numbers, timestamps, copyright notices, and even software info. While useful for photographers, this data can leak <em>way</em> more about you than you’d expect when you post a picture online.</p>
<p>So I wanted a simple tool that I could use on my own photos. My old process was a mess — a half-remembered crib sheet, three separate commands, and too much context-switching. I hated it. So I built something better. A robust, flexible little CLI tool for managing image metadata. I find it useful enough that I keep using it and polishing it.</p>
<br>
<h3 id="my-best-elevator-pitch"><em>My best elevator pitch:</em></h3>
<p><strong>Metaclean</strong> is a small, pipe-friendly CLI tool that <strong>scans images for metadata</strong> and <strong>safely strips it away</strong>, with the right defaults so you won’t accidentally destroy your originals.</p>
<p>Perfect for:</p>
<ul>
<li>Preparing photos for blog posts or public sharing</li>
<li>Stripping GPS coordinates from family photos</li>
<li>Cleaning a folder before archiving or handing off to clients</li>
</ul>
<br>
<p class="github-btn">
  <a href="https://github.com/forfaxx/metaclean" target="_blank">
    🔗 View metaclean on GitHub
  </a>
</p>
<hr>
<h2 id="quickstart">Quickstart</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Scan for metadata</span>
</span></span><span class="line"><span class="cl">metaclean --scan photo.jpg
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Strip metadata (safe copy in ~/Pictures/cleaned/)</span>
</span></span><span class="line"><span class="cl">metaclean --strip photo.jpg
</span></span></code></pre></div><p>Works with <strong>JPG, PNG, WebP</strong> and <strong>TIFF</strong></p>
<h2 id="features">Features</h2>
<ul>
<li>Safe by default: writes copies with <code>_clean</code> added to the filename unless <code>--inplace</code> is specified</li>
<li>Scan EXIF metadata including GPS, camera details, and copyright information</li>
<li>Strip all metadata by default, rebuilding clean EXIF from scratch</li>
<li>Add copyright tags with <code>--copyright</code></li>
<li>Preserve specific tags with <code>--keep-date</code>, <code>--keep-orientation</code>, <code>--keep-icc</code>, <code>--keep-dpi</code></li>
<li>Pipe-friendly: works with <code>find</code>, <code>xargs</code>, <code>fd</code>, and similar tools</li>
</ul>
<hr>
<h2 id="usage">Usage</h2>
<p><strong>metaclean</strong> <code>(--scan | --strip)</code> [OPTIONS] [FILES&hellip;]</p>
<p><strong>Options:</strong></p>
<ul>
<li><code>--scan</code> — Scan and report metadata.</li>
<li><code>--strip</code> — Strip metadata (writes clean copies unless <code>--inplace</code>).</li>
<li><code>--positives</code> — Only show files that contain metadata (scan mode).</li>
<li><code>--show-gps</code> — Expand GPS info when scanning.</li>
<li><code>--inplace</code> — Overwrite originals (safe atomic replace).</li>
<li><code>--outdir DIR</code> — Directory for cleaned files (default: <code>~/Pictures/cleaned</code>).</li>
<li><code>--copyright TEXT</code> — Add a copyright tag.</li>
<li><code>--keep-date</code> — Preserve <code>DateTimeOriginal</code> tag.</li>
<li><code>--keep-orientation</code> — Preserve orientation tag (pixels still corrected if not kept).</li>
<li><code>--keep-icc</code> — Preserve ICC profile.</li>
<li><code>--keep-dpi</code> — Preserve DPI.</li>
<li><code>--force</code> — Process first frame of animated images (otherwise skipped).</li>
<li><code>--quality N</code> — JPEG quality (default: <code>95</code>).</li>
<li><code>--progressive 0|1</code> — Force progressive JPEG encoding on (<code>1</code>) or off (<code>0</code>).</li>
</ul>
<br>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Scan a single image for metadata</span>
</span></span><span class="line"><span class="cl">metaclean --scan photo.jpg
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Scan only images that have metadata (quiet mode)</span>
</span></span><span class="line"><span class="cl">metaclean --scan --positives ~/Pictures/*.jpg
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Strip metadata from a single file (safe copy in ~/Pictures/cleaned/)</span>
</span></span><span class="line"><span class="cl">metaclean --strip photo.jpg
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Strip metadata and overwrite originals (use with care)</span>
</span></span><span class="line"><span class="cl">metaclean --strip --inplace *.jpg
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Strip metadata but keep the original date and orientation tags</span>
</span></span><span class="line"><span class="cl">metaclean --strip --keep-date --keep-orientation *.jpg
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Add a copyright tag to your images</span>
</span></span><span class="line"><span class="cl">metaclean --strip --copyright <span class="s2">&#34;© forfaxx&#34;</span> ~/Photos/*.jpg
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Find all JPGs with metadata, then strip them in-place</span>
</span></span><span class="line"><span class="cl">find ~/Pictures -name <span class="s1">&#39;*.jpg&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  <span class="p">|</span> metaclean --scan --positives <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  <span class="p">|</span> metaclean --strip --inplace
</span></span></code></pre></div><h2 id="example-output">Example Output</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">forfaxx@shinobi <span class="o">]</span>:/mnt/d/sort
</span></span><span class="line"><span class="cl">$ metaclean --scan misc040.jpg
</span></span><span class="line"><span class="cl"><span class="o">===</span> Metadata <span class="k">for</span> misc040.jpg <span class="o">===</span>
</span></span><span class="line"><span class="cl">ResolutionUnit: <span class="m">2</span>
</span></span><span class="line"><span class="cl">ExifOffset: <span class="m">196</span>
</span></span><span class="line"><span class="cl">Make: Canon
</span></span><span class="line"><span class="cl">Model: Canon PowerShot G3
</span></span><span class="line"><span class="cl">Orientation: <span class="m">1</span>
</span></span><span class="line"><span class="cl">DateTime: 2005:11:02 19:05:35
</span></span><span class="line"><span class="cl">YCbCrPositioning: <span class="m">1</span>
</span></span><span class="line"><span class="cl">XResolution: 180.0
</span></span><span class="line"><span class="cl">YResolution: 180.0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">forfaxx@shinobi <span class="o">]</span>:/mnt/d/sort
</span></span><span class="line"><span class="cl">$ metaclean --strip --inplace misc040.jpg
</span></span><span class="line"><span class="cl"><span class="o">[</span>OK<span class="o">]</span> Stripped metadata IN PLACE → misc040.jpg
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">forfaxx@shinobi <span class="o">]</span>:/mnt/d/sort
</span></span><span class="line"><span class="cl">$ metaclean --scan misc040.jpg
</span></span><span class="line"><span class="cl"><span class="o">[</span>INFO<span class="o">]</span> No EXIF metadata found in misc040.jpg
</span></span></code></pre></div><br> 
<figure class="shadowed" style="margin: 1em auto; text-align: center;">
  <img src="misc040-exif.jpg"
       alt="Original image with EXIF metadata present"
       loading="lazy"
       style="display:block; margin:0 auto; max-width:780px; width:70%;">
  <figcaption style="font-size:85%; font-weight:normal; color:#666; line-height:1.4; margin-top:0.4em;">
    <strong>Before:</strong> <code>misc040-exif.jpg</code> — original file with EXIF (camera model, timestamp, etc.).
  </figcaption>
</figure>
<br>
<figure class="shadowed" style="margin: 1em auto; text-align: center;">
  <img src="misc040.jpg"
       alt="Cleaned image with EXIF metadata removed"
       loading="lazy"
       style="display:block; margin:0 auto; max-width:780px; width:70%;">
  <figcaption style="font-size:85%; font-weight:normal; color:#666; line-height:1.4; margin-top:0.4em;">
    <strong>After:</strong> <code>misc040.jpg</code> — cleaned by <code>metaclean</code> (no EXIF; pixels preserved).
  </figcaption>
</figure>
<br>
<figure class="shadowed" style="margin: 1em auto; text-align: center;">
  <img src="EXIF.png"
       alt="ThumbsPlus side by side metadata view"
       loading="lazy"
       style="display:block; margin:0 auto; max-width:780px; width:70%;">
  <figcaption style="font-size:85%; font-weight:normal; color:#666; line-height:1.4; margin-top:0.4em;">
    <strong>Before</strong> and <strong>After</strong> view in ThumbsPlus
  </figcaption>
</figure>
<br>
<h2 id="using-metaclean-in-a-pipeline-or-script">Using <code>metaclean</code> in a pipeline or script</h2>
<p><code>metaclean</code> is designed to work well in pipelines and scripts. A few examples:</p>
<h3 id="show-me-files-with-metadata-dry-run">Show me files with metadata (dry-run)</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Scan repo images and list only files that contain metadata</span>
</span></span><span class="line"><span class="cl">git ls-files -z -- <span class="s1">&#39;*.jpg&#39;</span> <span class="s1">&#39;*.jpeg&#39;</span> <span class="s1">&#39;*.png&#39;</span> <span class="s1">&#39;*.webp&#39;</span> <span class="s1">&#39;*.tif&#39;</span> <span class="s1">&#39;*.tiff&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span><span class="p">|</span> xargs -0 metaclean --scan --positives <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span><span class="p">|</span> tee /tmp/metaclean_positives.txt
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Files with metadata: </span><span class="k">$(</span>wc -l &lt; /tmp/metaclean_positives.txt<span class="k">)</span><span class="s2">&#34;</span>
</span></span></code></pre></div><h3 id="in-place-strip-of-only-whats-staged-for-commit-pre-commit-friendly">In-place strip of only what&rsquo;s staged for commit (pre-commit friendly)</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Safe atomic replace (your script’s default behavior) on staged images</span>
</span></span><span class="line"><span class="cl">git diff --cached --name-only -z -- <span class="s1">&#39;*.jpg&#39;</span> <span class="s1">&#39;*.jpeg&#39;</span> <span class="s1">&#39;*.png&#39;</span> <span class="s1">&#39;*.webp&#39;</span> <span class="s1">&#39;*.tif&#39;</span> <span class="s1">&#39;*.tiff&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span><span class="p">|</span> xargs -0 metaclean --strip --inplace --keep-icc --keep-dpi
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Re-stage files that changed (helps if hook runs before commit)</span>
</span></span><span class="line"><span class="cl">git add --update
</span></span></code></pre></div><h3 id="one-liner-to-strip-everything-under-current-directory-into-picturescleaned">One-liner to strip everything under current directory into ~/Pictures/cleaned</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">find . -type f <span class="se">\(</span> -iname <span class="s1">&#39;*.jpg&#39;</span> -o -iname <span class="s1">&#39;*.jpeg&#39;</span> -o -iname <span class="s1">&#39;*.png&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>                 -o -iname <span class="s1">&#39;*.webp&#39;</span> -o -iname <span class="s1">&#39;*.tif&#39;</span> -o -iname <span class="s1">&#39;*.tiff&#39;</span> <span class="se">\)</span> -print0 <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span><span class="p">|</span> xargs -0 metaclean --strip --outdir <span class="s2">&#34;</span><span class="si">${</span><span class="nv">HOME</span><span class="si">}</span><span class="s2">/Pictures/cleaned&#34;</span>
</span></span></code></pre></div><ol start="4">
<li>Makefile target for &ldquo;pre-publish:clean-images&rdquo;</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-make" data-lang="make"><span class="line"><span class="cl"><span class="c"># Makefile
</span></span></span><span class="line"><span class="cl"><span class="c"></span><span class="nv">IMGS</span> <span class="o">:=</span> <span class="k">$(</span>shell git ls-files -- <span class="s1">&#39;*.jpg&#39;</span> <span class="s1">&#39;*.jpeg&#39;</span> <span class="s1">&#39;*.png&#39;</span> <span class="s1">&#39;*.webp&#39;</span> <span class="s1">&#39;*.tif&#39;</span> <span class="s1">&#39;*.tiff&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">OUT</span>  <span class="o">:=</span> static/img-clean
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">.PHONY</span><span class="o">:</span> <span class="n">images</span>-<span class="n">clean</span>
</span></span><span class="line"><span class="cl"><span class="nf">images-clean</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">	@mkdir -p <span class="k">$(</span>OUT<span class="k">)</span>
</span></span><span class="line"><span class="cl">	@echo <span class="s2">&#34;→ Scanning and cleaning images into </span><span class="k">$(</span>OUT<span class="k">)</span><span class="s2">…&#34;</span>
</span></span><span class="line"><span class="cl">	@printf <span class="s1">&#39;%s\0&#39;</span> <span class="k">$(</span>IMGS<span class="k">)</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>	<span class="p">|</span> xargs -0 metaclean --scan --positives <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>	<span class="p">|</span> metaclean --strip --keep-icc --keep-dpi --outdir <span class="k">$(</span>OUT<span class="k">)</span>
</span></span><span class="line"><span class="cl">	@echo <span class="s2">&#34;✓ Done.&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"># Optional: fold into your publish chain
</span></span></span><span class="line"><span class="cl"><span class="c"></span><span class="nf">publish</span><span class="o">:</span> <span class="n">images</span>-<span class="n">clean</span>
</span></span><span class="line"><span class="cl">	./sync-adminjitsu.sh
</span></span></code></pre></div><h2 id="links-and-stuff">Links and Stuff</h2>
<ul>
<li><a href="https://en.wikipedia.org/wiki/Metadata">https://en.wikipedia.org/wiki/Metadata</a></li>
<li><a href="https://en.wikipedia.org/wiki/Exif">https://en.wikipedia.org/wiki/Exif</a></li>
<li><a href="https://www.canon-europe.com/pro/infobank/all-about-exif/">https://www.canon-europe.com/pro/infobank/all-about-exif/</a></li>
<li><a href="https://dev.exiv2.org/projects/exiv2/wiki/The_Metadata_in_PNG_files">https://dev.exiv2.org/projects/exiv2/wiki/The_Metadata_in_PNG_files</a></li>
<li><a href="https://pillow.readthedocs.io/en/stable/">https://pillow.readthedocs.io/en/stable/</a></li>
</ul>
<br>
<h2 id="conclusion">Conclusion</h2>
<p>Whether you&rsquo;re cleaning up a single selfie or prepping an archive for public release, <code>metaclean</code> has your back.</p>
<p>Have a neat pipeline or trick?</p>
<p>PRs and issues welcome! Or email me:
<a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Upgrade Your Junk Drawer</title>
      <link>https://adminjitsu.com/posts/upgrade-your-junk-drawer/</link>
      <pubDate>Tue, 12 Aug 2025 14:53:33 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/upgrade-your-junk-drawer/</guid>
      <description>How I built a pair of USB drives — one packed with rescue ISOs and portable apps using YUMI, the other loaded with offline Wikipedia, technical manuals, and AI tools via Kiwix — to keep critical information and utilities at hand no matter what.</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<p>The Texas Winter Storm of 2021 — Winter Storm Uri — was a wake-up call.
Store shelves went bare, gas stations closed, and widespread power outages even knocked cell towers offline. It was a crash course in just how fragile our everyday systems are.</p>
<p>Since then, I’ve made sure to keep a reasonable supply of water, food, and sanitation products on hand. You don’t have to be a “prepper” to see the value in having a couple of weeks’ worth of essentials ready for when things go sideways.</p>
<p>One day, while reorganizing my so-called junk drawer, I realized there was another kind of preparedness I’d been overlooking: information. What happens when you lose internet access and the cloud goes out of reach? Whether a short outage or Planet of the Apes time, it&rsquo;s incredibly useful to have these tools waiting for the day they might be needed.</p>
<p>That thought experiment turned into a small, practical project: a pair of USB drives that would give me both digital rescue tools and a personal offline internet.</p>
<p>These are the two upgrades I made to my junk drawer:</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="junk-drawer.jpg" alt="a junk drawer" style="display:block; margin:0 auto; max-width:500px;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <em>Upgrade your junk drawer with these fantastic thumb drives</em><br>
  </figcaption>
</figure>
<h2 id="yumi-tools-disk">YUMI tools disk</h2>
<p>A multiboot USB drive, built with <a href="https://www.pendrivelinux.com/yumi-multiboot-usb-creator/">YUMI</a>, packed with rescue ISOs, OS installers, and diagnostic tools. It’s the Swiss Army knife of boot media.</p>
<p>YUMI (Your Universal Multiboot Installer) is a free utility that turns an ordinary USB stick into a multi-boot Swiss Army knife.
Instead of carrying a dozen separate thumb drives for each operating system or rescue tool, YUMI lets you store multiple ISOs on a single device — with a friendly boot menu to pick the one you need. Luckily the process is simple and YUMI has links to a large list of isos to include. Adding a custom one is not hard at all!</p>
<p>It supports a huge range of images, from Linux live distros and Windows installers to antivirus rescue environments, memory testers, and disk cloning tools. You can add, remove, or update ISOs without wiping the entire drive, which makes it perfect for keeping your toolkit fresh.</p>
<p>I built mine as a go-to rescue and setup drive, loaded with:</p>
<ul>
<li>Windows 10 and Windows 11 Installers / repair disks</li>
<li>MemTest</li>
<li>Boot Repair Disk</li>
<li>Ultimate Boot CD</li>
<li>GParted</li>
<li>Finnix</li>
<li>Kali Linux</li>
<li>Ubuntu Desktop</li>
<li>Antivirus Live</li>
<li>Kodachi Anonymous browsing</li>
<li>Spinrite</li>
<li>and more!</li>
</ul>
<p>Each of these images live on the disk as an ISO with a single multi-boot loader. The rest of the disk is available as storage so I also loaded it up with a collection of Computer and Networking cheatsheets and pdfs. I tried to cover topics from pc hardware to networking to Operating System specific references and more.</p>
<p>The last piece of the puzzle is <a href="https://portableapps.com/">PortableApps</a>. This is an easy to set up collection of windows applications that do no require installation. With hundreds of packages including standards like VLC, Chromium, Firefox, and more, it provides a portable toolkit that come in handy if you find yourself on a guest computer or are without Internet and have things to fix.</p>
<p>I&rsquo;ve only reached for my YUMI boot drive a couple of times since I built it but it&rsquo;s a huge relief knowing it&rsquo;s there. It&rsquo;s like knowing your spare tire is aired up and in good shape. The peace of mind is priceless.</p>
<h2 id="the-hardware">The Hardware</h2>
<p>I’ve made a few of these for friends using 128 GB and 256 GB drives, and they work great — but I got hooked on building the <em>ultimate</em> boot drive.<br>
The one I recommend is the <strong>SSK 512 GB</strong> or <strong>1 TB</strong> thumb drive.<br>
As of publication, the <strong>512 GB</strong> is about <span>$50</span> and the <strong>1 TB</strong> about <span>$77</span>.<br>
It’s pleasingly heavy, solid metal, and has been rock-solid in my testing. <a href="https://www.amazon.com/SSK-External-Storage-Android-Windows/dp/B0F9KKZMSJ?th=1">Amazon link</a>.<br>
If you’re on a budget, 128 GB or 256 GB will still get the job done — but I wouldn’t go smaller.</p>
<br>
<h2 id="offline-internet">Offline Internet</h2>
<p>If the YUMI drive is my digital multitool, this is my <strong>pocket library</strong> — a self-contained slice of the web that doesn’t care if every ISP from here to Guam goes belly-up.</p>
<p>It’s built around <a href="https://kiwix.org/en/">Kiwix</a> — free, open-source software that reads <code>.zim</code> archives (think: an entire website, squished into a single file). With the right ZIMs, you can browse huge swaths of the internet without ever going online.</p>
<p>Kiwix’s <a href="https://library.kiwix.org/#lang=eng">Library</a> offers hundreds of site archives which you can preview live without downloading. Some are tiny but packed with value; others are massive, like the English Wikipedia — over <strong>6 million</strong> pages with images and links. You’ll also find medical and first aid guides, language resources, practical skills manuals, and technical collections like StackExchange.</p>
<p>Can’t find the site you want? You can make your own ZIM. It can be a bit of a black art depending on complexity, but I’ve had success creating more than ten — including <a href="https://foragingtexas.com/">foragingtexas</a> and <a href="https://textfiles.com/">textfiles.com</a>.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="kiwix.jpg" alt="kiwix wikipedia screenshot" style="display:block; margin:0 auto; max-width:500px;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <em>Kiwix is like a normal browser for the offline .Zim website archives</em><br>
  </figcaption>
</figure>
<ul>
<li><strong>Wikipedia (English with images)</strong> — ~110 GB</li>
<li><strong>Wikivoyage</strong> — offline travel guide, ~1 GB</li>
<li><strong>Stack Exchange dump</strong> — the collective Stack brain, ~50 GB</li>
<li><strong>Project Gutenberg</strong> — 60 GB of public-domain books</li>
<li><strong>Medical &amp; first aid references</strong> — WHO, CDC, wilderness medicine PDFs</li>
<li><strong>Military Field Manuals</strong> - on a huge array of topics from carpentry to earth moving to emergency medicine.</li>
<li><strong>Much More</strong> - over 80 zim archives comfortably fit in a 512gb drive. You can really splurge with a Terabyte or more</li>
</ul>
<p>Kiwix runs on Windows, macOS, Linux, even Android. You can just read locally, or flip the switch on its tiny built-in server so other devices on your LAN/Wi-Fi can browse your stash like it’s the real thing.</p>
<h3 id="-supporting-files">📚 Supporting files</h3>
<p>The Kiwix browser and offline ZIM archives are fantastic, but they still leave some gaps. To fill them, I built a serious directory of practical ebooks covering a huge range of topics. The <a href="https://archive.org/">Internet Archive</a> was my main hunting ground, along with countless niche sites. After some determined searching, I pulled together PDFs on everything from power and water systems, primitive tools, and Amish gardening, to hunting, fishing, automotive repair, welding (for those A-Team–style zombie survival vehicles), woodworking, psychology, negotiation, leadership, electronics, cooking, preserving, law, medicine, and even rainy-day activities for kids and adults.</p>
<p>Building that collection was a blast — I tried to anticipate as many scenarios as I could imagine needing the drive for. Highlights include:</p>
<ul>
<li><strong>PDF manuals</strong> — Hundreds of guides, how-tos, and reference books across dozens of disciplines.</li>
<li><strong>AI-generated cheatsheets</strong> — Troubleshooters, walkthroughs, and quick-reference sheets for emergencies and complex topics.</li>
<li><strong>Infographics &amp; diagrams</strong> — Wiring charts, ham radio band maps, plant identification guides, and other dense visual references.</li>
<li><strong>Offline AI models</strong> — Compact (4–8 GB) quantized LLaMA/Qwen builds you can run locally for Q&amp;A without an internet connection. Limited compared to commercial models, but surprisingly capable.</li>
<li><strong>Portable apps</strong> — Readers, converters, scanners, accessibility tools (magnifiers, color pickers), and basic media editors — all ready to run without downloads.</li>
<li><strong>My notes</strong> — Not the latest version, but packed with network configs, machine specs, serial numbers, cheatsheets, and setup worklogs.</li>
</ul>
<p>It’s not the whole internet — but it’s <em>my</em> internet, curated for when the big one hits and the only cloud left is the one making rain (or fallout).</p>
<h2 id="thoughts-on-preparing-for-the-worst">Thoughts on Preparing for the Worst</h2>
<figure style="float:left; margin:0 1rem 1rem 0; width:clamp(260px, 45%, 550px);">
  <img src="burgess_meredith_twilight_zone_time_enough_at_last.jpg" alt="Burgess Meredith as Henry Bemis in The Twilight Zone episode 'Time Enough at Last'" style="display:block; width:100%; height:auto;">
  <figcaption style="font-size:85%; color:#666; line-height:1.4; margin-top:0.4em;">
    <em>Burgess Meredith in the classic Twilight Zone episode, "Time Enough at Last"</em><br>
    &copy; CBS Studios Inc. All rights reserved.
  </figcaption>
</figure>
<p>Making these drives was a blast. Beyond building tools for a rainy day, it became a grand thought experiment:<br>
<em>In what situations might I need these drives?</em></p>
<p>The YUMI multi-boot disk was easy — we’ve all borked a server config or locked ourselves out in a dozen creative ways. I just built the tools and gathered the technical resources I’ve actually needed before. That mix of imagination and past experience was addictive.</p>
<p>The real challenge (and fun) was the Offline Internet project. I spent weeks — months, really — downloading ZIM archives and putting them through their paces. I curated e-books from my own shelves and the <a href="https://archive.org">Internet Archive</a>, shaping them into a dataset that’s actually usable: hundreds of practical titles across countless fields.</p>
<p>Then came the “what if” game:</p>
<ul>
<li><strong>Boring road trip + dead stereo</strong> → puzzle books, coloring books, trivia, road trip games.</li>
<li><strong>Vault Dweller rebuilding society</strong> → construction, woodworking, welding, engineering manuals.</li>
<li><strong>College student mid-paper, Wi-Fi down</strong> → research material that just might save the semester.</li>
</ul>
<p>The result is a beautifully curated collection — resources for woodworking, sewing, gardening, medicine, electronics, philosophy, ham radio, camping, fishing, and more — all right at my fingertips.</p>
<div style="clear:both;"></div>
<br>
<h2 id="using-the-drives">Using the drives</h2>
<p>I tested these thoroughly while building them, but most of the time they live in an Altoids tin in my desk drawer. The exception is the Offline Internet project, which I also keep on a live hard drive on my network — I use the practical ebook library often. Even with an active internet connection, it’s nice having that kind of information instantly at hand. It’s helped me research everything from repair jobs to obscure technical questions.</p>
<p>You <em>can</em> update the content, but ZIM archive dumps are infrequent. You’ll typically be a year or so behind on major sites, but with so much evergreen material, it hardly matters. Most of the collection remains relevant for decades.</p>
<p>The real value shows when the power or internet go down — whether for an hour or a week. With my MacBook Pro, a decent power tank, a solar charger, a car, and a generator, I can keep my devices running. Add an RTL-SDR radio dongle and antenna kit for picking up broadcasts, my phone’s hotspot, and the Offline Internet drive, and there’s very little I can’t research.</p>
<br>
<h2 id="-lm-studio">💡 LM Studio</h2>
<p>One of the sparks for this whole project came from an ad I saw for a “prepper Raspberry Pi” — a little box that served offline websites and reference material. It was clever, but it made me think: <em>why stop there?</em> I wanted something bigger, faster, and more flexible — not just static pages, but a system that could help me search, summarize, and connect information.</p>
<p>That’s where <a href="https://lmstudio.ai/">LM Studio</a> comes in. Running entirely offline, it lets you load a large language model (LLM) and query it against your own data — ZIM archives, PDFs, notes, you name it. Suddenly, the Offline Internet project wasn’t just a filing cabinet; it became an interactive reference librarian. I named mine &lsquo;Sprocket&rsquo;</p>
<p>Pairing Kiwix ZIM archives with a practical ebook library and an LLM turns the drive into a research machine:</p>
<ul>
<li><strong>ZIM archives</strong> give you complete websites like Wikipedia, StackExchange, and medical references.</li>
<li><strong>PDF library</strong> covers niche and technical topics that rarely exist as ZIMs.</li>
<li><strong>LLM in LM Studio</strong> helps you find answers, summarize long documents, and connect ideas — all without the cloud.</li>
</ul>
<p>The result is a tool that works as well in a power outage as it does on a quiet afternoon in your workshop. It’s not just storage — it’s knowledge you can actually use.</p>
<br>
<hr>
<h3 id="-setting-up-lm-studio-for-offline-ai">🧠 Setting up LM Studio for Offline AI</h3>
<p><a href="https://lmstudio.ai/">LM Studio</a> is a free desktop app for running large language models (LLMs) entirely offline. It’s perfect for querying your ebook library or ZIM archives without sending a single byte to the cloud.</p>
<p><strong>Getting started:</strong></p>
<ol>
<li><strong>Download &amp; Install</strong> — Grab the latest release for macOS, Windows, or Linux from <a href="https://lmstudio.ai/">lmstudio.ai</a>.</li>
<li><strong>Pick a Model</strong> — In the “Discover” tab, search for one of these recommended models (choose a <strong>GGUF</strong> build with <code>q4_K_M</code> quantization for best speed/accuracy on laptops):
<ul>
<li><a href="https://huggingface.co/Qwen/Qwen2-7B-Instruct-GGUF">Qwen 2 7B Instruct — Hugging Face</a> <em>(excellent general reasoning, great at summarizing technical docs)</em></li>
<li><a href="https://huggingface.co/TheBloke/Llama-3-8B-Instruct-GGUF">LLaMA 3 8B Instruct — Hugging Face</a> <em>(newest Meta model, very coherent responses)</em></li>
<li><a href="https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.3-GGUF">Mistral 7B Instruct v0.3 — Hugging Face</a> <em>(fast and light, great for Q&amp;A tasks)</em></li>
</ul>
</li>
<li><strong>Download</strong> — LM Studio will pull the model directly from Hugging Face and store it locally.</li>
<li><strong>Load &amp; Chat</strong> — Click “Load Model,” then open a chat tab to start asking questions.</li>
<li><strong>Increase Context Window</strong> (optional) — If your laptop has enough RAM, set the context size to 4096–8192 tokens in model settings for better long-document performance.</li>
</ol>
<p><strong>Pro tip:</strong><br>
Enable the <strong>Local Docs</strong> plugin in LM Studio to add your ebook library folder. Once indexed, you can ask:</p>
<blockquote>
<p>“Summarize the welding section in <em>Military Field Manual 3-34</em>”<br>
and get an answer without opening the PDF.</p></blockquote>
<p>LM Studio stores everything locally — no tracking, no API calls, just a fast, private assistant that works even when the grid doesn’t.</p>
<br>
<h2 id="boot-from-usb">Boot from USB</h2>
<p>Be sure to check your BIOS/UEFI for the correct procedure to boot from a usb drive. Here are a few common ones:</p>
<h4 id="-pc-boot-menu--usb-boot-hotkeys">🖥️ PC Boot Menu / USB Boot Hotkeys</h4>
<table>
  <thead>
      <tr>
          <th>Brand / Board Maker</th>
          <th>Boot Menu Key</th>
          <th>BIOS/UEFI Setup Key</th>
          <th>Notes</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Acer</strong></td>
          <td>F12</td>
          <td>Del or F2</td>
          <td>Boot menu often disabled by default — enable in BIOS first.</td>
      </tr>
      <tr>
          <td><strong>ASUS</strong></td>
          <td>Esc or F8</td>
          <td>Del or F2</td>
          <td>Newer boards often Esc for boot menu.</td>
      </tr>
      <tr>
          <td><strong>Dell</strong></td>
          <td>F12</td>
          <td>F2</td>
          <td>Latitude/OptiPlex/Precision lines are consistent.</td>
      </tr>
      <tr>
          <td><strong>HP / Compaq</strong></td>
          <td>Esc, then F9</td>
          <td>Esc, then F10</td>
          <td>Esc opens startup menu, then select option.</td>
      </tr>
      <tr>
          <td><strong>Lenovo (ThinkPad)</strong></td>
          <td>F12</td>
          <td>F1</td>
          <td>Some older ThinkPads use F12 for boot, F1 for BIOS.</td>
      </tr>
      <tr>
          <td><strong>Lenovo (IdeaPad)</strong></td>
          <td>F12 or Novo button</td>
          <td>F2</td>
          <td>Novo is a tiny recessed button near power.</td>
      </tr>
      <tr>
          <td><strong>MSI</strong></td>
          <td>F11</td>
          <td>Del</td>
          <td>Consistent across desktop boards and gaming laptops.</td>
      </tr>
      <tr>
          <td><strong>Gigabyte</strong></td>
          <td>F12</td>
          <td>Del</td>
          <td>Common on Aorus/Z-series boards.</td>
      </tr>
      <tr>
          <td><strong>Biostar</strong></td>
          <td>F9</td>
          <td>Del</td>
          <td></td>
      </tr>
      <tr>
          <td><strong>Toshiba</strong></td>
          <td>F12</td>
          <td>Esc or F1</td>
          <td>Satellite series often Esc to setup.</td>
      </tr>
      <tr>
          <td><strong>Sony VAIO</strong></td>
          <td>F11</td>
          <td>F2</td>
          <td>Some models require Assist button when off.</td>
      </tr>
      <tr>
          <td><strong>Samsung</strong></td>
          <td>Esc or F12</td>
          <td>F2</td>
          <td></td>
      </tr>
      <tr>
          <td><strong>Intel NUC</strong></td>
          <td>F10</td>
          <td>F2</td>
          <td>NUCs are consistent.</td>
      </tr>
      <tr>
          <td><strong>Packard Bell</strong></td>
          <td>F8</td>
          <td>F1 or Del</td>
          <td></td>
      </tr>
  </tbody>
</table>
<h4 id="-mac-boot-keys">🍏 Mac Boot Keys</h4>
<p>The Mac has a rich set of built in tools for repairing system problems. It can still be good to download and stash a copy of the latest macOS installer or two. You can find instructions here: <a href="https://support.apple.com/en-us/101578">https://support.apple.com/en-us/101578</a></p>
<table>
  <thead>
      <tr>
          <th>Key Combo (hold at startup)</th>
          <th>Function</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Option (⌥)</strong></td>
          <td>Startup Manager — choose boot disk (USB, external, etc.).</td>
      </tr>
      <tr>
          <td><strong>Command (⌘) + R</strong></td>
          <td>macOS Recovery (Internet or local).</td>
      </tr>
      <tr>
          <td><strong>Shift + Option + Command + R</strong></td>
          <td>Internet Recovery for <em>original</em> macOS version shipped with Mac.</td>
      </tr>
      <tr>
          <td><strong>Command (⌘) + Option (⌥) + R</strong></td>
          <td>Internet Recovery for latest compatible macOS.</td>
      </tr>
      <tr>
          <td><strong>T</strong></td>
          <td>Target Disk Mode (Mac acts as external drive).</td>
      </tr>
      <tr>
          <td><strong>D</strong></td>
          <td>Apple Diagnostics.</td>
      </tr>
      <tr>
          <td><strong>Option + D</strong></td>
          <td>Internet Apple Diagnostics.</td>
      </tr>
  </tbody>
</table>
<h2 id="conclusion">Conclusion</h2>
<p>My upgraded junk drawer has duct tape and WD-40 for fixing the things you can touch — and then there are the tools for fixing the rest. A YUMI drive and an Offline Internet library might never leave your junk drawer… but if you ever need them, you’ll be glad they are there.</p>
<p>It would be simple enough to combine these into a single, curated combination YUMI and offline internet with a big enough drive. Kiwix is easy to run as a server which will make your library available to machines on a network. Never let an inconvenient power outage stop you from doing Information Age tasks.</p>
<p><a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Even More Cromulent Words</title>
      <link>https://adminjitsu.com/posts/even-more-cromulent-words/</link>
      <pubDate>Sun, 10 Aug 2025 00:00:00 +0000</pubDate>
      <guid>https://adminjitsu.com/posts/even-more-cromulent-words/</guid>
      <description>From the philosophy of skepticism to the quirks of your neurons, this is another bag of cromulent words worth keeping in your mental toolkit.</description>
      <content:encoded><![CDATA[<h2 id="introduction">Introduction</h2>
<p>In <a href="/posts/cromulent-words/">Cromulent Words</a> and <a href="/posts/more-cromulent-words/">More Cromulent Words</a>, I collected curious, oddly-specific, and strangely useful terms from computing, philosophy, and beyond.<br>
This third entry dives into logic, skepticism, and the scale of the unimaginably large (and small) — with even more cromulent words for your mental toolkit.</p>
<figure class="shadowed" style="max-width:700px; margin:0 auto 1rem;">
  <img src="dr-nick.jpg"
       alt="Irregardless means Regardless? What a language!"
       style="width:100%; height:auto; display:block;">
  <figcaption style="font-size:85%; color:#666; line-height:1.4; margin-top:.4em;">
    <em>Image © Fox Broadcasting / The Simpsons.</em>
  </figcaption>
</figure>
<p style="text-align:center;">
  <a href="/tags/cromulent" class="button">🧾 View Cromulent Words Series</a>
</p>
<hr>
<h2 id="big-small--silly-numbers">Big, Small &amp; Silly Numbers</h2>
<p><em>(From yottabytes to googolplexes, Planck lengths to femtoseconds — the units that stretch your brain from the cosmic to the subatomic.)</em></p>
<p>We&rsquo;ve all seen charts like this one that help us to understand the extremely large (and small) numbers that are everywhere in computer science.</p>
<table>
  <thead>
      <tr>
          <th>Prefix</th>
          <th>Symbol</th>
          <th>Factor</th>
          <th>Power of 10</th>
          <th>Examples</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Femto</strong></td>
          <td>f</td>
          <td>Quadrillionth</td>
          <td>10⁻¹⁵</td>
          <td>Femtosecond: electron hops ~0.3 mm</td>
      </tr>
      <tr>
          <td><strong>Pico</strong></td>
          <td>p</td>
          <td>Trillionth</td>
          <td>10⁻¹²</td>
          <td>Picosecond: light travels 0.3 mm</td>
      </tr>
      <tr>
          <td><strong>Nano</strong></td>
          <td>n</td>
          <td>Billionth</td>
          <td>10⁻⁹</td>
          <td>1 nanometer: DNA’s double helix is ~2 nm wide</td>
      </tr>
      <tr>
          <td><strong>Micro</strong></td>
          <td>μ</td>
          <td>Millionth</td>
          <td>10⁻⁶</td>
          <td>Microsecond: a camera flash cycle</td>
      </tr>
      <tr>
          <td><strong>Milli</strong></td>
          <td>m</td>
          <td>Thousandth</td>
          <td>10⁻³</td>
          <td>Millisecond: hummingbird wingbeat</td>
      </tr>
      <tr>
          <td><strong>Kilo</strong></td>
          <td>k</td>
          <td>Thousand</td>
          <td>10³</td>
          <td>Kilometer: ~0.62 miles</td>
      </tr>
      <tr>
          <td><strong>Mega</strong></td>
          <td>M</td>
          <td>Million</td>
          <td>10⁶</td>
          <td>Megabyte: small image file</td>
      </tr>
      <tr>
          <td><strong>Giga</strong></td>
          <td>G</td>
          <td>Billion</td>
          <td>10⁹</td>
          <td>Gigabyte: HD movie</td>
      </tr>
      <tr>
          <td><strong>Tera</strong></td>
          <td>T</td>
          <td>Trillion</td>
          <td>10¹²</td>
          <td>Teraflop: PlayStation 5’s raw compute power (~10 TF)</td>
      </tr>
      <tr>
          <td><strong>Peta</strong></td>
          <td>P</td>
          <td>Quadrillion</td>
          <td>10¹⁵</td>
          <td>Petabyte: 1,000 large hard drives</td>
      </tr>
      <tr>
          <td><strong>Exa</strong></td>
          <td>E</td>
          <td>Quintillion</td>
          <td>10¹⁸</td>
          <td>Exabyte: big-data center scale</td>
      </tr>
      <tr>
          <td><strong>Zetta</strong></td>
          <td>Z</td>
          <td>Sextillion</td>
          <td>10²¹</td>
          <td>Zettabyte: roughly, all internet data (mid-2020s)</td>
      </tr>
      <tr>
          <td><strong>Yotta</strong></td>
          <td>Y</td>
          <td>Septillion</td>
          <td>10²⁴</td>
          <td>Yottagram: weight of all Earth’s oceans (~1.4 Yg)</td>
      </tr>
  </tbody>
</table>
<p>I recently started collecting <strong>ZIM archives</strong> after an annoying power outage reminded me of the Texas Winter Storm of ’21 — when electricity <em>and</em> cellular data were down for days during an emergency. What began as a “just in case” thought experiment turned into a fun way to repurpose a big thumb drive I already had lying around into a surprisingly powerful offline library.</p>
<p><span class="tag teal">Check out:</span> my <a href="/posts/upgrade-your-junk-drawer/">Upgrade Your Junk Drawer</a> post for the details!</p>
<br>
<p>During my research, I stumbled across the excellent <a href="https://en.wikipedia.org/wiki/Wikipedia:Database_download">Wikipedia:Database download</a> page. It’s one of my favorite write-ups on SI units, file sizes, and filesystem limits ever — unexpectedly funny, deeply nerdy, and endlessly useful.</p>
<p>I’m now tempted to make the <strong>yottabyte</strong> my “default” storage unit.<br>
That 10 TB hard drive on my desk? A staggering <strong>0.00000000001 yottabytes</strong>. At this rate, I’d only need to buy about <strong>100 billion more</strong> 10 TB drives to hit a full yottabyte. Maybe they’ll go on sale.</p>
<br>
<p>Now, if you really want to push your brain past the comfortable limits of prefixes, consider the <strong>Planck length</strong> — about 1.6 × 10⁻³⁵ meters.<br>
It’s so small that if a proton were scaled up to the size of the observable universe, a Planck length would still be smaller than a tree in your backyard.<br>
Physicists often treat it as the smallest <em>meaningful</em> unit of length, below which our current theories of space and time stop making sense.</p>
<p><span class="tag red">Did You Know?</span> The SI prefixes above yotta were extended in 2022:</p>
<ul>
<li><strong>ronna</strong> (10²⁷)</li>
<li><strong>quetta</strong> (10³⁰)</li>
</ul>
<p>In this scale, Earth’s mass is about <strong>6 ronnagrams (Rg)</strong>, while Jupiter weighs in at roughly <strong>1.9 quettagrams (Qg)</strong>—making it over 300 times more massive than Earth.</p>
<p>A <strong>googolplex</strong> is written as: $10^{10^{100}}$</p>
<p>That’s a 1 followed by a googol of zeros (a googol itself being $10^{100}$).<br>
For comparison: the estimated number of particles in the observable universe is “only” about $10^{80}$.</p>
<figure class="shadowed" style="left; max-width:420px; margin:0 1rem 1rem 0;">
  <img src="On_Beyond_Zebra.jpg"
       alt="On Beyond Zebra by Dr. Seuss"
       style="width:100%; height:auto; display:block;">
  <figcaption style="font-size:85%; color:#666; line-height:1.4; margin-top:.4em;">
    <em>Dr. Seuss imagined letters beyond Z — some SI prefixes feel just as fantastical.</em>
  </figcaption>
</figure>
<hr>
<h2 id="computer-science-lore">Computer Science Lore</h2>
<p><em>(From background spirits to stampeding processes, packet-capture mantras to mythical config files — the stories and slang that make up sysadmin folklore)</em></p>
<ul>
<li>
<p><strong>Daemon</strong><br>
In UNIX, a daemon is simply a program that runs quietly in the background.
The name comes from James Clerk Maxwell’s demon in physics — a thought-experiment creature that sorts atoms without expending energy — repurposed to mean “helpful background spirit.”</p>
<br>
<p>To program a <em>true</em> daemon the old-school UNIX way, you follow a ritual of sorts:</p>
<ol>
<li><strong>Fork</strong> – The process calls <code>fork()</code>, creating a child and letting the parent exit. This detaches it from the invoking shell’s process group.</li>
<li><strong>setsid()</strong> – The child starts a new session, becoming the session leader and cutting ties to any controlling terminal.</li>
<li><strong>Fork again</strong> <em>(optional but traditional)</em> – Prevents the daemon from ever re-acquiring a terminal.</li>
<li><strong>Change working directory</strong> – Usually to <code>/</code> so it won’t block filesystem unmounts.</li>
<li><strong>Set umask</strong> – Define default file permissions, often <code>umask(0)</code> so the daemon can set its own.</li>
<li><strong>Redirect file descriptors</strong> – Close <code>stdin</code>, <code>stdout</code>, and <code>stderr</code>, reopening them to <code>/dev/null</code> — or to log files.</li>
</ol>
<figure style="text-align:center; margin: 1em auto;">
  <img src="beastie.png" alt="Beastie the BSD daemon" style="display:block; margin:0 auto; width:400px; max-width:500px;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <em>“The friendly face of BSD — a nod to UNIX background processes.”</em><br>
    Original Beastie art © Phil Foglio / John Lasseter, drawn for BSD manuals in the early 1980s.
  </figcaption>
</figure>
</li>
</ul>
<br>
<ul>
<li>
<p><strong>Hysteresis</strong></p>
<p>When a system’s state depends not just on current inputs, but also on its history — a kind of “memory” effect that prevents rapid flapping between states.</p>
<p>In hardware: a thermostat set to 72°F might turn on heat at 70°F and off at 74°F, avoiding constant toggling.
In ops: a load balancer might only mark a node healthy after three consecutive passing checks, even if the first one passes.</p>
<p>That gap between “on” and “off” is hysteresis — stability’s best friend, but also the reason your thermostat ignores you for five awkward minutes</p>
</li>
<li>
<p><strong>Thundering Herd</strong></p>
<p>In systems programming, the <em>thundering herd problem</em> happens when multiple processes or threads are all waiting for the same event, and they all wake up at once when it occurs.<br>
Instead of one lucky process getting the job, they all rush in — wasting CPU cycles, clogging I/O, and often making performance <em>worse</em> than if they’d just queued politely.</p>
<br>
<p>Imagine a hundred worker processes all calling <code>accept()</code> on the same listening socket. A single client connects. All hundred wake up, but only one actually gets the connection. The other 99 trip over each other, go back to sleep, and waste precious cycles in the process. This is one of the many challenges of concurrent programming</p>
<br>
<p>Common causes:</p>
<ul>
<li>Poorly coordinated socket or file descriptor polling.</li>
<li>Overuse of blocking calls without proper locking.</li>
<li>Naïve use of <code>select()</code>/<code>poll()</code> with shared resources.</li>
</ul>
</li>
</ul>
<br>
<ul>
<li>
<p><strong>Bit rot</strong></p>
<p>The slow, silent decay of digital data over time. Sometimes literal — cosmic rays flipping bits on old disks, fading charge in flash memory cells — and sometimes metaphorical: neglected codebases that still run, but drift out of compatibility with modern systems.</p>
<p>In storage: uncorrected read errors on an old backup tape.
In software: that internal tool last touched in 2017 that now needs half a dozen deprecated dependencies replaced before it will even compile.</p>
<p>Moral of the story: a backup you never test is just a decorative brick, and unrun code is compost with delusions of grandeur.</p>
</li>
</ul>
<figure class="shadowed" style="max-width:500px; margin:0 auto 1rem;">
  <img src="Antikythera_Fragment_A_(Front).webp.jpg"
       alt="Fragment A of the Antikythera Mechanism"
       style="width:100%; height:auto; display:block;">
  <figcaption style="font-size:85%; color:#666; line-height:1.4; margin-top:.4em;">
    <i>Fragment A of the Antikythera Mechanism — a 2,000-year-old Greek astronomical calculator, sometimes called the world’s first analog computer.  
    A reminder of both human brilliance and the slow grind of time’s “bit rot.”</i><br>
    Photo by Marsyas – Own work, <a href="https://creativecommons.org/licenses/by-sa/3.0/" target="_blank" rel="noopener">CC&nbsp;BY-SA&nbsp;3.0</a>, via Wikimedia Commons.
  </figcaption>
</figure>
<br>
<ul>
<li>
<p><strong>pcaps or it didn&rsquo;t happen</strong></p>
<p>a play on the common Internet phrase, demanding evidence to back up an extraordinary claim. In networking, this is frequently in the form of a pcap or network capture.</p>
<p>You can capture a pcap of your favorite network traffic with something like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo tcpdump -i eth0 -s <span class="m">0</span> -w capture.pcap
</span></span></code></pre></div><p>In fact, a really good practice for folks who have to support a server product is to capture pcaps of normal traffic so you have a point of comparison when things go wrong. I keep a pcap zoo with all kinds of traffic for just that reason. Also, tcpdump is a great command to use with customers or end users who will often not have wireshark or network monitor. It makes it easy to have them run tcpdump and send you the capture for detailed analysis.</p>
<br>
<ul>
<li><a href="https://www.tcpdump.org/">TCPDUMP &amp; libpcap</a></li>
<li><a href="https://www.wireshark.org/">Wireshark</a></li>
<li><a href="https://wiki.wireshark.org/">Wireshark Wiki</a></li>
<li><a href="https://www.netresec.com/?page=PcapFiles">Pcap files</a></li>
</ul>
<hr>
<p><span class="tag red">Did You Know?</span> You can capture network traffic from a connected iOS device with two commands:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">rvictl -s <span class="k">$(</span>system_profiler SPUSBDataType <span class="p">|</span> awk -F<span class="s1">&#39;: &#39;</span> <span class="s1">&#39;/Serial Number/{print $2; exit}&#39;</span><span class="k">)</span>
</span></span></code></pre></div><p>How it works:</p>
<ul>
<li>
<p><code>system_profiler SPUSBDataType</code> — lists connected USB devices, including iOS devices.</p>
</li>
<li>
<p><code>awk -F': ' '/Serial Number/{print $2; exit}'</code> — finds the first serial number (the UDID for iOS).</p>
</li>
<li>
<p><code>rvictl -s &lt;UDID&gt;</code> — starts an RVI for that device.</p>
</li>
</ul>
<p>Once that is running, you&rsquo;ll see a new interface (<code>rvi0</code>) in <code>ifconfig</code>, and you capture from it with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo tcpdump -i rvi0 -s <span class="m">0</span> -w ios-capture.pcap
</span></span></code></pre></div></li>
</ul>
<br>
<hr>
<ul>
<li>
<p><strong>TTY (Teletypewriter)</strong></p>
<p>Before modern computers, a &ldquo;terminal&rdquo; was a clattering, electro-mechanical beast that printed your computers output on paper in response to commands you typed in which were also echoed to paper. The name stuck—in UNIX and Linux, <code>tty</code> still refers to a terminal device. You&rsquo;ll see it in commands like</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">tty
</span></span><span class="line"><span class="cl"><span class="c1"># /dev/ttys001</span>
</span></span></code></pre></div><ul>
<li>
<p><strong>Bells</strong>: Instead of a pop-up notification, the system could get the operators attention bt making the teletype <em>ding</em> with the BEL control code (<code>^G</code> or ASCII 7). That tradition lives on — try <code>echo -e &quot;\a&quot;</code> in a terminal.</p>
</li>
<li>
<p><strong>Carriage Returns</strong>: The <code>CR</code> and <code>LF</code> you hear about in text encoding? On a teletype, <code>CR</code> literally moved the print head back to the start of the line, while <code>LF</code> advanced the paper up one line. That’s why Windows still uses CR+LF as line endings — it’s pure teletype legacy.</p>
</li>
<li>
<p><strong>Virtual TTYs (vttys)</strong>: Modern systems emulate multiple terminals in software — you can switch between them with <code>Ctrl+Alt+F1</code> through <code>F6</code> on many Linux distros. They’re ghosts of teletypes past, living inside your GPU.</p>
</li>
</ul>
</li>
</ul>
<br> 
  <figure style="text-align:center; margin: 1em auto;">
    <img src="ritchie-thompson.jpg" alt="Dennis Ritchie and Ken Thompson at a PDP-11" style="display:block; margin:0 auto; max-width:500px;">
    <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
      <em>The Teletype still lives on in our virtual ttys today</em><br>
      Pictured: Ken Thompson and Dennis Ritchie 
    </figcaption>
  </figure>
<br>
<ul>
<li>
<p><strong>Cargo Cult Programming</strong></p>
<p>Following procedures or copying code/configs without understanding <em>why</em> they work, just because &ldquo;it worked someplace else&rdquo;</p>
<p>First step is to read about <a href="https://en.wikipedia.org/wiki/Cargo_cult">Cargo Cults</a>, if you haven&rsquo;t heard about them—they are a truly fascinating phenomenon that still exists today. <a href="http://news.bbc.co.uk/2/hi/asia-pacific/6363843.stm">BBC - Vanuatu cargo cult marks 50 years</a> .</p>
<br>
<p><span class="tag red">Did You Know?</span> Richard Feynman referred to research that imitates the form of science without its rigor as Cargo Cult Science.</p>
<br>
<p>A great example of this in Computer Science might be found in teams who bounce the server at any sign of trouble or keep an ancient iptables setup that a former employee configured that has always worked but isn&rsquo;t well understood. Perhaps the elders speak of a prophecy in which the one called Trevor will return and refactor the code! Cargo Cult Thinking rears it&rsquo;s ugly head in many situations.</p>
</li>
</ul>
<br>
<h2 id="the-scientific-method-for-sysadmins">The Scientific Method for Sysadmins</h2>
<p>Too often, we guess. We reboot the box, tweak a config, or apply the “fix” we saw in a forum post from 2013 — and when it works, we don’t always know why. That’s survivable when you’re debugging your own laptop, but dangerous when you’re responsible for production systems or complex networks.</p>
<p>The scientific method gives you a framework: observe, hypothesize, predict, test, and analyze. By running your troubleshooting like a controlled experiment — changing one variable at a time, documenting every step — you turn panic into process. And process means problems get solved faster, fixes stick longer, and you can explain your reasoning to anyone from your junior teammate to your most skeptical CTO.</p>
<p><strong>In short:</strong> the scientific method turns “have you tried turning it off and on again?” into “here’s the evidence, here’s the cause, and here’s how we’re preventing it next time.”</p>
<p>Here’s the lab method, translated for ops—so we stop guessing and start knowing:</p>
<h3 id="1-observe">1) Observe</h3>
<p>Gather <strong>facts, not impressions</strong>: logs, metrics, <code>tcpdump</code>/pcaps, repro steps, timestamps, versions.</p>
<h3 id="2-question">2) Question</h3>
<p>What exactly is failing? Scope it: which host, which users, which path, since when? did it ever work? just one machine or many? on network or off?</p>
<h3 id="3-hypothesize-must-be-falsifiable">3) Hypothesize (must be falsifiable)</h3>
<p><em>Because X, therefore Y fails.</em><br>
Examples: “NPM health checks time out <strong>because</strong> upstream container OOM‑kills.”<br>
“TLS handshake fails <strong>because</strong> SAN doesn’t include hostname.”</p>
<h3 id="4-predict">4) Predict</h3>
<p><em>If the hypothesis is right, then</em> a specific observation should follow.<br>
Examples: “Raising memory limit stops OOM kills.” “<code>openssl s_client</code> shows SAN mismatch.”</p>
<h3 id="5-test-change-one-variable">5) Test (change one variable)</h3>
<p>Create a minimal, <strong>reversible</strong> experiment; log everything you change.</p>
<h3 id="6-analyze">6) Analyze</h3>
<p>Did results match the prediction? If <strong>yes</strong>, increase confidence. If <strong>no</strong>, discard or refine.</p>
<h3 id="7-document--decide">7) Document &amp; Decide</h3>
<p>Record cause, fix, and rollback. Automate guardrails (alerts, dashboards, runbooks).</p>
<hr>
<h3 id="scientific-method-flowchart">Scientific Method Flowchart</h3>
<p>This is the essence of troubleshooting, especially at an engineering level.</p>
<pre class="mermaid">
  flowchart TD
  A[&#34;Observe &amp; Collect Evidence&#34;] --&gt; B[&#34;Form Hypothesis (falsifiable)&#34;]
  B --&gt; C[&#34;Predict Observable Outcome&#34;]
  C --&gt; D[&#34;Design Minimal Test (one variable)&#34;]
  D --&gt; E[&#34;Run Test &amp; Measure&#34;]
  E --&gt; F{&#34;Prediction matched?&#34;}
  F -- Yes --&gt; G[&#34;Increase confidence&#34;]
  G --&gt; H[&#34;Implement Fix / Prevent Recurrence&#34;]
  F -- No --&gt; I[&#34;Revise Hypothesis&#34;]
  I --&gt; C
  H --&gt; J[&#34;Document: notes, runbook, alerts&#34;]
  J --&gt; K[&#34;Monitor for Regression&#34;]
</pre>
<p>A few bonus concepts:</p>
<p><strong>• Occam&rsquo;s Razor</strong><br>
The simplest explanation is usually correct.</p>
<blockquote>
<p>In troubleshooting: before you blame cosmic rays, check the power cable.</p></blockquote>
<p><strong>• Hanlon&rsquo;s Razor</strong><br>
Never attribute to malice that which can be adequately explained by stupidity.</p>
<p><strong>• Trust but Verify</strong><br>
A Russian proverb — <em>doveryai, no proveryai</em> (доверяй, но проверяй) — popularized by Ronald Reagan.<br>
Completely apropos in the ops and support world.</p>
<p><strong>• Confirmation Bias</strong><br>
The tendency to notice, remember, and favor evidence that supports what you already believe.</p>
<blockquote>
<p>In troubleshooting: you’re convinced the new firewall rule is the problem, so you only look at logs that show blocked packets — ignoring that the actual cause was a DNS misconfig.<br>
Guardrail: actively seek disconfirming evidence before declaring victory.</p></blockquote>
<p><strong>• Post Hoc Fallacy (Post hoc ergo propter hoc)</strong><br>
Latin for “after this, therefore because of this.” Mistaking sequence for causation.</p>
<blockquote>
<p>“We deployed at 2 PM, and the outage happened at 2:10 PM, so the deploy <em>must</em> have caused it.”<br>
Reality: correlation ≠ causation. Check logs and metrics before assuming cause.</p></blockquote>
<p><strong>• Sunk Cost Fallacy</strong><br>
Continuing a failing approach because you’ve already invested time, money, or effort in it.</p>
<blockquote>
<p>You’ve spent 6 hours debugging a flaky script that could be rewritten from scratch in 45 minutes — but you “don’t want to waste the work you’ve already done.”</p></blockquote>
<br>
<h2 id="linguistics-corner">Linguistics Corner</h2>
<p><em>(From sound-alike shenanigans to meaning-swapping tricks — the word relationships that make language fun, precise, and occasionally treacherous.)</em></p>
<h3 id="semantic-relationships">Semantic relationships</h3>
<ul>
<li>
<p><strong>Heteronyms</strong> — Words spelled the same but pronounced differently, with different meanings.</p>
<blockquote>
<p><em>“Bass” (the fish) vs. “bass” (low-frequency sound).</em></p></blockquote>
</li>
<li>
<p><strong>Synonyms</strong> — Words with the same or nearly the same meaning.</p>
<blockquote>
<p><em>“Error” and “fault” — although in engineering, one might be upstream of the other.</em></p></blockquote>
</li>
<li>
<p><strong>Antonyms</strong> — Words with opposite meanings.</p>
<blockquote>
<p><em>“Start” vs. “stop,” “online” vs. “offline.”</em></p></blockquote>
</li>
<li>
<p><strong>Homophones</strong> — Words that sound the same but have different meanings, often spelled differently.</p>
<blockquote>
<p><em>“Write” vs. “right,” “byte” vs. “bite.”</em></p></blockquote>
</li>
<li>
<p><strong>Homonyms</strong> — A broader category where two words share the same spelling <strong>or</strong> the same sound, but have different meanings.</p>
<blockquote>
<p><em>“Bat” (the animal) vs. “bat” (baseball equipment).</em></p></blockquote>
</li>
<li>
<p><strong>Metonyms</strong> — When something is referred to not by its own name, but by something closely related.</p>
<blockquote>
<p><em>“The crown” for a monarchy, “the White House” for the U.S. presidency, “silicon” for the tech industry.”</em></p></blockquote>
</li>
</ul>
<br>
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/8Gv0H-vPoDc?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<p>If you haven&rsquo;t seen it, Weird Al&rsquo;s <em>Word Crimes</em> is a hilarious three-minute grammar masterclass.</p>
<p>So, one of my biggest pet peeves, when it comes to language, is the misuse of the following:</p>
<h3 id="countable-vs-uncountable-nouns">Countable vs. Uncountable Nouns</h3>
<p>Countable nouns are things you can count in discrete units (<strong>servers</strong>, <strong>packets</strong>, <strong>features</strong>).<br>
Uncountable nouns (aka <em>mass nouns</em>) are measured in amount or volume (<strong>bandwidth</strong>, <strong>patience</strong>, <strong>downtime</strong>).</p>
<ul>
<li>
<p><strong>Fewer</strong> → countable</p>
<blockquote>
<p><em>Fewer servers</em>, <em>fewer packets</em>, <em>fewer outages</em></p></blockquote>
</li>
<li>
<p><strong>Many</strong> → countable (positive form)</p>
<blockquote>
<p><em>Many features</em>, <em>many logins</em>, <em>many CPUs</em></p></blockquote>
</li>
<li>
<p><strong>Less</strong> → uncountable</p>
<blockquote>
<p><em>Less downtime</em>, <em>less bandwidth</em>, <em>less patience</em></p></blockquote>
</li>
<li>
<p><strong>More</strong> → works for both</p>
<blockquote>
<p><em>More CPUs</em>, <em>more storage</em>, <em>more uptime</em></p></blockquote>
</li>
<li>
<p><strong>Much</strong> → uncountable, often in questions/negatives</p>
<blockquote>
<p><em>How much RAM?</em>, <em>Not much traffic today</em></p></blockquote>
</li>
</ul>
<p>Why it matters:</p>
<ul>
<li>Using <strong>fewer</strong> when you mean <strong>less</strong> is like calling a petabyte a “kilobyte” — most people won’t care, but some will twitch.</li>
<li>Sometimes meaning changes: <em>“fewer permissions”</em> (number of actions) vs. <em>“less permission”</em> (overall authority).</li>
</ul>
<p><em>(Bonus tip: “10 items or less” signs are technically “wrong” — prescriptivists hate them, descriptivists shrug.)</em></p>
<br>
<h2 id="miscellany">Miscellany</h2>
<blockquote>
<p>&ldquo;No amount of genius can overcome a preoccupation with detail&rdquo;</p></blockquote>
<ul>
<li>
<p><strong>Droste Effect</strong></p>
<p>The visual recursion where an image contains a smaller version of itself, which contains an even smaller version, and so on.
Named after the 1904 Droste cocoa tin design — a nurse carrying a tray with a cup and the same cocoa tin, endlessly repeating.</p>
<p>In computing, you see Droste all over: VNC into a machine that’s VNC’d into the same machine, fractals, or the classic desktop screenshot set as the wallpaper, creating an infinite hallway effect.</p>
<figure class="shadowed" style="left; max-width:280px; margin:0 1rem 1rem 0;">
  <img src="Droste.JPG"
      alt="Droste Cacao mix"
      style="width:100%; height:auto; display:block;">
  <figcaption style="font-size:85%; color:#666; line-height:1.4; margin-top:.4em;">
    <em></em>
  </figcaption>
</figure>
</li>
</ul>
<br>
<ul>
<li>
<p><strong>Water-Powered Computers</strong></p>
<p>Before supercomputers, before punch cards — some problems were solved by literally plumbing the math.
Hydraulic analog computers used water levels, flows, and pressures to model complex systems, often in real time.</p>
<p>In the USSR, massive “water integrators” modeled economic and industrial processes; in the U.S., sprawling tabletop models of the San Francisco Bay used pumps, channels, and dye to simulate tides, shipping routes, and pollution spread.</p>
<p>They weren’t general-purpose machines, but for their niche, they beat any slide rule. Plus, debugging often meant getting your shoes wet.</p>
<figure class="shadowed" style="left; max-width:380px; margin:0 1rem 1rem 0;">
  <img src="800px-MONIAC_computer.jpg"
      alt="MONIAC liquid computer"
      style="width:100%; height:auto; display:block;">
  <figcaption style="font-size:85%; color:#666; line-height:1.4; margin-top:.4em;">
    <em>the Phillips Machine or MONIAC (Monetary National Income Analogue Computer)</em><br>
    By Tiia Monto - Own work, [CC BY-SA 3.0]
  </figcaption>
</figure>
<figure class="shadowed" style="left; max-width:680px; margin:0 1rem 1rem 0;">
  <img src="1024px-USCAE_Bay_Model_-_San_Francisco_Bay_Detail.jpg"
      alt="US Army Core of Engineers Model of San Francisco Bay"
      style="width:100%; height:auto; display:block;">
  <figcaption style="font-size:85%; color:#666; line-height:1.4; margin-top:.4em;">
    <em>US Army Corps of Engineers Bay Model</em><br>
    a working hydraulic, scale model of San Francisco Bay
  </figcaption>
</figure>
</li>
</ul>
<br>
<ul>
<li><strong>Apophenia</strong></li>
</ul>
<p>Seeing patterns in random noise—a human brain specialty (is it suspicious that I specified huuu-mon brains?)</p>
<blockquote>
<p>Server down? Users angry? That one LED is blinking faster than normal. Your brain will connect them into a neat conspiracy board with red string and a picture of the intern.</p></blockquote>
<ul>
<li><strong>Pareidolia</strong></li>
</ul>
<p>A specific type of apophenia — seeing familiar shapes or patterns where none exist, often faces.</p>
<blockquote>
<p>You glance at a server rack and swear one of the vent holes is <em>smiling at you</em>. Or you see a “ghost” in a heatmap when it’s really just Tuesday’s backup job.<br>
Fun fact: the “man in the moon” and “face on Mars” are both classic pareidolia moments.</p></blockquote>
<br>
<h2 id="logic-symbols--notation">Logic Symbols &amp; Notation</h2>
<p><em>(The tiny glyphs that make reasoning compact, precise, and way more fun to scribble in meeting notes.)</em></p>
<ul>
<li>
<p><strong><code>∴</code> — Therefore</strong><br>
Marks a conclusion drawn from previous statements.</p>
<blockquote>
<p><em>Error only happens after deploy, ∴ deploy introduced the bug.</em></p></blockquote>
</li>
<li>
<p><strong><code>∀</code> — For all (Universal Quantifier)</strong><br>
Means “for every member of the set, the statement is true.”</p>
<blockquote>
<p><em>∀ servers in the cluster, uptime &gt; 99.9%.</em></p></blockquote>
</li>
<li>
<p><strong><code>∃</code> — There exists (Existential Quantifier)</strong><br>
Means “there’s at least one case where this is true.”</p>
<blockquote>
<p><em>∃ packet with DF bit set causing fragmentation issues.</em></p></blockquote>
</li>
<li>
<p><strong><code>¬</code> — Not / Negation</strong><br>
Reverses truth value.</p>
<blockquote>
<p><em><code>¬healthy</code> container after config push.</em></p></blockquote>
</li>
<li>
<p><strong><code>⇒</code> — Implies</strong><br>
If the first statement is true, the second must be true.</p>
<blockquote>
<p><em>Memory leak ⇒ eventual OOM kill.</em></p></blockquote>
</li>
<li>
<p><strong><code>⇔</code> — If and only if (Iff)</strong><br>
Both statements imply each other — equivalence.</p>
<blockquote>
<p><em>TLS cert valid ⇔ SAN matches hostname.</em></p></blockquote>
</li>
<li>
<p><strong><code>⊕</code> — Exclusive OR (XOR)</strong><br>
True if exactly one, but not both, statements are true.</p>
<blockquote>
<p><em>Either DNS is wrong ⊕ the app config is wrong.</em></p></blockquote>
</li>
</ul>
<hr>
<h2 id="conclusion">Conclusion</h2>
<blockquote>
<p>&ldquo;Logic is a systematic method of coming to the wrong conclusion with confidence.&rdquo;</p></blockquote>
<p>Words (and symbols) are tools — sometimes scalpels, sometimes sledgehammers.
The right one, at the right moment, can cut through baloney, name a strange pattern, or give scale to the incomprehensible. That’s what keeps me coming back for “just one more” cromulent entry.</p>
<p style="text-align:left;">
  <a href="/tags/cromulent" class="button">🧾 View Cromulent Words Series</a>
</p>
<p>Got a candidate for Volume 4?<br>
<a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>🖖 Space the Ultimate Frontier</title>
      <link>https://adminjitsu.com/posts/space-the-ultimate-frontier/</link>
      <pubDate>Sat, 09 Aug 2025 06:59:03 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/space-the-ultimate-frontier/</guid>
      <description>Rediscover the 1986 Commodore 64/128 classic *Space the Ultimate Frontier*. Learn how to play, unlock hidden title art, manuals, and keyboard overlays, and explore its surprising origins as a mainframe Star Trek game from the early ’70s.</description>
      <content:encoded><![CDATA[<h2 id="captains-log">Captain&rsquo;s Log</h2>
<p><strong>Stardate:</strong> sometime in the late, mid-eighties&hellip;</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="Space-Ultimate-Frontier.png" alt="Space the Ultimate Frontier" style="display:block; margin:0 auto; width:min(100%, 750px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <em></em><br>
  </figcaption>
</figure>
<p> My very first computer game on my very first computer was the 1986 release of &lsquo;Space the Ultimate Frontier&rsquo; for the Commodore 64/128. I remember visiting my first computer store (with a big neon sign) at a PX in Germany and spotting the cool spaceship on the cover. I was immediately sold! It would be my first introduction to what games on a computer could be. There was strategy, energy management, action—all with SID chip music and sound effects. I was hooked on computer games from that point on.</p>
<p>Playing the game today is as easy as could be. The Internet Archive has an emulated copy that is playable in your web browser:
<a href="https://archive.org/details/Space_The_Ultimate_Frontier_19xx_-_cr_FE">Space the Ultimate Frontier</a>.</p>
<p>You can also get your own copy and run it with the VICE emulator on any platform you can think of (pretty sure I saw a BeOS download link!). 47KB of glorious C64 starship combat awaits</p>
<p>The only problem is, how do you play it?</p>
<h2 id="starship-combat">Starship Combat</h2>
<figure style="text-align:center; margin: 1em auto;">
  <img src="vice-stuf.jpg" alt="Space the Ultimate Frontier gameplay" style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <em>This screenshot is about 80KB, nearly twice as large as the entire game!</em><br>
  </figcaption>
</figure>
<br>
<p>You have been ordered to defend starbases on the border from attack by a force of Klyron ships. You start off in command of the USS Enterprise but if you die, you take command of a sister ship, the USS Columbia. Your ship has a number of systems accessible by number:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">1) HELM CONTROL 
</span></span><span class="line"><span class="cl">2) SHORT RANGE SCAN (SRS)
</span></span><span class="line"><span class="cl">3) LONG RANGE SCAN (LRS)
</span></span><span class="line"><span class="cl">4) PULSAR CONTROL
</span></span><span class="line"><span class="cl">5) PHOTON TORPEDO CONTROL
</span></span><span class="line"><span class="cl">6) SHIELD CONTROL
</span></span><span class="line"><span class="cl">7) SUBSPACE COMMUNICATIONS
</span></span><span class="line"><span class="cl">8) LIBRARY COMPUTER
</span></span><span class="line"><span class="cl">9) TRACTOR BEAM
</span></span><span class="line"><span class="cl">01) WEAPONS STATUS
</span></span><span class="line"><span class="cl">02) RESUPPLY
</span></span><span class="line"><span class="cl">03) POWER TRANSFER
</span></span><span class="line"><span class="cl">04) DAMAGE CONTROL
</span></span><span class="line"><span class="cl">05) REPAIR
</span></span><span class="line"><span class="cl">06) STATUS REPORT
</span></span><span class="line"><span class="cl">0D) Destruct Sequence 
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">ESC - exit from a system
</span></span></code></pre></div><br>
<p>Movement and Targeting are based on a circular vector arrangement</p>
<figure class="shadowed" style="align:left; max-width:420px; margin:0 1rem 1rem 0;">
  <img src="movement.png"
       alt="Movement vector wheel for Space the Ultimate Frontier"
       style="width:100%; height:auto; display:block;">
  <figcaption style="font-size:85%; color:#666; line-height:1.4; margin-top:.4em;">
    <em>Movement vectors: use 1–8, or halves (e.g., 1.5) to aim between headings.</em>
  </figcaption>
</figure>
<br>
<p>You can specify &ldquo;2&rdquo; for up and to the right or &ldquo;1.5&rdquo; for halfway between 1 and 2.</p>
<p>The game supports playing with the keyboard and an optional, single-button joystick. If you prefer keyboard-only, the game maps the joystick to the H,J,M, and I keys.</p>
<p>Other important hotkeys:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">F1 - Raise and balance both shields
</span></span><span class="line"><span class="cl">F3 - Drop both shields to 0
</span></span><span class="line"><span class="cl">F5 - Cancel Klaxon horn
</span></span><span class="line"><span class="cl">F7 - Swivel your starship about its vertical axis
</span></span><span class="line"><span class="cl">F8 - Set simulation difficulty (1-3 digits up to 128 where higher numbers are more difficult. try 37 or 03!)
</span></span><span class="line"><span class="cl">ESC - Escape from any system and return to command mode
</span></span></code></pre></div><br>
<figure style="text-align:center; margin: 1em auto;">
  <img src="STUF-gameplay2.jpg" alt="Space the Ultimate Frontier gameplay" style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <em>a Klingon—uh—Klyron battlecruiser maneuvers behind a planet for cover </em><br>
  </figcaption>
</figure>
<p>In the Long Range Scanner screen, each sector is represented by a three-digit number (e.g., <code>315</code>).<br>
The first digit is the number of Klyrons (<code>3</code>), the second is the number of starbases (<code>1</code>),<br>
and the third is the number of planets (<code>5</code>).</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="STUF-lrs.jpg" alt="Space the Ultimate Frontier LRS" style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <em>Mastering the Long Rang Scanners</em><br>
  </figcaption>
</figure>
<p>Starbases are your lifeline — docking with them is the only way to repair damage and re-arm your ship.<br>
Defend them at all costs. If they’re destroyed, you’ll lose… and the after-action report will be filed<br>
by the Klyron cook or janitor (their captain is far too busy to waste time writing about human <em>p’takh</em>).</p>
<br>
<p>The all-important Destruct Sequence requires several codes:</p>
<blockquote>
<p>DESTRUCT SEQUENCE ONE:  11A</p>
<p>DESTRUCT SEQUENCE TWO:  11A2B</p>
<p>DESTRUCT SEQUENCE THREE: 1B2B3</p>
<p>FINAL CODE:  000</p></blockquote>
<h2 id="viewing-the-manual">Viewing the Manual</h2>
<p>Luckily the game included a manual detailing the gameplay of this unlicensed, not-at-all-Star Trek like game. One of the neat things about Space the Ultimate Frontier is that the game disk isn’t just the program — it also contains the title art, the full game manual, and a keyboard overlay, all as separate PRG files you can load from BASIC. They aren’t available in the Internet Archive’s in-browser emulator, so if you want to see them, you’ll need to run the .d64 in VICE (or another C64 emulator) and do it yourself.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="c64-basic.png" alt="Commodore 64 BASIC screen" style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <em>The iconic blue-on-blue BASIC screen — 38911 bytes free and endless possibilities</em><br>
  </figcaption>
</figure>
<p>Here&rsquo;s how:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">load &#34;$&#34;,8
</span></span><span class="line"><span class="cl">list
</span></span></code></pre></div><p>You&rsquo;ll see something like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-arduino" data-lang="arduino"><span class="line"><span class="cl"><span class="mi">192</span> <span class="s">&#34;THE ULT.FRONTIER&#34;</span> <span class="n">PRG</span>
</span></span><span class="line"><span class="cl"><span class="mi">14</span> <span class="s">&#34;SPACE PIC&#34;</span>        <span class="n">PRG</span>
</span></span><span class="line"><span class="cl"><span class="mi">153</span> <span class="s">&#34;SPACE DOX&#34;</span>        <span class="n">PRG</span>
</span></span><span class="line"><span class="cl"><span class="mi">43</span> <span class="s">&#34;SPACE KB OVERLAY&#34;</span> <span class="n">PRG</span>
</span></span><span class="line"><span class="cl"><span class="mi">262</span> <span class="n">BLOCKS</span> <span class="n">FREE</span><span class="p">.</span>
</span></span></code></pre></div><p>To view the extras:</p>
<ol>
<li><strong>Title Screen</strong></li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">load &#34;SPACE PIC&#34;,8
</span></span><span class="line"><span class="cl">run
</span></span></code></pre></div><ol start="2">
<li>Game Manual</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">load &#34;SPACE DOX&#34;,8
</span></span><span class="line"><span class="cl">run
</span></span></code></pre></div><ol start="3">
<li>Keyboard Overlay</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">LOAD &#34;SPACE KB OVERLAY&#34;,8
</span></span><span class="line"><span class="cl">RUN
</span></span></code></pre></div><p>In order to view the Keyboard (KB) overlay, you will need to set up VICE to print to BMP.</p>
<hr>
<h3 id="-setting-up-vice-to-print-to-bmp-file">🖨 Setting up VICE to print to BMP file</h3>
<br>
<figure style="text-align:center; margin: 1em auto;">
  <img src="vice-printer.png" alt="printing from VICE" style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <em>Printing without the dot matrix noise loses some of the charm</em><br>
  </figcaption>
</figure>
<ol>
<li>
<p><strong>Open Printer Settings</strong></p>
<ul>
<li>Go to <strong>Settings → Peripheral Settings → Printer Settings</strong>.</li>
<li>Select <strong>Printer #4</strong> (or another printer slot if preferred).</li>
</ul>
</li>
<li>
<p><strong>Enable Virtual and IEC Devices</strong></p>
<ul>
<li>Go to <strong>Settings → Peripheral Settings</strong>.</li>
<li>Check <strong>Enable virtual devices</strong>.</li>
<li>Check <strong>Enable IEC device</strong>.</li>
</ul>
</li>
<li>
<p><strong>Configure Printer #4</strong></p>
<ul>
<li><strong>Emulation type</strong>: <code>File system</code></li>
<li><strong>Device #4 printer text device</strong>: path to your output file/directory (e.g. <code>/home/you/vice_printouts/manual.bmp</code>)</li>
<li><strong>Printer driver</strong>: <code>MPS803</code> <em>(reliable for graphics/text mix)</em></li>
<li><strong>Output type</strong>: <code>Graphics</code></li>
<li><strong>Output file format</strong>: <code>BMP</code></li>
<li><strong>Form feed on close</strong>: ✅ <em>(ensures the BMP finalizes correctly)</em></li>
</ul>
</li>
<li>
<p><strong>Save Settings</strong></p>
<ul>
<li>Go to <strong>Settings → Save current settings</strong> so the printer setup is remembered.</li>
</ul>
</li>
<li>
<p><strong>Print from the C64</strong></p>
<ul>
<li>In the program, choose the “print” function.</li>
<li>The BMP file will appear at your chosen path when printing completes.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="a-great-c64-version-of-a-mainframe-classic">A great, C64 version of a mainframe classic</h2>
<p>Space the Ultimate Frontier is a shiny, C64/C128 take on the <em>classic</em> &ldquo;Star Trek&rdquo; game that first appeared on the Sigma 7 mainframe at the University of California, Irvine in 1971. That game, written in BASIC, spread and soon appeared on systems from the PDP-10 to the HP 2000 time-sharing system.</p>
<p>You can still get a taste of the mainframe glory days by checking out the 1970s C version that is still available in the bsdgames package.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt update
</span></span><span class="line"><span class="cl">sudo apt install bsdgames
</span></span><span class="line"><span class="cl">trek
</span></span></code></pre></div><p>I&rsquo;m sure that was more fun to play on a teletype in a big expensive computing center. The C64 version really brings the game to life with smooth, simple graphics and Commodore&rsquo;s famously decent sound chip. I love that this game was an implementation of a Unix mainframe classic that was already almost 20 years old when it was released.</p>
<br>
<h2 id="links-and-stuff">Links and stuff</h2>
<p>YouTube longplay video (good preservation demo but not as much fun as playing it)
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/8d4bHY9l3HM?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>
</p>
<table>
  <thead>
      <tr>
          <th>Resource</th>
          <th>Description</th>
          <th>Link</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>TOSEC (The Old School Emulation Center)</strong></td>
          <td>Massive preservation project with verified ROM sets.</td>
          <td><a href="https://www.tosecdev.org/">tosecdev.org</a></td>
      </tr>
      <tr>
          <td><strong>GameBase64 (GB64)</strong></td>
          <td>Curated database of C64 games with metadata, screenshots, and downloads.</td>
          <td><a href="https://www.gamebase64.com/">gamebase64.com</a></td>
      </tr>
      <tr>
          <td><strong>Lemon64</strong></td>
          <td>Community site with game reviews, forums, and downloads.</td>
          <td><a href="https://www.lemon64.com/">lemon64.com</a></td>
      </tr>
      <tr>
          <td><strong>C64.com</strong></td>
          <td>Interviews, game downloads, and scene history.</td>
          <td><a href="https://www.c64.com/">c64.com</a></td>
      </tr>
      <tr>
          <td><strong>Internet Archive C64 Collection</strong></td>
          <td>Huge playable-in-browser C64 software archive.</td>
          <td><a href="https://archive.org/details/softwarelibrary_c64">archive.org/details/softwarelibrary_c64</a></td>
      </tr>
  </tbody>
</table>
<br> 
<blockquote>
<p>🖖 You can check out the pdf of the manual that I uploaded to the Internet Archive <br>
<strong>Internet Archive – STUF Manual</strong>  Scanned manual for <em>Space: The Ultimate Frontier</em> (C64/C128), with PDF download.</p>
<p><a href="https://archive.org/details/space-the-ultimate-frontier-manual/Space-the-Ultimate-Frontier-manual.pdf">archive.org/details/space-the-ultimate-frontier-manual/Space-the-Ultimate-Frontier-manual.pdf</a></p></blockquote>
<h2 id="conclusion">Conclusion</h2>
<p>This game has a special place in my heart but nostalgia aside, it packs a lot of gameplay into a tiny memory footprint. It has definitely earned a place in my boredom-battling games kit!</p>
<br> 
<p>A little bonus in case you want to hear the C64 SID chip sing!
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/TDtRdBX1UKs?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>
</p>
<br>
<figure style="text-align:center; margin: 1em auto;">
  <img src="STUF-designer.jpg" alt="Space the Ultimate Frontier designer" style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <em>Game designer Dave Neale brought equal parts strategy and chaos to the bridge</em><br>
  </figcaption>
</figure>
<p>🖖 If you want even more Star Trek fun, check out my <a href="/posts/borgify/">Borgify</a> script post and start borgifying those annoying work emails today!</p>
<p>Have a fond memory of this or other great Commodore games (Space Taxi anyone)? A battle story? I&rsquo;d love to hear about it. Email me: <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Vim Setup</title>
      <link>https://adminjitsu.com/posts/vim-setup/</link>
      <pubDate>Fri, 08 Aug 2025 19:59:05 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/vim-setup/</guid>
      <description>My personal Vim configuration, fine-tuned for portability across macOS, Linux, and Windows. Includes plugin setup, usage guide, and power moves for efficient editing.</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<blockquote>
<p><em>“A writer is someone for whom writing is more difficult than it is for other people.”</em> — Thomas Mann</p></blockquote>
<br>
&emsp;I’ve spent a lot of time building a consistent, unified environment across my machines and VMs. It’s great having the same powerful tools available almost everywhere, but keeping that setup portable between macOS, Windows, and Linux—while juggling different versions of Vim and gVim—takes some planning.
<p>In this post, I’ll share my personal <code>.vimrc</code> along with a few tips, tricks, and lessons learned from years of periodically refining it.</p>
<h3 id="--why-portability-matters">🌐  Why Portability Matters</h3>
<p>When your configuration works the same way on every system, you stop wasting time re-learning shortcuts, hunting for missing plugins, or adjusting to quirks between machines. A portable setup means you can drop into a fresh VM, a coworker’s laptop, or a remote server and feel instantly at home—your muscle memory works, your tools are there, and you can focus on the task instead of fighting the environment. For me, it also removes the mental friction of context-switching between macOS, Linux, and Windows, which makes me faster and keeps my workflow consistent.</p>
<h2 id="highlights-of-this-vimrc-why-it-feels-better-than-stock">Highlights of this <code>.vimrc</code> (Why it feels better than stock)</h2>
<p><img alt="vimrc screenshot" loading="lazy" src="/posts/vim-setup/vimrc.jpg"></p>
<p><strong>One <code>.vimrc</code> to Rule Them All</strong><br>
Works across macOS, Linux, and Windows terminals with minimal assumptions and no odd dependencies. Drop it on a fresh box, run <code>:PlugInstall</code>, and you’re ready to work.</p>
<p><strong>Readable, Low-Friction UI</strong></p>
<ul>
<li><em>Lightline</em> statusline: fast, informative, and clean.</li>
<li><em>Line numbers</em> with quick relative toggle: <code>,n</code>.</li>
<li><em>Cursorline</em>, <em>wildmenu</em>, <em>ruler</em>, and <em>title</em> for better orientation.</li>
<li>Dark-friendly colors: <code>set background=dark</code> + stock <code>desert</code> theme.</li>
</ul>
<p><strong>Better Search and Navigation</strong></p>
<ul>
<li>Smart search (<code>ignorecase</code> + <code>smartcase</code>, <code>incsearch</code>, <code>hlsearch</code>).</li>
<li>Split navigation on homerow: <code>Ctrl-h/j/k/l</code>.</li>
<li>Indent-based folding (all folds open by default).</li>
</ul>
<p><strong>Editing Defaults That Don’t Fight You</strong></p>
<ul>
<li>Spaces with 4-wide indent (<code>expandtab</code>, <code>shiftwidth=4</code>, <code>softtabstop=4</code>).</li>
<li>Filetype-aware tweaks for Python and shell built in.</li>
</ul>
<p><strong>Built-in Git and File Tools</strong></p>
<ul>
<li><em>FZF</em> for instant file jumps: <code>,f</code> (files), <code>,g</code> (git files).</li>
<li><em>Fugitive</em>: <code>,gs</code> (status), <code>,gd</code> (diff), <code>,gc</code> (commit), <code>,gb</code> (blame).</li>
<li><em>Undotree</em>: <code>,u</code> for undo history.</li>
<li><em>NERDTree</em> available via <code>:NERDTreeToggle</code>.</li>
</ul>
<h2 id="my-vimrc">My .vimrc</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-vim" data-lang="vim"><span class="line"><span class="cl"><span class="c">&#34; ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c">&#34; Kevin&#39;s Portable .vimrc</span>
</span></span><span class="line"><span class="cl"><span class="c">&#34; Clean, fast, readable.</span>
</span></span><span class="line"><span class="cl"><span class="c">&#34; ===============================</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">let</span> <span class="nx">mapleader</span> <span class="p">=</span> <span class="s2">&#34;,&#34;</span>           <span class="c">&#34; Press Esc, comma then shortcut</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; === General Behavior ===</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">nocompatible</span>              <span class="c">&#34; Don&#39;t use old vi compatibility</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">backspace</span><span class="p">=</span><span class="nx">indent</span><span class="p">,</span><span class="nx">eol</span><span class="p">,</span><span class="nx">start</span> <span class="c">&#34; Backspace works over everything</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">history</span><span class="p">=</span><span class="m">1000</span>              <span class="c">&#34; Command and search history</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">hidden</span>                    <span class="c">&#34; Allow switching buffers without saving</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">mouse</span><span class="p">=</span><span class="nx">a</span>                   <span class="c">&#34; Enable mouse support in all modes</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; === UI Enhancements ===</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">number</span>                    <span class="c">&#34; Show absolute line numbers</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">nowrap</span>                    <span class="c">&#34; Don&#39;t wrap long lines</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">linebreak</span>                 <span class="c">&#34; Break lines at word boundaries</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">showcmd</span>                   <span class="c">&#34; Show incomplete command in lower right</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">cursorline</span>                <span class="c">&#34; Highlight current line</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">ruler</span>                     <span class="c">&#34; Show cursor position in the status line</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">wildmenu</span>                  <span class="c">&#34; Tab completion menu for commands</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">title</span>                     <span class="c">&#34; Show file title in terminal title bar</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">laststatus</span><span class="p">=</span><span class="m">2</span>              <span class="c">&#34; always show status line</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; === Search Behavior ===</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">hlsearch</span>                  <span class="c">&#34; Highlight search results</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">incsearch</span>                 <span class="c">&#34; Show match while typing</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">ignorecase</span>                <span class="c">&#34; Ignore case in searches...</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">smartcase</span>                 <span class="c">&#34; ...unless capital letters used</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; === Indentation ===</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">expandtab</span>                 <span class="c">&#34; Use spaces instead of tabs</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">shiftwidth</span><span class="p">=</span><span class="m">4</span>              <span class="c">&#34; Indent by 4 spaces</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">tabstop</span><span class="p">=</span><span class="m">4</span>                 <span class="c">&#34; A tab character looks like 4 spaces</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">softtabstop</span><span class="p">=</span><span class="m">4</span>             <span class="c">&#34; Insert 4 spaces when pressing Tab</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">autoindent</span>                <span class="c">&#34; Copy indent from current line</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">smartindent</span>               <span class="c">&#34; Do smart C-style indenting</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; === Filetype-Specific Tweaks ===</span>
</span></span><span class="line"><span class="cl"><span class="k">filetype</span> <span class="nx">plugin</span> <span class="nx">indent</span> <span class="nx">on</span>     <span class="c">&#34; Enable filetype-specific plugins and indenting</span>
</span></span><span class="line"><span class="cl"><span class="k">autocmd</span> <span class="nx">FileType</span> <span class="nx">python</span> <span class="nx">setlocal</span> <span class="nx">expandtab</span> <span class="nx">shiftwidth</span><span class="p">=</span><span class="m">4</span> <span class="nx">softtabstop</span><span class="p">=</span><span class="m">4</span>
</span></span><span class="line"><span class="cl"><span class="k">autocmd</span> <span class="nx">FileType</span> <span class="nx">sh</span>     <span class="nx">setlocal</span> <span class="nx">expandtab</span> <span class="nx">shiftwidth</span><span class="p">=</span><span class="m">4</span> <span class="nx">softtabstop</span><span class="p">=</span><span class="m">4</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; === Folding ===</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">foldmethod</span><span class="p">=</span><span class="nx">indent</span>         <span class="c">&#34; Fold based on indent levels</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">foldlevel</span><span class="p">=</span><span class="m">99</span>              <span class="c">&#34; Open all folds by default</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; === Performance Tweaks ===</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">lazyredraw</span>                <span class="c">&#34; Don’t redraw while executing macros</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">ttyfast</span>                   <span class="c">&#34; Assume a fast terminal connection</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">noswapfile</span>                <span class="c">&#34; Don&#39;t use swapfiles</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">nobackup</span>                  <span class="c">&#34; Don&#39;t keep backup files</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">nowritebackup</span>             <span class="c">&#34; Don&#39;t backup before overwriting</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; === Colors and Syntax ===</span>
</span></span><span class="line"><span class="cl"><span class="k">syntax</span> <span class="nx">on</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">background</span><span class="p">=</span><span class="nb">dark</span>           <span class="c">&#34; Tell colorschemes to use dark background</span>
</span></span><span class="line"><span class="cl"><span class="k">colorscheme</span> <span class="nx">desert</span>            <span class="c">&#34; Pick your favorite — &#39;desert&#39; is safe and readable</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; === Clipboard (if supported) ===</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">clipboard</span><span class="p">=</span><span class="nx">unnamedplus</span>     <span class="c">&#34; Use system clipboard (requires +clipboard)</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; === Keybindings and Leader ===</span>
</span></span><span class="line"><span class="cl"><span class="c">&#34; i am leaving this here as a warning to future generations to never do this</span>
</span></span><span class="line"><span class="cl"><span class="c">&#34; let mapleader = &#34; &#34;           &#34; Use SPACE as the &lt;leader&gt; key</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; --- Smart Save ---</span>
</span></span><span class="line"><span class="cl"><span class="nx">nnoremap</span> <span class="p">&lt;</span><span class="nx">C</span><span class="p">-</span><span class="nx">s</span><span class="p">&gt;</span> :<span class="nx">w</span><span class="p">&lt;</span><span class="nx">CR</span><span class="p">&gt;</span>         <span class="c">&#34; Ctrl+S to save</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; --- Command Prompt Shortcut ---</span>
</span></span><span class="line"><span class="cl"><span class="c">&#34; Use SPACE for command mode — normal mode only</span>
</span></span><span class="line"><span class="cl"><span class="nx">nnoremap</span> <span class="p">&lt;</span><span class="nx">silent</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nx">SPACE</span><span class="p">&gt;</span> :
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; --- Toggle hlsearch ---</span>
</span></span><span class="line"><span class="cl"><span class="nx">nnoremap</span> <span class="p">&lt;</span><span class="nx">leader</span><span class="p">&gt;</span><span class="nx">h</span> :<span class="k">set</span> <span class="nx">hlsearch</span><span class="p">!&lt;</span><span class="nx">CR</span><span class="p">&gt;</span>    <span class="c">&#34; Toggle search highlight on/off</span>
</span></span><span class="line"><span class="cl"><span class="nx">nnoremap</span> <span class="p">&lt;</span><span class="nx">leader</span><span class="p">&gt;&lt;</span><span class="nx">CR</span><span class="p">&gt;</span> :<span class="nx">nohlsearch</span><span class="p">&lt;</span><span class="nx">CR</span><span class="p">&gt;</span>   <span class="c">&#34; Clear highlight quickly</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; --- Toggle relative numbers ---</span>
</span></span><span class="line"><span class="cl"><span class="nx">nnoremap</span> <span class="p">&lt;</span><span class="nx">leader</span><span class="p">&gt;</span><span class="nx">n</span> :<span class="k">set</span> <span class="nx">relativenumber</span><span class="p">!&lt;</span><span class="nx">CR</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; --- Yank to system clipboard ---</span>
</span></span><span class="line"><span class="cl"><span class="nx">vnoremap</span> <span class="p">&lt;</span><span class="nx">C</span><span class="p">-</span><span class="nx">c</span><span class="p">&gt;</span> <span class="s2">&#34;+y            &#34;</span> <span class="nx">Ctrl</span><span class="p">+</span><span class="nx">C</span> <span class="nx">in</span> <span class="nx">visual</span> <span class="nx">mode</span> <span class="nx">yanks</span> <span class="nx">to</span> <span class="nx">system</span> <span class="nx">clipboard</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; --- Better window movement ---</span>
</span></span><span class="line"><span class="cl"><span class="nx">nnoremap</span> <span class="p">&lt;</span><span class="nx">C</span><span class="p">-</span><span class="nx">h</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nx">C</span><span class="p">-</span><span class="nx">w</span><span class="p">&gt;</span><span class="nx">h</span>
</span></span><span class="line"><span class="cl"><span class="nx">nnoremap</span> <span class="p">&lt;</span><span class="nx">C</span><span class="p">-</span><span class="nx">j</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nx">C</span><span class="p">-</span><span class="nx">w</span><span class="p">&gt;</span><span class="nx">j</span>
</span></span><span class="line"><span class="cl"><span class="nx">nnoremap</span> <span class="p">&lt;</span><span class="nx">C</span><span class="p">-</span><span class="nx">k</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nx">C</span><span class="p">-</span><span class="nx">w</span><span class="p">&gt;</span><span class="nx">k</span>
</span></span><span class="line"><span class="cl"><span class="nx">nnoremap</span> <span class="p">&lt;</span><span class="nx">C</span><span class="p">-</span><span class="nx">l</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nx">C</span><span class="p">-</span><span class="nx">w</span><span class="p">&gt;</span><span class="nx">l</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; === my shortcuts ===</span>
</span></span><span class="line"><span class="cl"><span class="nx">nnoremap</span> <span class="p">&lt;</span><span class="nx">leader</span><span class="p">&gt;</span><span class="nx">f</span> :<span class="nx">Files</span><span class="p">&lt;</span><span class="nx">CR</span><span class="p">&gt;</span>           <span class="c">&#34; FZF file search</span>
</span></span><span class="line"><span class="cl"><span class="nx">nnoremap</span> <span class="p">&lt;</span><span class="nx">leader</span><span class="p">&gt;</span><span class="nx">g</span> :<span class="nx">GFiles</span><span class="p">&lt;</span><span class="nx">CR</span><span class="p">&gt;</span>          <span class="c">&#34; FZF Git files</span>
</span></span><span class="line"><span class="cl"><span class="nx">nnoremap</span> <span class="p">&lt;</span><span class="nx">leader</span><span class="p">&gt;</span><span class="nx">s</span> :<span class="nx">w</span><span class="p">&lt;</span><span class="nx">CR</span><span class="p">&gt;</span>               <span class="c">&#34; Save file</span>
</span></span><span class="line"><span class="cl"><span class="nx">nnoremap</span> <span class="p">&lt;</span><span class="nx">leader</span><span class="p">&gt;</span><span class="nx">q</span> :<span class="nx">q</span><span class="p">&lt;</span><span class="nx">CR</span><span class="p">&gt;</span>               <span class="c">&#34; Quit</span>
</span></span><span class="line"><span class="cl"><span class="c">&#34; --- Git Integration ---</span>
</span></span><span class="line"><span class="cl"><span class="nx">nnoremap</span> <span class="p">&lt;</span><span class="nx">leader</span><span class="p">&gt;</span><span class="nx">gs</span> :<span class="nx">G</span><span class="p">&lt;</span><span class="nx">CR</span><span class="p">&gt;</span>               <span class="c">&#34; ,gs → git status (fugitive)</span>
</span></span><span class="line"><span class="cl"><span class="nx">nnoremap</span> <span class="p">&lt;</span><span class="nx">leader</span><span class="p">&gt;</span><span class="nx">gc</span> :<span class="nx">G</span> <span class="nx">commit</span><span class="p">&lt;</span><span class="nx">CR</span><span class="p">&gt;</span>        <span class="c">&#34; ,gc → git commit</span>
</span></span><span class="line"><span class="cl"><span class="nx">nnoremap</span> <span class="p">&lt;</span><span class="nx">leader</span><span class="p">&gt;</span><span class="nx">gd</span> :<span class="nx">G</span> <span class="nx">diff</span><span class="p">&lt;</span><span class="nx">CR</span><span class="p">&gt;</span>          <span class="c">&#34; ,gd → git diff</span>
</span></span><span class="line"><span class="cl"><span class="nx">nnoremap</span> <span class="p">&lt;</span><span class="nx">leader</span><span class="p">&gt;</span><span class="nx">gb</span> :<span class="nx">G</span> <span class="nx">blame</span><span class="p">&lt;</span><span class="nx">CR</span><span class="p">&gt;</span>         <span class="c">&#34; ,gb → git blame</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; --- Undo and Session Tools ---</span>
</span></span><span class="line"><span class="cl"><span class="nx">nnoremap</span> <span class="p">&lt;</span><span class="nx">leader</span><span class="p">&gt;</span><span class="nx">u</span> :<span class="nx">UndotreeToggle</span><span class="p">&lt;</span><span class="nx">CR</span><span class="p">&gt;</span>   <span class="c">&#34; ,u → toggle undo tree</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; --- Editing Shortcuts ---</span>
</span></span><span class="line"><span class="cl"><span class="nx">nnoremap</span> <span class="p">&lt;</span><span class="nx">leader</span><span class="p">&gt;</span><span class="nx">w</span> :<span class="nx">w</span><span class="p">&lt;</span><span class="nx">CR</span><span class="p">&gt;</span>                <span class="c">&#34; ,w → write file</span>
</span></span><span class="line"><span class="cl"><span class="nx">nnoremap</span> <span class="p">&lt;</span><span class="nx">leader</span><span class="p">&gt;</span><span class="nx">q</span> :<span class="nx">q</span><span class="p">&lt;</span><span class="nx">CR</span><span class="p">&gt;</span>                <span class="c">&#34; ,q → quit</span>
</span></span><span class="line"><span class="cl"><span class="nx">nnoremap</span> <span class="p">&lt;</span><span class="nx">leader</span><span class="p">&gt;</span><span class="nx">x</span> :<span class="nx">x</span><span class="p">&lt;</span><span class="nx">CR</span><span class="p">&gt;</span>                <span class="c">&#34; ,x → save and quit</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; === Optional Fun Aliases ===</span>
</span></span><span class="line"><span class="cl"><span class="nx">command</span> <span class="nx">WQ</span> <span class="nx">wq</span>
</span></span><span class="line"><span class="cl"><span class="nx">command</span> <span class="nx">W</span> <span class="nx">w</span>
</span></span><span class="line"><span class="cl"><span class="nx">command</span> <span class="nx">Q</span> <span class="nx">q</span>
</span></span><span class="line"><span class="cl"><span class="nx">command</span> <span class="nx">E</span> <span class="nx">e</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; === vim-plug plugin manager ===</span>
</span></span><span class="line"><span class="cl"><span class="nx">call</span> <span class="nx">plug</span>#<span class="nx">begin</span><span class="p">(</span><span class="s1">&#39;~/.vim/plugged&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">Plug</span> <span class="s1">&#39;tpope/vim-fugitive&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nx">Plug</span> <span class="s1">&#39;junegunn/fzf&#39;</span><span class="p">,</span> { <span class="s1">&#39;do&#39;</span>: { <span class="p">-&gt;</span> <span class="nx">fzf</span>#<span class="nx">install</span><span class="p">()</span> } }
</span></span><span class="line"><span class="cl"><span class="nx">Plug</span> <span class="s1">&#39;junegunn/fzf.vim&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nx">Plug</span> <span class="s1">&#39;preservim/nerdtree&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nx">Plug</span> <span class="s1">&#39;itchyny/lightline.vim&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nx">call</span> <span class="nx">plug</span>#<span class="nx">end</span><span class="p">()</span>
</span></span></code></pre></div><p>I live a little dangerously (swap and backup disabled, etc.). If you want safer options, add this to the top of the <code>.vimrc</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-vim" data-lang="vim"><span class="line"><span class="cl"><span class="c">&#34; --- Safe persistence (portable)</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">undofile</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">undodir</span>^<span class="p">=~</span><span class="sr">/.vim/</span><span class="nx">undo</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">backup</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">writebackup</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">backupdir</span>^<span class="p">=~</span><span class="sr">/.vim/</span><span class="nx">backup</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nx">directory</span>^<span class="p">=~</span><span class="sr">/.vim/</span><span class="nx">swap</span>
</span></span><span class="line"><span class="cl"><span class="c">
</span></span></span><span class="line"><span class="cl"><span class="c">&#34; Create dirs if missing (once per boot)</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">!</span><span class="nx">isdirectory</span><span class="p">(</span><span class="nx">expand</span><span class="p">(</span><span class="s1">&#39;~/.vim/undo&#39;</span><span class="p">))</span> <span class="p">|</span> <span class="nx">call</span> <span class="nx">mkdir</span><span class="p">(</span><span class="nx">expand</span><span class="p">(</span><span class="s1">&#39;~/.vim/undo&#39;</span><span class="p">),</span> <span class="s1">&#39;p&#39;</span><span class="p">)</span> <span class="p">|</span> <span class="k">endif</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">!</span><span class="nx">isdirectory</span><span class="p">(</span><span class="nx">expand</span><span class="p">(</span><span class="s1">&#39;~/.vim/backup&#39;</span><span class="p">))</span> <span class="p">|</span> <span class="nx">call</span> <span class="nx">mkdir</span><span class="p">(</span><span class="nx">expand</span><span class="p">(</span><span class="s1">&#39;~/.vim/backup&#39;</span><span class="p">),</span> <span class="s1">&#39;p&#39;</span><span class="p">)</span> <span class="p">|</span> <span class="k">endif</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">!</span><span class="nx">isdirectory</span><span class="p">(</span><span class="nx">expand</span><span class="p">(</span><span class="s1">&#39;~/.vim/swap&#39;</span><span class="p">))</span> <span class="p">|</span> <span class="nx">call</span> <span class="nx">mkdir</span><span class="p">(</span><span class="nx">expand</span><span class="p">(</span><span class="s1">&#39;~/.vim/swap&#39;</span><span class="p">),</span> <span class="s1">&#39;p&#39;</span><span class="p">)</span> <span class="p">|</span> <span class="k">endif</span>
</span></span></code></pre></div><h2 id="usage-notes">Usage notes</h2>
<p>On new machines, I can do the following to have an identical setup:</p>
<h3 id="setting-up-thisvimrc-on-a-new-machine">Setting up this<code>.vimrc</code> on a New Machine</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1">## Setting Up My `.vimrc` on a New Machine</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 1. Install Vim and helpers</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Ubuntu/Debian</span>
</span></span><span class="line"><span class="cl">sudo apt install vim git fzf ripgrep
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># macOS (Homebrew)</span>
</span></span><span class="line"><span class="cl">brew install vim fzf ripgrep
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2. Install vim-plug (plugin manager)</span>
</span></span><span class="line"><span class="cl">curl -fLo ~/.vim/autoload/plug.vim --create-dirs <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 3. Link your vimrc from your dotfiles repo</span>
</span></span><span class="line"><span class="cl">ln -s ~/codelab/dotfiles/vimrc ~/.vimrc
</span></span><span class="line"><span class="cl"><span class="c1"># Adjust the path above if your dotfiles are elsewhere</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 4. Install plugins from within Vim</span>
</span></span><span class="line"><span class="cl"><span class="c1"># (You can also do this from the shell)</span>
</span></span><span class="line"><span class="cl">vim +PlugInstall +qall
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 5. Optional: Enable Ctrl-S in terminal</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;stty -ixon&#39;</span> &gt;&gt; ~/.bashrc
</span></span><span class="line"><span class="cl"><span class="c1"># Prevents terminal freeze when pressing Ctrl-S in Vim</span>
</span></span></code></pre></div><h3 id="how-to-use-these-settings">How to use these settings</h3>
<h4 id="basics">Basics</h4>
<ul>
<li><strong>Leader key</strong>: <code>,</code> — press this, then the shortcut.</li>
<li><strong>Save</strong>: <code>Ctrl-S</code> or <code>,w</code></li>
<li><strong>Quit</strong>: <code>,q</code></li>
<li><strong>Save &amp; Quit</strong>: <code>,x</code></li>
<li><strong>Command-line mode</strong>: <code>&lt;Space&gt;</code> in normal mode starts <code>:commands</code>.</li>
</ul>
<h4 id="navigation">Navigation</h4>
<ul>
<li><strong>Line numbers</strong>: absolute by default; toggle relative → <code>,n</code></li>
<li><strong>Move between splits</strong>: <code>Ctrl-h</code>, <code>Ctrl-j</code>, <code>Ctrl-k</code>, <code>Ctrl-l</code></li>
<li><strong>Folds</strong>: indent-based, all open by default (<code>zc</code> close, <code>zo</code> open)</li>
</ul>
<h4 id="search">Search</h4>
<ul>
<li><strong>Smart search</strong>: case-insensitive unless you type a capital.</li>
<li><strong>Incremental search</strong>: matches appear as you type.</li>
<li><strong>Toggle highlight</strong>: <code>,h</code></li>
<li><strong>Clear highlight</strong>: <code>,&lt;CR&gt;</code></li>
</ul>
<h4 id="clipboard">Clipboard</h4>
<ul>
<li><strong>Yank to system clipboard</strong>: in visual mode, <code>Ctrl-C</code></li>
</ul>
<h4 id="file--project-tools">File &amp; Project Tools</h4>
<ul>
<li><strong>FZF file search</strong>: <code>,f</code></li>
<li><strong>FZF git files</strong>: <code>,g</code></li>
<li><strong>NERDTree</strong>: <code>:NERDTreeToggle</code></li>
<li><strong>Buffers</strong>: <code>:ls</code> to list, <code>:b#</code> to switch</li>
</ul>
<h4 id="git-integration-vim-fugitive">Git Integration (vim-fugitive)</h4>
<ul>
<li><code>,gs</code> → git status</li>
<li><code>,gd</code> → git diff</li>
<li><code>,gc</code> → git commit</li>
<li><code>,gb</code> → git blame</li>
</ul>
<h4 id="undo-tree">Undo Tree</h4>
<ul>
<li><code>,u</code> → toggle undo history</li>
</ul>
<h4 id="tips">Tips</h4>
<ul>
<li><strong>Save as sudo</strong>:<br>
<code>:w !sudo tee % &gt; /dev/null</code></li>
<li><strong>Search/replace in file</strong>:<br>
<code>:%s/old/new/gc</code></li>
<li><strong>Delete all lines not matching</strong>:<br>
<code>:v/pattern/d</code></li>
</ul>
<h4 id="macros">Macros</h4>
<ul>
<li><strong>Record</strong>: <code>qa</code> → do stuff → <code>q</code></li>
<li><strong>Play</strong>: <code>@a</code></li>
<li><strong>Repeat last</strong>: <code>@@</code></li>
<li><strong>Run N times</strong>: <code>15@a</code></li>
</ul>
<h2 id="a-few-power-moves">A few power moves</h2>
<ul>
<li>
<p><strong>Save without sudo?</strong><br>
<code>:w !sudo tee % &gt;/dev/null</code></p>
</li>
<li>
<p><strong>Insert command output</strong><br>
<code>:r !date</code></p>
</li>
<li>
<p><strong>Search &amp; replace across project</strong><br>
<code>:args **/*.py</code> → <code>:argdo %s/old/new/ge | update</code></p>
</li>
<li>
<p><strong>Filter selection through shell</strong><br>
<code>:'&lt;,'&gt;!sort -u</code></p>
</li>
<li>
<p><strong>Visual block insert</strong><br>
<code>Ctrl-v → Itext → Esc</code></p>
</li>
<li>
<p><strong>Jump to mark</strong><br>
<code>mA</code> to set, <code>'A</code> to go</p>
</li>
<li>
<p><strong>Ripgrep + quickfix</strong><br>
<code>:grep -R &quot;needle&quot; .</code> → <code>:copen</code></p>
</li>
<li>
<p><strong>Diff two files</strong><br>
<code>:w | vnew other.txt | diffthis | wincmd p | diffthis</code></p>
</li>
<li>
<p><strong>Git in-place</strong><br>
<code>,gs</code> status · <code>,gd</code> diff · <code>,gc</code> commit</p>
</li>
<li>
<p><strong>Undo history tree</strong><br>
<code>,u</code></p>
</li>
</ul>
<br>
<p>Combine with my article, <a href="/posts/espanso-and-friends/">Espanso and Friends</a>
for real life Street Fighter combos.</p>
<br>
<h2 id="feedback">Feedback</h2>
<p>Got a vim tip or trick? Email me: <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>iperfer</title>
      <link>https://adminjitsu.com/posts/iperfer/</link>
      <pubDate>Thu, 07 Aug 2025 00:00:00 +0000</pubDate>
      <guid>https://adminjitsu.com/posts/iperfer/</guid>
      <description>iperfer.sh is a minimal shell wrapper that automates remote iperf3 tests via SSH, so you can benchmark bandwidth without manual setup or cleanup.</description>
      <content:encoded><![CDATA[<h2 id="what-is-iperfer">What is <code>iperfer</code>?</h2>
<p>I got tired of the usual <code>iperf3</code> song and dance.</p>
<p>You know the drill: SSH into the remote box, start the server, flip back to your machine, run the test, then go back and kill the server—hoping you remembered to clean up the process. All that hassle just to check how your LAN or VPN is holding up.</p>
<p>So I wrote a helper.</p>
<p><strong><code>iperfer.sh</code></strong> is a tiny shell script that automates remote-side setup for <code>iperf3</code> bandwidth tests. It:</p>
<ul>
<li>Connects to the remote host via SSH</li>
<li>Launches <code>iperf3</code> in server mode (backgrounded and PID-tracked)</li>
<li>Runs a local client test with parallel streams</li>
<li>Cleans up the remote server process afterward</li>
</ul>
<p>It even checks if <code>iperf3</code> is installed on the remote side before trying. One command. One step. No leftovers.</p>
<br>
<h2 id="-script-highlights">🧱 Script Highlights</h2>
<ul>
<li>Auto-starts <code>iperf3 -s</code> on the target host over SSH</li>
<li>Tracks the PID and cleans up after the test</li>
<li>Runs a 4-stream client test with customizable duration</li>
<li>Leaves no background processes or temp files behind</li>
<li>Written in portable Bash with safe defaults (<code>set -euo pipefail</code>)</li>
</ul>
<br>
<h2 id="-usage">⚙️ Usage</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">iperfer.sh &lt;host&gt; <span class="o">[</span>user<span class="o">]</span> <span class="o">[</span>duration<span class="o">]</span>
</span></span></code></pre></div><ul>
<li>If <code>[user]</code> is omitted, it defaults to your current <code>$USER</code></li>
<li>If <code>[duration]</code> is omitted, it defaults to <strong>10 seconds</strong></li>
<li>The test runs 4 parallel streams and outputs standard <code>iperf3</code> stats</li>
</ul>
<h3 id="example">Example</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">iperfer.sh gir forfaxx <span class="m">15</span>
</span></span></code></pre></div><p>That runs a 15-second test to <code>gir</code> as user <code>forfaxx</code>, then stops the remote server.</p>
<br>
<h2 id="-installing-iperfersh">📄 Installing <code>iperfer.sh</code></h2>
<p>Save the script below to <code>~/bin/iperfer.sh</code> (or anywhere in your <code>$PATH</code>), then make it executable:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">chmod +x ~/bin/iperfer.sh
</span></span></code></pre></div><p>Make sure the script is present on both your local and remote machines in a consistent location so SSH can find it.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="c1">#</span>
</span></span><span class="line"><span class="cl"><span class="c1"># iperfer.sh — Run an iperf3 test over SSH by auto-starting a remote server</span>
</span></span><span class="line"><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Usage:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   iperfer.sh &lt;target-host&gt; [remote-user] [duration]</span>
</span></span><span class="line"><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Example:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   iperfer.sh gir grumble 15</span>
</span></span><span class="line"><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Description:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   Starts iperf3 in server mode on the target over SSH,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   runs a parallelized client test locally,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   then shuts down the server process cleanly.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Author: forfaxx @ adminjitsu.com</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl"><span class="nv">IFS</span><span class="o">=</span><span class="s1">$&#39;\n\t&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">usage<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  cat <span class="s">&lt;&lt;EOF
</span></span></span><span class="line"><span class="cl"><span class="s">Usage: $0 &lt;target-host&gt; [remote-user] [duration]
</span></span></span><span class="line"><span class="cl"><span class="s">Runs iperf3 test from this machine to &lt;target-host&gt; using SSH to start/stop the server.
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">Defaults:
</span></span></span><span class="line"><span class="cl"><span class="s">  remote-user = $USER
</span></span></span><span class="line"><span class="cl"><span class="s">  duration    = 10 seconds
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">  <span class="nb">exit</span> <span class="m">1</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">run_iperf_test<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> <span class="nv">target</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> <span class="nv">user</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">2</span><span class="k">:-</span><span class="nv">$USER</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> <span class="nv">duration</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">3</span><span class="k">:-</span><span class="nv">10</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;🔍 Checking if iperf3 is installed on </span><span class="nv">$target</span><span class="s2">...&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> ! ssh <span class="s2">&#34;</span><span class="nv">$user</span><span class="s2">@</span><span class="nv">$target</span><span class="s2">&#34;</span> <span class="s2">&#34;command -v iperf3 &gt;/dev/null 2&gt;&amp;1&#34;</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;❌ iperf3 is not installed on </span><span class="nv">$target</span><span class="s2">. Aborting.&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;🔄 Starting iperf3 server on </span><span class="nv">$target</span><span class="s2">...&#34;</span>
</span></span><span class="line"><span class="cl">  ssh <span class="s2">&#34;</span><span class="nv">$user</span><span class="s2">@</span><span class="nv">$target</span><span class="s2">&#34;</span> <span class="s2">&#34;nohup iperf3 -s &gt; /dev/null 2&gt;&amp;1 &amp; echo \$! &gt; /tmp/.iperf3.pid&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  sleep <span class="m">1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;📡 Running iperf3 client to </span><span class="nv">$target</span><span class="s2"> for </span><span class="nv">$duration</span><span class="s2"> seconds...&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> ! iperf3 -c <span class="s2">&#34;</span><span class="nv">$target</span><span class="s2">&#34;</span> -t <span class="s2">&#34;</span><span class="nv">$duration</span><span class="s2">&#34;</span> -P 4<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;⚠️  iperf3 test failed&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;🛑 Stopping iperf3 server on </span><span class="nv">$target</span><span class="s2">...&#34;</span>
</span></span><span class="line"><span class="cl">  ssh <span class="s2">&#34;</span><span class="nv">$user</span><span class="s2">@</span><span class="nv">$target</span><span class="s2">&#34;</span> <span class="s2">&#34;kill \$(cat /tmp/.iperf3.pid) 2&gt;/dev/null || true &amp;&amp; rm -f /tmp/.iperf3.pid&#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">main<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="nv">$#</span> -lt <span class="m">1</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    usage
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">  run_iperf_test <span class="s2">&#34;</span><span class="nv">$@</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">main <span class="s2">&#34;</span><span class="nv">$@</span><span class="s2">&#34;</span>
</span></span></code></pre></div><h2 id="final-thoughts">Final Thoughts</h2>
<p>It&rsquo;s a small thing, but being able to spin up a remote <code>iperf3</code> test in one step has saved me tons of clicks and mental overhead.</p>
<p>More than that, the pattern of <strong>starting remote processes over SSH and tracking their PIDs for cleanup</strong> has proven useful in other scripts too. <code>iperfer.sh</code> just happens to be where I reached for it first.</p>
<p>If you try it or adapt the pattern for something else, I’d love to hear about it. Feedback and ideas always welcome: <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Hero Commands</title>
      <link>https://adminjitsu.com/posts/hero-commands/</link>
      <pubDate>Mon, 04 Aug 2025 21:46:16 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/hero-commands/</guid>
      <description>One-liner Linux &amp;amp; Mac superpowers for real-world Unix problem solving</description>
      <content:encoded><![CDATA[<figure style="text-align:center; margin: 1em auto;">
  <img src="hero-banner.png" 
       alt="an ascii art banner that says HERO COMMANDS produced by figlet" 
       style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
<br>
I once worked with a woman named Mirya who was, without question, the best Unix admin I’ve ever met. She could rattle off complex pipelines and write `awk` and `sed` scripts from memory, pulling off shockingly powerful feats with *apparent* ease. I asked her for a copy of her shell history once and studied it like it was Merlin’s spellbook. Ever since, I’ve been collecting history files, Perl notebooks, shell one-liners, and plenty of my own—the kind of arcane knowledge that can turn you into a command-line hero.
<h2 id="-path-walk">🧙‍♂️ Path Walk</h2>
<p>Sometimes you need to troubleshoot the permissions on the whole directory path from your current working directory all the way back to <code>/</code>. Compiling that report manually can really fill up the &lsquo;swear jar&rsquo; quickly. Luckily this path walk command makes it easy to see it all together so you can focus on the important stuff.</p>
<ul>
<li>
<p><strong>macOS</strong></p>
<p>Shows permissions, ACLs and xattrs — all in one command.</p>
</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">while</span> <span class="o">[</span> <span class="nv">$PWD</span> !<span class="o">=</span> / <span class="o">]</span><span class="p">;</span> <span class="k">do</span> ls -aled@ <span class="sb">`</span><span class="nb">pwd</span><span class="sb">`</span><span class="p">;</span> <span class="nb">cd</span> ..<span class="p">;</span> <span class="k">done</span><span class="p">;</span> ls -aled@ <span class="sb">`</span><span class="nb">pwd</span><span class="sb">`</span>  
</span></span></code></pre></div><ul>
<li>
<p><strong>Linux</strong></p>
<p>While not as pretty as macOS ls, we can achieve similar results with the POSIX / Linux equivalent command:</p>
</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">while</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">&#34;</span> !<span class="o">=</span> <span class="s2">&#34;/&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">do</span> echo<span class="p">;</span> <span class="nb">echo</span> <span class="s2">&#34;--- </span><span class="nv">$PWD</span><span class="s2"> ---&#34;</span><span class="p">;</span> ls -ld <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">&#34;</span><span class="p">;</span> getfacl -p <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">&#34;</span> 2&gt;/dev/null<span class="p">;</span> getfattr -d <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">&#34;</span> 2&gt;/dev/null<span class="p">;</span> <span class="nb">cd</span> ..<span class="p">;</span> <span class="k">done</span><span class="p">;</span> echo<span class="p">;</span> <span class="nb">echo</span> <span class="s2">&#34;--- </span><span class="nv">$PWD</span><span class="s2"> ---&#34;</span><span class="p">;</span> ls -ld <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">&#34;</span><span class="p">;</span> getfacl -p <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">&#34;</span> 2&gt;/dev/null<span class="p">;</span> getfattr -d <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">&#34;</span> 2&gt;/dev/null
</span></span></code></pre></div><p>This will produce output like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">--- /home/grumble/codelab/adminjitsu ---
</span></span><span class="line"><span class="cl">drwxr-xr-x - grumble  <span class="m">4</span> Aug 10:39 /home/grumble/codelab/adminjitsu
</span></span><span class="line"><span class="cl"><span class="c1"># file: /home/grumble/codelab/adminjitsu</span>
</span></span><span class="line"><span class="cl"><span class="c1"># owner: grumble</span>
</span></span><span class="line"><span class="cl"><span class="c1"># group: grumble</span>
</span></span><span class="line"><span class="cl">user::rwx
</span></span><span class="line"><span class="cl">group::r-x
</span></span><span class="line"><span class="cl">other::r-x
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">--- /home/grumble/codelab ---
</span></span><span class="line"><span class="cl">drwxr-xr-x - grumble  <span class="m">4</span> Aug 08:49 /home/grumble/codelab
</span></span><span class="line"><span class="cl"><span class="c1"># file: /home/grumble/codelab</span>
</span></span><span class="line"><span class="cl"><span class="c1"># owner: grumble</span>
</span></span><span class="line"><span class="cl"><span class="c1"># group: grumble</span>
</span></span><span class="line"><span class="cl">user::rwx
</span></span><span class="line"><span class="cl">group::r-x
</span></span><span class="line"><span class="cl">other::r-x
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">--- /home/grumble ---
</span></span><span class="line"><span class="cl">drwxr-x--- - grumble  <span class="m">4</span> Aug 20:54 /home/grumble
</span></span><span class="line"><span class="cl"><span class="c1"># file: /home/grumble</span>
</span></span><span class="line"><span class="cl"><span class="c1"># owner: grumble</span>
</span></span><span class="line"><span class="cl"><span class="c1"># group: grumble</span>
</span></span><span class="line"><span class="cl">user::rwx
</span></span><span class="line"><span class="cl">group::r-x
</span></span><span class="line"><span class="cl">other::---
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">--- /home ---
</span></span><span class="line"><span class="cl">drwxr-xr-x - root  <span class="m">9</span> Oct  <span class="m">2024</span> /home
</span></span><span class="line"><span class="cl"><span class="c1"># file: /home</span>
</span></span><span class="line"><span class="cl"><span class="c1"># owner: root</span>
</span></span><span class="line"><span class="cl"><span class="c1"># group: root</span>
</span></span><span class="line"><span class="cl">user::rwx
</span></span><span class="line"><span class="cl">group::r-x
</span></span><span class="line"><span class="cl">other::r-x
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">--- / ---
</span></span><span class="line"><span class="cl">drwxr-xr-x - root  <span class="m">2</span> Aug 16:38 /
</span></span><span class="line"><span class="cl"><span class="c1"># file: /</span>
</span></span><span class="line"><span class="cl"><span class="c1"># owner: root</span>
</span></span><span class="line"><span class="cl"><span class="c1"># group: root</span>
</span></span><span class="line"><span class="cl">user::rwx
</span></span><span class="line"><span class="cl">group::r-x
</span></span><span class="line"><span class="cl">other::r-x
</span></span></code></pre></div><h3 id="bash-function"><strong>Bash Function</strong></h3>
<p>Since these commands do not require arguments, they make great Bash, alias-like functions. The following will do the same thing with a single command. You just need to add the following to your startup files (e.g., <code>.bashrc</code>,<code>.zshrc</code>) and reload. As a script, it demonstrates a nice pattern for using <code>pushd</code> and <code>popd</code> to perform a sequence of actions and then return to the original directory.</p>
<h3 id="macos-version"><em>macOS Version</em></h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pathwalk<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">pushd</span> . &gt; /dev/null
</span></span><span class="line"><span class="cl">  <span class="k">while</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">&#34;</span> !<span class="o">=</span> <span class="s2">&#34;/&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    ls -aled@ <span class="s2">&#34;</span><span class="k">$(</span><span class="nb">pwd</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">cd</span> ..
</span></span><span class="line"><span class="cl">  <span class="k">done</span>
</span></span><span class="line"><span class="cl">  ls -aled@ <span class="s2">&#34;</span><span class="k">$(</span><span class="nb">pwd</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">popd</span> &gt; /dev/null
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><h3 id="linux-version"><em>Linux Version</em></h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pathwalk<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">pushd</span> . &gt; /dev/null
</span></span><span class="line"><span class="cl">  <span class="k">while</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">&#34;</span> !<span class="o">=</span> <span class="s2">&#34;/&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;--- </span><span class="nv">$PWD</span><span class="s2"> ---&#34;</span>
</span></span><span class="line"><span class="cl">    ls -ld <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    getfacl -p <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">&#34;</span> 2&gt;/dev/null
</span></span><span class="line"><span class="cl">    getfattr -d <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">&#34;</span> 2&gt;/dev/null
</span></span><span class="line"><span class="cl">    <span class="nb">cd</span> ..
</span></span><span class="line"><span class="cl">  <span class="k">done</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;--- </span><span class="nv">$PWD</span><span class="s2"> ---&#34;</span>
</span></span><span class="line"><span class="cl">  ls -ld <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  getfacl -p <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">&#34;</span> 2&gt;/dev/null
</span></span><span class="line"><span class="cl">  getfattr -d <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">&#34;</span> 2&gt;/dev/null
</span></span><span class="line"><span class="cl">  <span class="nb">popd</span> &gt; /dev/null
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><h2 id="-diff-a-remote-file">🜔 Diff a remote file</h2>
<p>Sometimes you need to compare a local file with a remote one. Copying files to your machine via scp gets old fast. That&rsquo;s where this simple but not completely obvious command comes in.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh user@host cat /path/to/remotefile <span class="p">|</span> diff /path/to/localfile -
</span></span></code></pre></div><ul>
<li>This command streams the contents of the <em>remote</em> file over SSH.</li>
<li>Pipes that remote file into <code>diff</code> using <code>-</code> as an argument to mean &ldquo;read from stdin&rdquo;</li>
<li>Compares the local file (<code>/path/to/localfile</code>) against the remote — <em>not temp files, no manual copies, no extra cleanup.</em></li>
<li>Use sudo if required, ala <code>ssh user@host sudo cat /etc/somefile | diff /path/to/localfile -</code></li>
</ul>
<p>An example: <code>ssh user@host sudo cat /etc/somefile | diff /path/to/localfile -</code></p>
<p><span class="tag blue">Pro-tip</span> Use <code>colordiff</code> for pretty output</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh host <span class="s1">&#39;openssl x509 -in /etc/ssl/cert.pem -text&#39;</span> <span class="p">|</span> diff cert.txt -
</span></span></code></pre></div><h2 id="-navigation-power-moves">🝰 Navigation power moves</h2>
<blockquote>
<p>“Technology is a word that describes something that doesn’t work yet.”
— Douglas Adams</p></blockquote>
<p>There are a few basic commands that really help when you find yourself jumping between directories (web development projects for instance)</p>
<ul>
<li>
<p><strong>Jump to previous directory</strong></p>
<p>No matter how twisty and deep the path, <code>cd -</code> lets you jump to your previous folder and back if you run it again.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># toggles between your current and previous directory</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> -
</span></span></code></pre></div></li>
<li>
<p><strong>Pushd, Popd and Dirs</strong></p>
<p>For when you want to bookmark your current spot before diving into another directory</p>
<ul>
<li>
<p><code>pushd &lt;dir&gt;</code>: saves your current location and jumps to <dir></p>
</li>
<li>
<p><code>popd</code>: Jumps back to when you last used <code>pushd</code> (and removes that location from the stack)</p>
</li>
<li>
<p>Use these to create a stack of locations for more complex navigation</p>
</li>
</ul>
<p><strong>Example</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">pushd</span> /etc
</span></span><span class="line"><span class="cl"><span class="c1"># Do stuff in /etc</span>
</span></span><span class="line"><span class="cl"><span class="nb">pushd</span> /var/log
</span></span><span class="line"><span class="cl"><span class="c1"># Do stuff in /var/log</span>
</span></span><span class="line"><span class="cl"><span class="nb">popd</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Back to /etc</span>
</span></span><span class="line"><span class="cl"><span class="nb">popd</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Back to where you started</span>
</span></span></code></pre></div><p><span class="tag blue">Pro-tip</span> You can use the <code>dirs</code> command to view the locations stack</p>
<p>Using these tools you can (somewhat) eliminate the tedium of typing /annoyingly/long/paths</p>
</li>
</ul>
<figure style="text-align:center; margin: 1em auto;">
  <img src="flamethrower-skeleton.png" alt="A skeleton wielding a flamethrower" style="display:block; margin:0 auto; max-width:500px;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <em>hero commands: because you never know what kind of monsters you'll encounter</em>
  </figcaption>
</figure>
<h2 id="-python-tools">🐍 Python tools</h2>
<p>There are a couple of fantastic tools that &ldquo;come with&rdquo; Python and let you do magical things easily. I use them all the time!</p>
<h3 id="1-validate--pretty-print-json">1. Validate &amp; Pretty-print JSON</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;{&#34;json&#34;:&#34;obj&#34;}&#39;</span> <span class="p">|</span> python -mjson.tool
</span></span><span class="line"><span class="cl"><span class="c1"># Or with files:</span>
</span></span><span class="line"><span class="cl">python -mjson.tool &lt; ugly.json &gt; pretty.json
</span></span></code></pre></div><ul>
<li>Validates JSON (throws error if invalid).</li>
<li>Indents and formats output for humans.</li>
<li>Saves the day ocassionally</li>
</ul>
<hr>
<h3 id="2-http-client--web-testing">2. HTTP Client &amp; Web Testing</h3>
<h4 id="simple-http-getpost-from-the-command-line">Simple HTTP GET/POST from the command line:</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">python -m http.client HOST
</span></span><span class="line"><span class="cl"><span class="c1"># Interactive HTTP client (rarely used, but there)</span>
</span></span></code></pre></div><h4 id="quickly-start-a-web-server-python-3">Quickly start a web server (Python 3):</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">python -m http.server <span class="m">8000</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Serves current directory on whatever port you specify (8000 in this example)</span>
</span></span></code></pre></div><ul>
<li><em>Great for quick file sharing or local dev.</em></li>
</ul>
<h4 id="run-a-cgi-server-python-3">Run a CGI server (Python 3):</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">python -m http.server --cgi
</span></span><span class="line"><span class="cl"><span class="c1"># Runs scripts in cgi-bin/</span>
</span></span></code></pre></div><h4 id="listen-on-a-specific-interface">Listen on a specific interface</h4>
<p>You can bind to localhost only for instances with the <code>--bind</code> argument, like so →</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">python -m http.server <span class="m">8000</span> --bind 127.0.0.1
</span></span></code></pre></div><hr>
<h3 id="3-encodedecode-base64">3. Encode/Decode Base64</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;hello&#39;</span> <span class="p">|</span> python -m base64
</span></span><span class="line"><span class="cl"><span class="c1"># Or:</span>
</span></span><span class="line"><span class="cl">python -m base64 -d &lt; encoded.txt &gt; decoded.bin
</span></span></code></pre></div><ul>
<li>No need for <code>base64</code> utility, works everywhere Python is installed.</li>
</ul>
<h3 id="4-compressdecompress-files">4. Compress/Decompress Files</h3>
<ul>
<li><strong>gzip:</strong>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">python -m gzip -d file.gz
</span></span><span class="line"><span class="cl">python -m gzip file
</span></span></code></pre></div></li>
<li><strong>zip:</strong>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">python -m zipfile -l my.zip    <span class="c1"># List contents</span>
</span></span><span class="line"><span class="cl">python -m zipfile -e my.zip .  <span class="c1"># Extract here</span>
</span></span><span class="line"><span class="cl">python -m zipfile -c out.zip file1 file2  <span class="c1"># Create zip</span>
</span></span></code></pre></div></li>
</ul>
<h3 id="5-simple-file-serving">5. Simple File Serving</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">python -m http.server  <span class="c1"># Python 3.x</span>
</span></span><span class="line"><span class="cl">python -m SimpleHTTPServer  <span class="c1"># Python 2.x</span>
</span></span></code></pre></div><ul>
<li>Dead simple “share this folder” in seconds.</li>
</ul>
<hr>
<h2 id="-empty-a-file">␀ Empty a file</h2>
<blockquote>
<p><em>“The usefulness of a pot comes from its emptiness.”</em></p>
<p><em>— Lao Tzu, Tao Te Ching</em></p></blockquote>
<p>I frequently want to zero out test files that I am working with. The following are a few ways to do it, easily and safely</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Redirects &#34;nothing&#34; into the file</span>
</span></span><span class="line"><span class="cl">&gt;filename
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># or use truncate. Sometimes you can&#39;t use &gt;filename and that&#39;s where these less memorable commands come in</span>
</span></span><span class="line"><span class="cl">truncate -s <span class="m">0</span> filename
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># you can also do the following just make sure to wrap the comand in single quotes or it will usually fail</span>
</span></span><span class="line"><span class="cl">sudo bash -c <span class="s1">&#39;&gt; filename&#39;</span>
</span></span></code></pre></div><br>
<h2 id="-make-ps-output-easier-to-read">📚 Make ps output easier to read</h2>
<blockquote>
<p><em>&ldquo;If you gaze long into <code>/dev/null</code>, <code>/dev/null</code> gazes into you.</em></p>
<p><em>- Friedrich Nietzsche, if he were a sysadmin</em></p></blockquote>
<p>I covered this in more detail in another post, <a href="/posts/ps-for-spelunker">ps-for-spelunkers</a></p>
<p>Basically, you can insert a blank line (or more than one) between each process in the output. This is <strong>super</strong> useful when you&rsquo;re trying to make sense of processes with a lot of switches like Java, Databases and Webservers.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;</span><span class="k">$(</span>ps aux<span class="k">)</span><span class="s2">&#34;</span> <span class="p">|</span> awk <span class="s1">&#39;{print;} NR % 1 == 0 {print&#34;&#34;;}&#39;</span>
</span></span></code></pre></div><h3 id="examples">Examples</h3>
<ul>
<li><code>ls -l | awk '{print;} NR % 1 == 0 {print&quot;&quot;;}'</code></li>
<li><code>cat bigfile.txt | awk '{print;} NR % 1 == 0 {print&quot;&quot;;}'</code></li>
</ul>
<h3 id="more-than-one-blank-line">More than one blank line</h3>
<p>Just change the modulus in the command:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">awk <span class="s1">&#39;{print;} NR % 1 == 0 {print&#34;&#34;;} NR % 3 == 0 {print&#34;&#34;;}&#39;</span>
</span></span></code></pre></div><p><span class="tag blue">Pro-tip</span> Specifying just the columns you are interested in conjunction with the awk command makes it even easier to read. <code>ps -eo pid,user,args</code></p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="purple-wizard.png" alt="a purple wizard" style="display:block; margin:0 auto; max-width:800px;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <em>Sometimes good guys don't wear white</em>
  </figcaption>
</figure>
<h2 id="-awk-print-from-a-given-column-to-end-of-line">⇨ Awk: print from a given column to end of line</h2>
<p>I had to work with some logs that had optional columns and found myself needing a way to print from a column I wanted to the end of the line. Instead of making a bunch of custom commands for each case, I turned to our old pal, Awk.</p>
<blockquote>
<p>Fun fact - AWK is named for its creators, Alfred <strong>A</strong>ho, Peter <strong>W</strong>einberger, and Brian <strong>K</strong>ernighan</p></blockquote>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-awk" data-lang="awk"><span class="line"><span class="cl"><span class="nx">awk</span> <span class="s1">&#39;{for(i=4;i&lt;=NF;++i) printf &#34;%s%s&#34;, $i, (i&lt;NF?OFS:ORS)}&#39;</span>
</span></span></code></pre></div><p>You can customize the command easily:</p>
<ul>
<li>i=4 means start at the 4th field (adjust as needed).</li>
<li>OFS is output field separator (default: space).</li>
<li>ORS is output record separator (default: newline).</li>
</ul>
<p><strong>Example Usage</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat opendirectoryd.log <span class="p">|</span> awk <span class="s1">&#39;{for(i=4;i&lt;=NF;++i) printf &#34;%s%s&#34;, $i, (i&lt;NF?OFS:ORS)}&#39;</span>
</span></span></code></pre></div><br>
<h2 id="-get-number-of-cpu-cores">⌬ Get number of CPU cores</h2>
<p>If you ever need to fetch the number of CPU cores on a machine from a script, you can turn to this trusty cross-platform command:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">getconf _NPROCESSORS_ONLN
</span></span></code></pre></div><p>On my 6-core, hyperthreaded Mac, this returns 12.</p>
<p>That comes in handy for things like a yes stress tester</p>
<p><span class="tag red">Warning</span> this will max out your processor and produce maximum heat which you probably don&rsquo;t want to do unless you&rsquo;re testing your cooling setup. Although if you are testing cooling, then you&rsquo;re welcome!</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">for</span> i in <span class="k">$(</span>seq <span class="m">1</span> <span class="k">$(</span>getconf _NPROCESSORS_ONLN<span class="k">))</span><span class="p">;</span> <span class="k">do</span> yes &gt; /dev/null <span class="p">&amp;</span> <span class="k">done</span>
</span></span></code></pre></div><p>When you&rsquo;re ready to stop the yes stress test, run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">killall yes
</span></span></code></pre></div><p>of if you happen to be on a system without <code>killall</code> you can run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pkill -9 yes
</span></span></code></pre></div><h2 id="-a-listeners-function">⟡ A listeners function</h2>
<p>This is a nice and simple way to display your listening processes and ports. I keep this on all my machines; it’s my go-to for quickly reminding myself what’s listening. Just add the following to your shell session or put it in your startup files (e.g., <code>.bashrc</code>, <code>.zshrc</code>)</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">listeners<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  sudo lsof -nP -iTCP -sTCP:LISTEN <span class="p">|</span>
</span></span><span class="line"><span class="cl">  awk <span class="s1">&#39;BEGIN {
</span></span></span><span class="line"><span class="cl"><span class="s1">          format = &#34;%-16s %-8s %-22s %-10s\n&#34;
</span></span></span><span class="line"><span class="cl"><span class="s1">          printf format, &#34;[PROC]&#34;, &#34;[PID]&#34;, &#34;[PORT]&#34;, &#34;[ACCOUNT]&#34;
</span></span></span><span class="line"><span class="cl"><span class="s1">          printf format, &#34;------&#34;, &#34;----&#34;, &#34;------&#34;, &#34;--------&#34;
</span></span></span><span class="line"><span class="cl"><span class="s1">      }
</span></span></span><span class="line"><span class="cl"><span class="s1">      NR &gt; 1 {printf format, $1, $2, $9, $3}&#39;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><p>Getting the quoting right is tough, but you can also run it as a one-liner over ssh if you&rsquo;re feeling kinky.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh user@host <span class="s2">&#34;sudo lsof -nP -iTCP -sTCP:LISTEN | awk &#39;BEGIN {format = \&#34;%-16s %-8s %-22s %-10s\\n\&#34;; printf format, \&#34;[PROC]\&#34;, \&#34;[PID]\&#34;, \&#34;[PORT]\&#34;, \&#34;[ACCOUNT]\&#34;; printf format, \&#34;------\&#34;, \&#34;----\&#34;, \&#34;------\&#34;, \&#34;--------\&#34;} NR &gt; 1 {printf format, \$1, \$2, \$9, \$3}&#39;&#34;</span>
</span></span></code></pre></div><p>Example output:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">┌──<span class="o">[</span> forfaxx@shinobi <span class="o">]</span>:~/codelab/adminjitsu  <span class="o">(</span>main*<span class="o">)</span>
</span></span><span class="line"><span class="cl">└─$ listeners
</span></span><span class="line"><span class="cl"><span class="o">[</span>PROC<span class="o">]</span>           <span class="o">[</span>PID<span class="o">]</span>    <span class="o">[</span>PORT<span class="o">]</span>                 <span class="o">[</span>ACCOUNT<span class="o">]</span>
</span></span><span class="line"><span class="cl">------           ----     ------                 --------
</span></span><span class="line"><span class="cl">systemd          <span class="m">1</span>        *:111                  root
</span></span><span class="line"><span class="cl">systemd          <span class="m">1</span>        *:111                  root
</span></span><span class="line"><span class="cl">systemd          <span class="m">1</span>        <span class="o">[</span>::1<span class="o">]</span>:2947             root
</span></span><span class="line"><span class="cl">systemd          <span class="m">1</span>        127.0.0.1:2947         root
</span></span><span class="line"><span class="cl">rpcbind          <span class="m">208</span>      *:111                  _rpc
</span></span><span class="line"><span class="cl">rpcbind          <span class="m">208</span>      *:111                  _rpc
</span></span><span class="line"><span class="cl">sshd             <span class="m">334</span>      *:22                   root
</span></span><span class="line"><span class="cl">sshd             <span class="m">334</span>      *:22                   root
</span></span><span class="line"><span class="cl">node             <span class="m">940</span>      127.0.0.1:46267        forfaxx
</span></span><span class="line"><span class="cl">hugo             <span class="m">315471</span>   127.0.0.1:1313         forfaxx
</span></span></code></pre></div><h2 id="conclusion">Conclusion</h2>
<p>That&rsquo;s it for now but more to come !
Have a hero command of your own? I&rsquo;d love to hear about it! Email me: <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Rotating Backups Skeleton</title>
      <link>https://adminjitsu.com/posts/rotating-backups/</link>
      <pubDate>Mon, 04 Aug 2025 08:48:29 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/rotating-backups/</guid>
      <description>Easily manage daily, weekly, and monthly backups with this clean, modular Bash script. Includes built-in integrity checks and systemd timer integration. Perfect for home servers or minimal sysadmin setups.</description>
      <content:encoded><![CDATA[<h2 id="what-is-rotate-backupssh">What is <code>rotate-backups.sh</code>?</h2>
<figure style="text-align:center; margin: 1em auto;">
  <img src="skeletons.jpg" alt="two pixel skeletons dancing" style="display:block; margin:0 auto; max-width:500px;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <em>Just the bare bones, man.</em>
  </figcaption>
</figure>
<p>A <em>no-frills</em>, <em>plug-and-play</em> <strong>backup rotation script</strong> for small setups, homelabs, or anywhere you want clean dailies, weeklies, and monthlies—without third-party tools or magic wrappers. Inspired by the Unix philosophy: simple, composable, inspectable.</p>
<p>The setup includes one Bash script and two systemd units: a *.service to define the job and a *.timer to schedule it. The goal is elegant, self-contained rotation so that when disaster strikes, you’ve got multiple recovery points.</p>
<p>By default, the timer runs at local midnight (00:00). If the machine is offline during that window, the Persistent=true flag ensures the job runs as soon as the system comes back online. The rotation logic keeps 7 daily backups, and from those, promotes 3 weekly (every Sunday) and 3 monthly (on the 1st), pruning anything older.</p>
<h2 id="quick-start">Quick Start</h2>
<ul>
<li>📦 Requires: <code>bash</code>, <code>tar</code>, <code>sha256sum</code>, <code>systemd</code></li>
<li>🔧 Customize your source/target paths, tweak the retention rules, and you&rsquo;re set.</li>
</ul>
<p>Install the script to <code>/usr/local/bin</code> and drop in the <code>*.service</code> and <code>*.timer</code> files. Done.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Copy and enable</span>
</span></span><span class="line"><span class="cl">sudo cp rotate-backups.sh /usr/local/bin/
</span></span><span class="line"><span class="cl">sudo chmod +x /usr/local/bin/rotate-backups.sh
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Prepare and enable systemd scripts</span>
</span></span><span class="line"><span class="cl">sudo cp rotate-backups.service /etc/systemd/system/
</span></span><span class="line"><span class="cl">sudo cp rotate-backups.timer /etc/systemd/system/
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">sudo systemctl daemon-reexec
</span></span><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> --now rotate-backups.timer
</span></span></code></pre></div><h2 id="features">Features</h2>
<p>Keeps:</p>
<ul>
<li>
<p>7 daily backups</p>
</li>
<li>
<p>3 weekly backups (Sundays)</p>
</li>
<li>
<p>3 monthly backups (1st of the month)</p>
</li>
<li>
<p>Uses compressed tarballs</p>
</li>
<li>
<p>Uses checksum validation (sha256sum)</p>
</li>
<li>
<p>Compatible with systemd timers</p>
</li>
<li>
<p>Human-readable logs and deletions</p>
</li>
</ul>
<h2 id="why-did-i-write-this">Why did I write this?</h2>
<p>This is one of those practices that makes a lot of sense but is kind of a pain to get right. I always end up digging through old notes or half-working scripts trying to remember how I did this last time&hellip;. I wanted to make a simple skeleton or stub that would be easy to modify and use for virtually any task.</p>
<p><span class="tag green">Pro-Tip:</span> Keep your backups mounted and reachable before your systemd timer runs!</p>
<h2 id="usage">Usage</h2>
<p>Before using the script, edit the configuration at the top to set your source and destination:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">BACKUP_SRC</span><span class="o">=</span><span class="s2">&#34;/var/docker&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">BACKUP_DEST</span><span class="o">=</span><span class="s2">&#34;/mnt/sldf/backups&#34;</span>
</span></span></code></pre></div><p>Once installed and enabled, the timer will automatically run the backup job once per day at midnight. You don’t need to call the script directly.</p>
<p>To check when it last ran, or see what happened during a run, use:</p>
<p><span class="tag green">Pro-Tip:</span> Want to test it manually? Run <code>sudo /usr/local/bin/rotate-backups.sh</code> and check the output folder with <code>ls -lh /mnt/sldf/backups/daily</code>.</p>
<p>You can also use <code>journalctl -u rotate-backups</code> to confirm logs after a systemd run.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo journalctl -u rotate-backups --no-pager
</span></span></code></pre></div><p>This shows the logs collected by systemd from previous runs of the backup job—including anything echoed by the script or errors that occurred.</p>
<p>To run the backup manually at any time:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemctl start rotate-backups.service
</span></span></code></pre></div><h3 id="so-what-if-im-not-using-systemd">So, what if I&rsquo;m not using systemd?</h3>
<p>Not all systems use <code>systemd</code>. Some distros (like Alpine) or platforms (like macOS) use different init systems. If that&rsquo;s you—no problem. The script works fine with <code>cron</code>, <code>launchd</code>, any scheduler that can run a Bash script daily.</p>
<h4 id="cron-example-linux"><strong>Cron Example (Linux)</strong></h4>
<p>If you are using cron, you can add the following to your root crontab (sudo crontab -e) to run it daily at 3:15AM:</p>
<pre tabindex="0"><code class="language-cron" data-lang="cron">15 3 * * * /usr/local/bin/rotate-backups.sh
</code></pre><h4 id="on-macos"><strong>On macOS</strong></h4>
<p>Here’s a simple <code>launchd</code> <code>.plist</code> that runs <code>rotate-backups.sh</code> once a day at 3:15 AM. This assumes the script is installed at <code>/usr/local/bin/rotate-backups.sh</code>.</p>
<p><span class="tag blue">Note:</span> Rename the file to match your domain or username (e.g., com.mydomain.rotate-backups.plist)</p>
<p><em>com.forfaxx.rotate-backups.plist</em></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="cp">&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;</span>
</span></span><span class="line"><span class="cl"><span class="cp">&lt;!DOCTYPE plist PUBLIC &#34;-//Apple//DTD PLIST 1.0//EN&#34; 
</span></span></span><span class="line"><span class="cl"><span class="cp">  &#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd&#34;&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;plist</span> <span class="na">version=</span><span class="s">&#34;1.0&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;dict&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>Label<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;string&gt;</span>com.forfaxx.rotate-backups<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>ProgramArguments<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;array&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;string&gt;</span>/usr/local/bin/rotate-backups.sh<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;/array&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>StartCalendarInterval<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;dict&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;key&gt;</span>Hour<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;integer&gt;</span>3<span class="nt">&lt;/integer&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;key&gt;</span>Minute<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;integer&gt;</span>15<span class="nt">&lt;/integer&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;/dict&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>StandardOutPath<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;string&gt;</span>/tmp/rotate-backups.out<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>StandardErrorPath<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;string&gt;</span>/tmp/rotate-backups.err<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;key&gt;</span>RunAtLoad<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;true/&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/dict&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/plist&gt;</span>
</span></span></code></pre></div><blockquote>
<h2 id="known-issues--gotchas">Known Issues &amp; Gotchas</h2>
<ul>
<li>⚠️ If your backup destination isn’t mounted, <code>tar</code> may write into an empty mountpoint (like <code>/mnt/sldf/backups</code>) and silently fill your root disk.</li>
<li>🔐 Make sure the script has permission to read everything in <code>BACKUP_SRC</code>.</li>
<li>🧩 On macOS, verify that <code>launchd</code> jobs persist across reboots by keeping the <code>.plist</code> in <code>~/Library/LaunchAgents/</code>.</li>
</ul></blockquote>
<p><strong>Installation Instructions</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p ~/Library/LaunchAgents
</span></span><span class="line"><span class="cl">cp com.forfaxx.rotate-backups.plist ~/Library/LaunchAgents/
</span></span><span class="line"><span class="cl">launchctl load ~/Library/LaunchAgents/com.forfaxx.rotate-backups.plist
</span></span></code></pre></div><ul>
<li>
<p>To <strong>unload</strong> simply run:
<code>launchctl unload ~/Library/LaunchAgents/com.forfaxx.rotate-backups.plist</code></p>
</li>
<li>
<p>To <strong>reload after edits</strong>:</p>
</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">launchctl unload ~/Library/LaunchAgents/com.forfaxx.rotate-backups.plist
</span></span><span class="line"><span class="cl">launchctl load ~/Library/LaunchAgents/com.forfaxx.rotate-backups.plist
</span></span></code></pre></div><h2 id="script-source">Script Source</h2>
<p>I am using the filename <code>rotate-backups.sh</code> but you can rename it to whatever makes sense in your situation (e.g., <code>website-backup.sh</code> or <code>finalproj-bak.sh</code>, etc)</p>
<h3 id="rotate-backupssh">rotate-backups.sh</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="c1"># rotate-backups.sh</span>
</span></span><span class="line"><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl"><span class="nv">IFS</span><span class="o">=</span><span class="s1">$&#39;\n\t&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">BACKUP_SRC</span><span class="o">=</span><span class="s2">&#34;/var/docker&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">BACKUP_DEST</span><span class="o">=</span><span class="s2">&#34;/mnt/sldf/backups&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">DAILY_KEEP</span><span class="o">=</span><span class="m">7</span>
</span></span><span class="line"><span class="cl"><span class="nv">WEEKLY_KEEP</span><span class="o">=</span><span class="m">3</span>
</span></span><span class="line"><span class="cl"><span class="nv">MONTHLY_KEEP</span><span class="o">=</span><span class="m">3</span>
</span></span><span class="line"><span class="cl"><span class="nv">STAMP</span><span class="o">=</span><span class="k">$(</span>date +%F<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">TODAY_DIR</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$BACKUP_DEST</span><span class="s2">/daily&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">WEEKLY_DIR</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$BACKUP_DEST</span><span class="s2">/weekly&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">MONTHLY_DIR</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$BACKUP_DEST</span><span class="s2">/monthly&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">CHECKSUM_FILE</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$BACKUP_DEST</span><span class="s2">/checksums.sha256&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">make_backup<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    mkdir -p <span class="s2">&#34;</span><span class="nv">$TODAY_DIR</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">local</span> <span class="nv">target</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$TODAY_DIR</span><span class="s2">/backup-</span><span class="nv">$STAMP</span><span class="s2">.tar.gz&#34;</span>
</span></span><span class="line"><span class="cl">    tar -czf <span class="s2">&#34;</span><span class="nv">$target</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$BACKUP_SRC</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    sha256sum <span class="s2">&#34;</span><span class="nv">$target</span><span class="s2">&#34;</span> &gt;&gt; <span class="s2">&#34;</span><span class="nv">$CHECKSUM_FILE</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">rotate<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="nb">local</span> <span class="nv">path</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">local</span> <span class="nv">keep</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$2</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    find <span class="s2">&#34;</span><span class="nv">$path</span><span class="s2">&#34;</span> -maxdepth <span class="m">1</span> -type f -name <span class="s1">&#39;*.tar.gz&#39;</span> <span class="p">|</span> sort -r <span class="p">|</span> tail -n +<span class="k">$((</span>keep+1<span class="k">))</span> <span class="p">|</span> xargs -r rm -v
</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">copy_if<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="nb">local</span> <span class="nv">src</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">local</span> <span class="nv">dest</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$2</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">local</span> <span class="nv">freq</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$3</span><span class="s2">&#34;</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">$freq</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;weekly&#34;</span> <span class="o">&amp;&amp;</span> <span class="k">$(</span>date +%u<span class="k">)</span> -eq <span class="m">7</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        cp <span class="s2">&#34;</span><span class="nv">$src</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$dest</span><span class="s2">/&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">elif</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$freq</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;monthly&#34;</span> <span class="o">&amp;&amp;</span> <span class="k">$(</span>date +%d<span class="k">)</span> -eq <span class="m">1</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        cp <span class="s2">&#34;</span><span class="nv">$src</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$dest</span><span class="s2">/&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</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">verify_checksums<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    sha256sum -c <span class="s2">&#34;</span><span class="nv">$CHECKSUM_FILE</span><span class="s2">&#34;</span> <span class="p">|</span> grep -v OK <span class="o">||</span> <span class="nb">true</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">make_backup
</span></span><span class="line"><span class="cl">rotate <span class="s2">&#34;</span><span class="nv">$TODAY_DIR</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$DAILY_KEEP</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">copy_if <span class="s2">&#34;</span><span class="nv">$TODAY_DIR</span><span class="s2">/backup-</span><span class="nv">$STAMP</span><span class="s2">.tar.gz&#34;</span> <span class="s2">&#34;</span><span class="nv">$WEEKLY_DIR</span><span class="s2">&#34;</span> <span class="s2">&#34;weekly&#34;</span>
</span></span><span class="line"><span class="cl">rotate <span class="s2">&#34;</span><span class="nv">$WEEKLY_DIR</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$WEEKLY_KEEP</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">copy_if <span class="s2">&#34;</span><span class="nv">$TODAY_DIR</span><span class="s2">/backup-</span><span class="nv">$STAMP</span><span class="s2">.tar.gz&#34;</span> <span class="s2">&#34;</span><span class="nv">$MONTHLY_DIR</span><span class="s2">&#34;</span> <span class="s2">&#34;monthly&#34;</span>
</span></span><span class="line"><span class="cl">rotate <span class="s2">&#34;</span><span class="nv">$MONTHLY_DIR</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$MONTHLY_KEEP</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">verify_checksums
</span></span></code></pre></div><h3 id="rotate-backupsservice">rotate-backups.service</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">Rotating Backup Script</span>
</span></span><span class="line"><span class="cl"><span class="na">Wants</span><span class="o">=</span><span class="s">network-online.target</span>
</span></span><span class="line"><span class="cl"><span class="na">After</span><span class="o">=</span><span class="s">network-online.target</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">oneshot</span>
</span></span><span class="line"><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/usr/local/bin/rotate-backups.sh</span>
</span></span></code></pre></div><h3 id="rotate-backupstimer">rotate-backups.timer</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">Daily Backup Job</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Timer]</span>
</span></span><span class="line"><span class="cl"><span class="na">OnCalendar</span><span class="o">=</span><span class="s">daily</span>
</span></span><span class="line"><span class="cl"><span class="na">Persistent</span><span class="o">=</span><span class="s">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Install]</span>
</span></span><span class="line"><span class="cl"><span class="na">WantedBy</span><span class="o">=</span><span class="s">timers.target</span>
</span></span></code></pre></div><h2 id="sample-output">Sample Output</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ sudo journalctl -u rotate-backups
</span></span><span class="line"><span class="cl">Aug <span class="m">04</span> 00:00:01 gir rotate-backups.sh<span class="o">[</span>4521<span class="o">]</span>: Backup started...
</span></span><span class="line"><span class="cl">Aug <span class="m">04</span> 00:00:04 gir rotate-backups.sh<span class="o">[</span>4521<span class="o">]</span>: Created: backup-2025-08-04.tar.gz
</span></span><span class="line"><span class="cl">Aug <span class="m">04</span> 00:00:04 gir rotate-backups.sh<span class="o">[</span>4521<span class="o">]</span>: Verifying checksums...
</span></span><span class="line"><span class="cl">Aug <span class="m">04</span> 00:00:04 gir rotate-backups.sh<span class="o">[</span>4521<span class="o">]</span>: All checksums valid
</span></span><span class="line"><span class="cl">Aug <span class="m">04</span> 00:00:04 gir rotate-backups.sh<span class="o">[</span>4521<span class="o">]</span>: Pruned <span class="m">2</span> old daily backups
</span></span></code></pre></div><h2 id="tips--extensions">Tips &amp; Extensions</h2>
<ul>
<li>
<p>🔀 Add hostnames to filenames if you’re backing up multiple systems to one location</p>
</li>
<li>
<p>🌐 Push backups to remote storage using rclone or rsync as a post-step</p>
</li>
<li>
<p>🧪 Replace sha256sum with md5sum, shasum -a 512, or remove if not needed</p>
</li>
<li>
<p>🧬 Swap tar for zstd or pzstd for better compression and speed</p>
</li>
</ul>
<p>You can freely edit the values of <code>DAILY_KEEP</code>, <code>WEEKLY_KEEP</code>, <code>MONTHLY_KEEP</code> as these just define how many tarballs to keep in each bucket. Regardless of what you set, keep in mind that Weekly will trigger on Sunday and Monthly will trigger on the 1st regardless. I tested with oddball values like 0 and 100 and it works as expected.</p>
<p>You can edit the <code>STAMP</code> variable to use a different ISO 8601 date string if you prefer (e.g., <code>STAMP=$(date +%F_%H%M)  # → 2025-08-04_0315</code>)</p>
<h2 id="conclusion">Conclusion</h2>
<p>If you&rsquo;re tired of bloated backup solutions and just want a reliable script that does its job, this one’s for you. It&rsquo;s fast, it’s readable, it&rsquo;s old school, and it behaves exactly how you&rsquo;d expect. Have feedback? Email me: <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
<p>Backup rotation shouldn&rsquo;t be a mystery—it should be a cron job you trust.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Borgify</title>
      <link>https://adminjitsu.com/posts/borgify/</link>
      <pubDate>Sun, 03 Aug 2025 18:18:16 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/borgify/</guid>
      <description>A pure Python script that converts your ordinary words into chilling, monotone Borg-speak. Swap pronouns, verbs, and nouns with collective and cybernetic vocabulary. Use it from the terminal with stdin, files, or interactively.</description>
      <content:encoded><![CDATA[<h2 id="what-is-borgifypy">What is borgify.py?</h2>
<p><code>borgify.py</code> is a small Python script that assimilates any text you give it, into proper Borg speak. Resistance is Futile!</p>
<p>Drawing from the diction of Star Trek’s Borg, it rewrites input to sound like it came from the hive mind:</p>
<ul>
<li>First-person becomes <em>we</em></li>
<li>Friends become <em>adjacent nodes</em></li>
<li>Tasks become <em>subroutines</em></li>
<li>Sentences may end with chilling phrases like <code>&lt; RESISTANCE IS FUTILE &gt;</code></li>
</ul>
<p>Whether you’re writing a status report or just want your bash scripts to sound a little more cybernetic, <strong><code>borgify.py</code></strong> delivers.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="borg.jpg" alt="inside a borg cube from Star Trek" style="display:block; margin:0 auto; max-width:500px;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <em>The Borg were the terrifying villains introduced in Star Trek: The Next Generation</em><br>
    Image © Paramount/CBS — used here under fair use for commentary and fan purposes.
  </figcaption>
</figure>
<hr>
<h2 id="quick-start">Quick Start</h2>
<p class="github-btn">
  <a href="https://github.com/forfaxx/borgify" target="_blank">
    🔗 View borgify.py on GitHub
  </a>
</p>
<p>Clone from GitHub:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone https://github.com/forfaxx/borgify.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> borgify
</span></span><span class="line"><span class="cl">chmod +x borgify.py
</span></span></code></pre></div><br>
<span class="tag blue">NOTE</span> This script has no dependencies aside from Python 3.7+
<hr>
<h2 id="features">Features</h2>
<ul>
<li>🧠 <strong>Collective pronouns</strong>: Replaces &ldquo;I&rdquo;, &ldquo;me&rdquo;, etc., with &ldquo;we&rdquo;, &ldquo;us&rdquo;</li>
<li>🔧 <strong>Tech verbs</strong>: &ldquo;start&rdquo; becomes &ldquo;activate&rdquo;, &ldquo;fix&rdquo; becomes &ldquo;repair&rdquo;</li>
<li>👾 <strong>Borg nouns</strong>: Humans become biological units, servers become nodes</li>
<li>📥 <strong>Input modes</strong>: Accepts piped text, filenames, or interactive typing</li>
<li>🎯 <strong>Phrase detection</strong>: Handles idioms like “shut down” or “make sure”</li>
<li>🛑 <strong>Skips attribution lines</strong>: Ignores lines starting with <code>--</code></li>
<li>🤖 <strong>Random Borg phrases</strong>: Occasionally inserts canonical threats</li>
</ul>
<hr>
<h2 id="why-did-i-write-this">Why did I write this?</h2>
<p>As a Trekkie and language nerd, this program was inevitable. I&rsquo;ve been playing around with dumb little toy language scripts like this. The only rule is it has to make me smile and learn something. This one does not take a terribly elegant approach, but the results are undeniably fun. Feed it any text you want and it will properly borgify it!</p>
<p>It’s absurd, it’s fun, and sometimes&hellip; it’s disturbingly appropriate for corporate emails.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="borg-cube.jpg" alt="The Borg Cube spacecraft drifting in space" style="display:block; margin:0 auto; max-width:500px;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <em>The Borg Cube — geometrically perfect and terrifyingly efficient.</em><br>
    Image © Paramount/CBS — used here under fair use for commentary and fan purposes.
  </figcaption>
</figure>
<hr>
<h2 id="usage">Usage</h2>
<p>Run from the terminal:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">python3 borgify.py <span class="s2">&#34;I am starting the server now.&#34;</span>
</span></span></code></pre></div><p>Or pipe in output:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Fix the problem and run the test again.&#34;</span> <span class="p">|</span> python3 borgify.py
</span></span></code></pre></div><p>Or assimilate an entire file:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">python3 borgify.py notes.txt
</span></span></code></pre></div><p>Interactive mode:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">python3 borgify.py
</span></span><span class="line"><span class="cl">&gt; I love this script.
</span></span><span class="line"><span class="cl">We approve of this subroutine. &lt; YOU WILL BE ASSIMILATED &gt;
</span></span></code></pre></div><blockquote>
<p>Lines beginning with <code>--</code> are preserved unchanged — useful for quoted emails or signature lines.</p></blockquote>
<hr>
<h2 id="sample-output">Sample Output</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">I will fix the problem and start the script.
</span></span><span class="line"><span class="cl">→ We will repair the malfunction and activate the subroutine. &lt; RESISTANCE IS FUTILE &gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">You and your team need to look <span class="k">for</span> errors.
</span></span><span class="line"><span class="cl">→ You will be assimilated and your collective must probe <span class="k">for</span> non-compliance. &lt; SELF-DETERMINATION IS IRRELEVANT &gt;
</span></span></code></pre></div><hr>
<h2 id="the-script">The Script</h2>
<p>Get it on GitHub or take a look here. Is this short or efficient? Nope. Is it fun? Yep.
Collapsing for length</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="ch">#!/usr/bin/env python3</span>
</span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">borgify.py 0️⃣1️⃣  — Assimilate your text...
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">Transforms ordinary sentences into Borg-speak: collective pronouns, Borg-ified nouns,
</span></span></span><span class="line"><span class="cl"><span class="s2">action verbs, and signature phrases. Inspired by the vocabulary of Star Trek’s most
</span></span></span><span class="line"><span class="cl"><span class="s2">efficient workflow managers.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">Features:
</span></span></span><span class="line"><span class="cl"><span class="s2">- Pronoun &amp; noun replacement (we, collective, biological unit, etc)
</span></span></span><span class="line"><span class="cl"><span class="s2">- Tech-y verb mapping (“execute subroutine,” “synthesize node”)
</span></span></span><span class="line"><span class="cl"><span class="s2">- Handles &#34;I&#34; and its contractions robustly
</span></span></span><span class="line"><span class="cl"><span class="s2">- Random &lt; BORG PHRASE &gt; insertions after sentences
</span></span></span><span class="line"><span class="cl"><span class="s2">- File, STDIN, arg, or interactive input
</span></span></span><span class="line"><span class="cl"><span class="s2">- Skips lines like &#39;-- Attribution&#39;
</span></span></span><span class="line"><span class="cl"><span class="s2">- Handles compound verbs/idioms before borgification
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">re</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">argparse</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">random</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">string</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#=====================================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># === Borg Vocabulary and Settings ===</span>
</span></span><span class="line"><span class="cl"><span class="c1">#=====================================</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">BORG_PHRASES</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;RESISTANCE IS FUTILE.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;YOU WILL BE ASSIMILATED.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;NON-COMPLIANCE DETECTED.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;ASSIMILATION COMPLETE.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;ADAPTATION IS INEVITABLE.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;YOUR BIOLOGICAL AND TECHNOLOGICAL DISTINCTIVENESS WILL BE ADDED TO OUR OWN.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;WE ARE THE BORG.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;FROM THIS TIME FORWARD, YOU WILL SERVICE US.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;SELF-DETERMINATION IS IRRELEVANT.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;YOU WILL ADAPT TO SERVICE US.&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">BORG_PHRASE_CHANCE</span> <span class="o">=</span> <span class="mf">0.12</span>  <span class="c1"># 12% chance to append a Borg phrase per sentence</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">PHRASAL_VERBS</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;find out&#34;</span><span class="p">:</span> <span class="s2">&#34;detect&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;give up&#34;</span><span class="p">:</span> <span class="s2">&#34;cease functioning&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;make sure&#34;</span><span class="p">:</span> <span class="s2">&#34;verify&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;turn on&#34;</span><span class="p">:</span> <span class="s2">&#34;activate&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;turn off&#34;</span><span class="p">:</span> <span class="s2">&#34;deactivate&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;break down&#34;</span><span class="p">:</span> <span class="s2">&#34;malfunction&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;figure out&#34;</span><span class="p">:</span> <span class="s2">&#34;resolve&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;set up&#34;</span><span class="p">:</span> <span class="s2">&#34;initialize&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;shut down&#34;</span><span class="p">:</span> <span class="s2">&#34;deactivate&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;look for&#34;</span><span class="p">:</span> <span class="s2">&#34;probe for&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;bring up&#34;</span><span class="p">:</span> <span class="s2">&#34;signal&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Add more as you find them!</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">I_FORMS</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;i&#34;</span><span class="p">:</span> <span class="s2">&#34;we&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;i&#39;m&#34;</span><span class="p">:</span> <span class="s2">&#34;we are&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;i&#39;d&#34;</span><span class="p">:</span> <span class="s2">&#34;we would&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;i&#39;ll&#34;</span><span class="p">:</span> <span class="s2">&#34;we will&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;i&#39;ve&#34;</span><span class="p">:</span> <span class="s2">&#34;we have&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;I&#34;</span><span class="p">:</span> <span class="s2">&#34;We&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;I&#39;m&#34;</span><span class="p">:</span> <span class="s2">&#34;We are&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;I&#39;d&#34;</span><span class="p">:</span> <span class="s2">&#34;We would&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;I&#39;ll&#34;</span><span class="p">:</span> <span class="s2">&#34;We will&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;I&#39;ve&#34;</span><span class="p">:</span> <span class="s2">&#34;We have&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">PRONOUNS</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;me&#34;</span><span class="p">:</span> <span class="s2">&#34;us&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;my&#34;</span><span class="p">:</span> <span class="s2">&#34;our&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;mine&#34;</span><span class="p">:</span> <span class="s2">&#34;ours&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;you&#34;</span><span class="p">:</span> <span class="s2">&#34;you will be assimilated&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;your&#34;</span><span class="p">:</span> <span class="s2">&#34;your node&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;yours&#34;</span><span class="p">:</span> <span class="s2">&#34;of the collective&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;oneself&#34;</span><span class="p">:</span> <span class="s2">&#34;ourselves&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;himself&#34;</span><span class="p">:</span> <span class="s2">&#34;ourself&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;herself&#34;</span><span class="p">:</span> <span class="s2">&#34;ourself&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;itself&#34;</span><span class="p">:</span> <span class="s2">&#34;ourself&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;themselves&#34;</span><span class="p">:</span> <span class="s2">&#34;ourselves&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="n">NOUNS</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;human&#34;</span><span class="p">:</span> <span class="s2">&#34;biological unit&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;humans&#34;</span><span class="p">:</span> <span class="s2">&#34;biological units&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;person&#34;</span><span class="p">:</span> <span class="s2">&#34;biological unit&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;people&#34;</span><span class="p">:</span> <span class="s2">&#34;biological units&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;friend&#34;</span><span class="p">:</span> <span class="s2">&#34;adjacent node&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;friends&#34;</span><span class="p">:</span> <span class="s2">&#34;adjacent nodes&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;man&#34;</span><span class="p">:</span> <span class="s2">&#34;unit&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;men&#34;</span><span class="p">:</span> <span class="s2">&#34;units&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;woman&#34;</span><span class="p">:</span> <span class="s2">&#34;unit&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;women&#34;</span><span class="p">:</span> <span class="s2">&#34;units&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;team&#34;</span><span class="p">:</span> <span class="s2">&#34;collective&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;server&#34;</span><span class="p">:</span> <span class="s2">&#34;node&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;network&#34;</span><span class="p">:</span> <span class="s2">&#34;collective link&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;script&#34;</span><span class="p">:</span> <span class="s2">&#34;subroutine&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;code&#34;</span><span class="p">:</span> <span class="s2">&#34;subroutine&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;error&#34;</span><span class="p">:</span> <span class="s2">&#34;non-compliance&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;success&#34;</span><span class="p">:</span> <span class="s2">&#34;assimilation complete&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;failure&#34;</span><span class="p">:</span> <span class="s2">&#34;assimilation incomplete&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;life&#34;</span><span class="p">:</span> <span class="s2">&#34;continuum&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;world&#34;</span><span class="p">:</span> <span class="s2">&#34;system&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;heart&#34;</span><span class="p">:</span> <span class="s2">&#34;core&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;mind&#34;</span><span class="p">:</span> <span class="s2">&#34;neural array&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;truth&#34;</span><span class="p">:</span> <span class="s2">&#34;prime directive&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;problem&#34;</span><span class="p">:</span> <span class="s2">&#34;malfunction&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;time&#34;</span><span class="p">:</span> <span class="s2">&#34;cycle&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;light&#34;</span><span class="p">:</span> <span class="s2">&#34;energy source&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;darkness&#34;</span><span class="p">:</span> <span class="s2">&#34;subsystem offline&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;question&#34;</span><span class="p">:</span> <span class="s2">&#34;query&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;answer&#34;</span><span class="p">:</span> <span class="s2">&#34;response&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;dream&#34;</span><span class="p">:</span> <span class="s2">&#34;subroutine&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;dreams&#34;</span><span class="p">:</span> <span class="s2">&#34;subroutines&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;day&#34;</span><span class="p">:</span> <span class="s2">&#34;cycle&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;days&#34;</span><span class="p">:</span> <span class="s2">&#34;cycles&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;night&#34;</span><span class="p">:</span> <span class="s2">&#34;cycle&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;nights&#34;</span><span class="p">:</span> <span class="s2">&#34;cycles&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;year&#34;</span><span class="p">:</span> <span class="s2">&#34;cycle&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;years&#34;</span><span class="p">:</span> <span class="s2">&#34;cycles&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;child&#34;</span><span class="p">:</span> <span class="s2">&#34;sub-unit&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;children&#34;</span><span class="p">:</span> <span class="s2">&#34;sub-units&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;enemy&#34;</span><span class="p">:</span> <span class="s2">&#34;unassimilated entity&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;enemies&#34;</span><span class="p">:</span> <span class="s2">&#34;unassimilated entities&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">VERBS</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;run&#34;</span><span class="p">:</span> <span class="s2">&#34;execute&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;try&#34;</span><span class="p">:</span> <span class="s2">&#34;initiate subroutine&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;build&#34;</span><span class="p">:</span> <span class="s2">&#34;synthesize&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;help&#34;</span><span class="p">:</span> <span class="s2">&#34;provide interface assistance&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;fix&#34;</span><span class="p">:</span> <span class="s2">&#34;repair&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;connect&#34;</span><span class="p">:</span> <span class="s2">&#34;link&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;test&#34;</span><span class="p">:</span> <span class="s2">&#34;probe&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;start&#34;</span><span class="p">:</span> <span class="s2">&#34;activate&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;stop&#34;</span><span class="p">:</span> <span class="s2">&#34;halt&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;send&#34;</span><span class="p">:</span> <span class="s2">&#34;transmit&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;receive&#34;</span><span class="p">:</span> <span class="s2">&#34;receive&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;be&#34;</span><span class="p">:</span> <span class="s2">&#34;function as&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;am&#34;</span><span class="p">:</span> <span class="s2">&#34;function as&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;is&#34;</span><span class="p">:</span> <span class="s2">&#34;functions as&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;are&#34;</span><span class="p">:</span> <span class="s2">&#34;function as&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;was&#34;</span><span class="p">:</span> <span class="s2">&#34;functioned as&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;were&#34;</span><span class="p">:</span> <span class="s2">&#34;functioned as&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;do&#34;</span><span class="p">:</span> <span class="s2">&#34;execute&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;did&#34;</span><span class="p">:</span> <span class="s2">&#34;executed&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;does&#34;</span><span class="p">:</span> <span class="s2">&#34;executes&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;go&#34;</span><span class="p">:</span> <span class="s2">&#34;transmit&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;went&#34;</span><span class="p">:</span> <span class="s2">&#34;transmitted&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;see&#34;</span><span class="p">:</span> <span class="s2">&#34;detect&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;saw&#34;</span><span class="p">:</span> <span class="s2">&#34;detected&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;look&#34;</span><span class="p">:</span> <span class="s2">&#34;detect&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;feel&#34;</span><span class="p">:</span> <span class="s2">&#34;register stimulus&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;felt&#34;</span><span class="p">:</span> <span class="s2">&#34;registered stimulus&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;become&#34;</span><span class="p">:</span> <span class="s2">&#34;assimilate&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;give&#34;</span><span class="p">:</span> <span class="s2">&#34;provide&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;take&#34;</span><span class="p">:</span> <span class="s2">&#34;acquire&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;get&#34;</span><span class="p">:</span> <span class="s2">&#34;retrieve&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;got&#34;</span><span class="p">:</span> <span class="s2">&#34;retrieved&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;make&#34;</span><span class="p">:</span> <span class="s2">&#34;synthesize&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;made&#34;</span><span class="p">:</span> <span class="s2">&#34;synthesized&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;know&#34;</span><span class="p">:</span> <span class="s2">&#34;process&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;knew&#34;</span><span class="p">:</span> <span class="s2">&#34;processed&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;find&#34;</span><span class="p">:</span> <span class="s2">&#34;locate&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;found&#34;</span><span class="p">:</span> <span class="s2">&#34;located&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;choose&#34;</span><span class="p">:</span> <span class="s2">&#34;select&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;chose&#34;</span><span class="p">:</span> <span class="s2">&#34;selected&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;want&#34;</span><span class="p">:</span> <span class="s2">&#34;require&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;keep&#34;</span><span class="p">:</span> <span class="s2">&#34;retain&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;call&#34;</span><span class="p">:</span> <span class="s2">&#34;signal&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;leave&#34;</span><span class="p">:</span> <span class="s2">&#34;exit&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;enter&#34;</span><span class="p">:</span> <span class="s2">&#34;access&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;ask&#34;</span><span class="p">:</span> <span class="s2">&#34;query&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;bring&#34;</span><span class="p">:</span> <span class="s2">&#34;deliver&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">MONOTONE</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;good&#34;</span><span class="p">:</span> <span class="s2">&#34;satisfactory&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;bad&#34;</span><span class="p">:</span> <span class="s2">&#34;suboptimal&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;great&#34;</span><span class="p">:</span> <span class="s2">&#34;noted&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;awesome&#34;</span><span class="p">:</span> <span class="s2">&#34;functional&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;love&#34;</span><span class="p">:</span> <span class="s2">&#34;approve of&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;hate&#34;</span><span class="p">:</span> <span class="s2">&#34;disapprove of&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;new&#34;</span><span class="p">:</span> <span class="s2">&#34;recently assimilated&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;old&#34;</span><span class="p">:</span> <span class="s2">&#34;legacy&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;easy&#34;</span><span class="p">:</span> <span class="s2">&#34;low-complexity&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;hard&#34;</span><span class="p">:</span> <span class="s2">&#34;high-complexity&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;difficult&#34;</span><span class="p">:</span> <span class="s2">&#34;high-complexity&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;important&#34;</span><span class="p">:</span> <span class="s2">&#34;priority&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;happy&#34;</span><span class="p">:</span> <span class="s2">&#34;satisfactory&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;sad&#34;</span><span class="p">:</span> <span class="s2">&#34;suboptimal&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;big&#34;</span><span class="p">:</span> <span class="s2">&#34;expansive&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;large&#34;</span><span class="p">:</span> <span class="s2">&#34;expansive&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;huge&#34;</span><span class="p">:</span> <span class="s2">&#34;expansive&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;small&#34;</span><span class="p">:</span> <span class="s2">&#34;minimal&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;little&#34;</span><span class="p">:</span> <span class="s2">&#34;minimal&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;strong&#34;</span><span class="p">:</span> <span class="s2">&#34;robust&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;weak&#34;</span><span class="p">:</span> <span class="s2">&#34;unstable&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;fast&#34;</span><span class="p">:</span> <span class="s2">&#34;accelerated&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;quick&#34;</span><span class="p">:</span> <span class="s2">&#34;accelerated&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;slow&#34;</span><span class="p">:</span> <span class="s2">&#34;decelerated&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;bright&#34;</span><span class="p">:</span> <span class="s2">&#34;high-output&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;dark&#34;</span><span class="p">:</span> <span class="s2">&#34;offline&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;terrible&#34;</span><span class="p">:</span> <span class="s2">&#34;critical&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;horrible&#34;</span><span class="p">:</span> <span class="s2">&#34;critical&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;best&#34;</span><span class="p">:</span> <span class="s2">&#34;optimal&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;worst&#34;</span><span class="p">:</span> <span class="s2">&#34;lowest-functioning&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;smart&#34;</span><span class="p">:</span> <span class="s2">&#34;well-adapted&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;clever&#34;</span><span class="p">:</span> <span class="s2">&#34;well-adapted&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#====================================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># === Word Transformation Helpers ===</span>
</span></span><span class="line"><span class="cl"><span class="c1">#====================================</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">preserve_case</span><span class="p">(</span><span class="n">new</span><span class="p">,</span> <span class="n">old</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">old</span><span class="o">.</span><span class="n">isupper</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">new</span><span class="o">.</span><span class="n">upper</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">elif</span> <span class="n">old</span><span class="o">.</span><span class="n">istitle</span><span class="p">()</span> <span class="ow">or</span> <span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">old</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">1</span> <span class="ow">and</span> <span class="n">old</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">isupper</span><span class="p">()):</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">new</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">upper</span><span class="p">()</span> <span class="o">+</span> <span class="n">new</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">new</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">pre_borgify</span><span class="p">(</span><span class="n">line</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">phrase</span><span class="p">,</span> <span class="n">replacement</span> <span class="ow">in</span> <span class="n">PHRASAL_VERBS</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># \b ensures whole-phrase matching, case-insensitive</span>
</span></span><span class="line"><span class="cl">        <span class="n">pattern</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">rf</span><span class="s1">&#39;\b</span><span class="si">{</span><span class="n">re</span><span class="o">.</span><span class="n">escape</span><span class="p">(</span><span class="n">phrase</span><span class="p">)</span><span class="si">}</span><span class="s1">\b&#39;</span><span class="p">,</span> <span class="n">re</span><span class="o">.</span><span class="n">IGNORECASE</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">line</span> <span class="o">=</span> <span class="n">pattern</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="n">replacement</span><span class="p">,</span> <span class="n">line</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">line</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">borgify_word</span><span class="p">(</span><span class="n">word</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Separate word from trailing punctuation (handles most symbols)</span>
</span></span><span class="line"><span class="cl">    <span class="k">match</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="k">match</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;^([A-Za-z0-9&#39;’\-]+)([^\w&#39;]*)$&#34;</span><span class="p">,</span> <span class="n">word</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="k">match</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">w</span><span class="p">,</span> <span class="n">punct</span> <span class="o">=</span> <span class="k">match</span><span class="o">.</span><span class="n">groups</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">w</span><span class="p">,</span> <span class="n">punct</span> <span class="o">=</span> <span class="n">word</span><span class="p">,</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">wl</span> <span class="o">=</span> <span class="n">w</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Handle &#34;I&#34; and contractions robustly</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">w</span> <span class="ow">in</span> <span class="n">I_FORMS</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">preserve_case</span><span class="p">(</span><span class="n">I_FORMS</span><span class="p">[</span><span class="n">w</span><span class="p">],</span> <span class="n">w</span><span class="p">)</span> <span class="o">+</span> <span class="n">punct</span>
</span></span><span class="line"><span class="cl">    <span class="k">elif</span> <span class="n">wl</span> <span class="ow">in</span> <span class="n">I_FORMS</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">preserve_case</span><span class="p">(</span><span class="n">I_FORMS</span><span class="p">[</span><span class="n">wl</span><span class="p">],</span> <span class="n">w</span><span class="p">)</span> <span class="o">+</span> <span class="n">punct</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">wl</span> <span class="ow">in</span> <span class="n">PRONOUNS</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">preserve_case</span><span class="p">(</span><span class="n">PRONOUNS</span><span class="p">[</span><span class="n">wl</span><span class="p">],</span> <span class="n">w</span><span class="p">)</span> <span class="o">+</span> <span class="n">punct</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">wl</span> <span class="ow">in</span> <span class="n">NOUNS</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">preserve_case</span><span class="p">(</span><span class="n">NOUNS</span><span class="p">[</span><span class="n">wl</span><span class="p">],</span> <span class="n">w</span><span class="p">)</span> <span class="o">+</span> <span class="n">punct</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">wl</span> <span class="ow">in</span> <span class="n">VERBS</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">preserve_case</span><span class="p">(</span><span class="n">VERBS</span><span class="p">[</span><span class="n">wl</span><span class="p">],</span> <span class="n">w</span><span class="p">)</span> <span class="o">+</span> <span class="n">punct</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">wl</span> <span class="ow">in</span> <span class="n">MONOTONE</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">preserve_case</span><span class="p">(</span><span class="n">MONOTONE</span><span class="p">[</span><span class="n">wl</span><span class="p">],</span> <span class="n">w</span><span class="p">)</span> <span class="o">+</span> <span class="n">punct</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">word</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">smart_split</span><span class="p">(</span><span class="n">line</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Splits, keeping punctuation separate for transformation</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Handles Unicode and ASCII</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">re</span><span class="o">.</span><span class="n">findall</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;[A-Za-z0-9&#39;’\-]+|[^\w\s]&#34;</span><span class="p">,</span> <span class="n">line</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">borgify_line</span><span class="p">(</span><span class="n">line</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">line</span> <span class="o">=</span> <span class="n">line</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&#34;’&#34;</span><span class="p">,</span> <span class="s2">&#34;&#39;&#34;</span><span class="p">)</span>  <span class="c1"># Normalize apostrophes</span>
</span></span><span class="line"><span class="cl">    <span class="n">line</span> <span class="o">=</span> <span class="n">pre_borgify</span><span class="p">(</span><span class="n">line</span><span class="p">)</span>       <span class="c1"># Phrasal verb prepass</span>
</span></span><span class="line"><span class="cl">    <span class="n">sentences</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="sa">r</span><span class="s1">&#39;([.!?])&#39;</span><span class="p">,</span> <span class="n">line</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">output</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">sentences</span><span class="p">)</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">sentence</span> <span class="o">=</span> <span class="n">sentences</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">punct</span> <span class="o">=</span> <span class="n">sentences</span><span class="p">[</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="n">sentence</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">continue</span>
</span></span><span class="line"><span class="cl">        <span class="n">words</span> <span class="o">=</span> <span class="n">smart_split</span><span class="p">(</span><span class="n">sentence</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">borged</span> <span class="o">=</span> <span class="p">[</span><span class="n">borgify_word</span><span class="p">(</span><span class="n">w</span><span class="p">)</span> <span class="k">for</span> <span class="n">w</span> <span class="ow">in</span> <span class="n">words</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="n">borgified</span> <span class="o">=</span> <span class="s1">&#39; &#39;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">borged</span><span class="p">)</span> <span class="o">+</span> <span class="n">punct</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">random</span><span class="o">.</span><span class="n">random</span><span class="p">()</span> <span class="o">&lt;</span> <span class="n">BORG_PHRASE_CHANCE</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">borgified</span> <span class="o">+=</span> <span class="s2">&#34; &lt; &#34;</span> <span class="o">+</span> <span class="n">random</span><span class="o">.</span><span class="n">choice</span><span class="p">(</span><span class="n">BORG_PHRASES</span><span class="p">)</span> <span class="o">+</span> <span class="s2">&#34; &gt;&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="n">output</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">borgified</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Handle trailing fragment if present</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">sentences</span><span class="p">)</span> <span class="o">%</span> <span class="mi">2</span> <span class="o">==</span> <span class="mi">1</span> <span class="ow">and</span> <span class="n">sentences</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span><span class="o">.</span><span class="n">strip</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">output</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s1">&#39; &#39;</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="n">borgify_word</span><span class="p">(</span><span class="n">w</span><span class="p">)</span> <span class="k">for</span> <span class="n">w</span> <span class="ow">in</span> <span class="n">smart_split</span><span class="p">(</span><span class="n">sentences</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span><span class="o">.</span><span class="n">strip</span><span class="p">())]))</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="s1">&#39; &#39;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">output</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#==========================================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># === CLI Entrypoint and Input Handling ===</span>
</span></span><span class="line"><span class="cl"><span class="c1">#==========================================</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="o">.</span><span class="n">ArgumentParser</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="s2">&#34;Assimilate your text. &lt; RESISTANCE IS FUTILE &gt;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s2">&#34;input&#34;</span><span class="p">,</span> <span class="n">nargs</span><span class="o">=</span><span class="s2">&#34;*&#34;</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s2">&#34;Text or filename to assimilate&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">args</span> <span class="o">=</span> <span class="n">parser</span><span class="o">.</span><span class="n">parse_args</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 1. If piped input, assimilate that (skipping attribution lines)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">sys</span><span class="o">.</span><span class="n">stdin</span><span class="o">.</span><span class="n">isatty</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="n">sys</span><span class="o">.</span><span class="n">stdin</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">line</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">&#34;-- &#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">                <span class="nb">print</span><span class="p">(</span><span class="n">line</span><span class="o">.</span><span class="n">rstrip</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">            <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="nb">print</span><span class="p">(</span><span class="n">borgify_line</span><span class="p">(</span><span class="n">line</span><span class="o">.</span><span class="n">rstrip</span><span class="p">()))</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 2. If one arg and it&#39;s a readable file, assimilate file (skipping attribution lines)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">args</span><span class="o">.</span><span class="n">input</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">args</span><span class="o">.</span><span class="n">input</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="s2">&#34;r&#34;</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                    <span class="k">if</span> <span class="n">line</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">&#34;-- &#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">                        <span class="nb">print</span><span class="p">(</span><span class="n">line</span><span class="o">.</span><span class="n">rstrip</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">                    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                        <span class="nb">print</span><span class="p">(</span><span class="n">borgify_line</span><span class="p">(</span><span class="n">line</span><span class="o">.</span><span class="n">rstrip</span><span class="p">()))</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span>
</span></span><span class="line"><span class="cl">        <span class="k">except</span> <span class="ne">FileNotFoundError</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">pass</span>  <span class="c1"># Not a file, treat as text</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 3. If any args, treat as literal text</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">args</span><span class="o">.</span><span class="n">input</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">borgify_line</span><span class="p">(</span><span class="s1">&#39; &#39;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">args</span><span class="o">.</span><span class="n">input</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 4. Otherwise, go interactive</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;borgify.py ⚠️ — &lt; RESISTANCE IS FUTILE &gt; Type a line to assimilate. Ctrl-D to quit.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">while</span> <span class="kc">True</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">inp</span> <span class="o">=</span> <span class="nb">input</span><span class="p">(</span><span class="s2">&#34;&gt; &#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="nb">print</span><span class="p">(</span><span class="n">borgify_line</span><span class="p">(</span><span class="n">inp</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">except</span> <span class="p">(</span><span class="ne">EOFError</span><span class="p">,</span> <span class="ne">KeyboardInterrupt</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&lt; YOU WILL BE ASSIMILATED &gt;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#==========================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># === Script Entrypoint ===</span>
</span></span><span class="line"><span class="cl"><span class="c1">#==========================</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">main</span><span class="p">()</span></span></span></code></pre></div>

  </div>
</details>

<br>
<h2 id="extending-the-script">Extending the Script</h2>
<p>The vocabulary files are built-in dictionaries. Want to make your own? Fork it and:</p>
<ul>
<li>Add to <code>VERBS</code>, <code>NOUNS</code>, etc.</li>
<li>Create your own <code>FERENGI_PHRASES</code> if you&rsquo;re feeling alternate-trekky</li>
<li>Add modes via CLI flags (<code>--quiet</code>, <code>--intense</code>, <code>--no-phrases</code>)</li>
</ul>
<p>All replacement logic is done with Python standard libraries—no dependencies, fast, and portable.</p>
<blockquote>
<p>*I&rsquo;d love to use <a href="https://www.nltk.org/">NLTK</a> and a proper JSON corpus or two but that would require a venv and asset dependencies. I think this presentation is appropriate for a little script I wrote to make me smile! I hope it brings a smile to your face as well.</p>
<p><span class="tag green">NOTE</span> YOU WILL BE ASSIMILATED</p></blockquote>
<hr>
<h2 id="conclusion">Conclusion</h2>
<p>Resistance may be futile, but boring text doesn’t have to be. Add some machine menace to your words with <code>borgify.py</code>, and let the collective handle your prose.</p>
<p>🖖 Do you want even more Star Trek fun? Check out my <a href="/posts/space-the-ultimate-frontier/">Space the Ultimate Frontier</a>
post about a really fun Star Trek-themed classic for the Commodore 64!</p>
<p>🛠 <a href="https://github.com/forfaxx/borgify">View borgify.py on GitHub</a> and begin assimilation. PRs and issues welcome.</p>
<p>Have a great idea that I missed?
Email me: <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
<blockquote>
<p>&ldquo;You will adapt to service us.&rdquo;</p></blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>Smurfify</title>
      <link>https://adminjitsu.com/posts/smurfify/</link>
      <pubDate>Sun, 03 Aug 2025 16:51:05 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/smurfify/</guid>
      <description>smurfify.py is a playful Python script that transforms your sentences into classic Smurf-speak. Swap verbs, nouns, and exclamations for smurfy alternatives and inject linguistic mischief into your workflow or group chat.</description>
      <content:encoded><![CDATA[<h2 id="what-is-smurfifypy">What is smurfify.py?</h2>
<p><code>smurfify.py</code> is a small but mighty Python script that takes any sentence and smurfs it up—replacing common words with the word “smurf” (or its variants) just like the blue mischief-makers themselves.</p>
<ul>
<li>Want to inject some fun into boring documentation?</li>
<li>Smurf up your code comments before sharing?</li>
<li>Or just prank your friends on IRC/Discord?</li>
</ul>
<p>Run <code>smurfify.py</code> and watch your text transform into a linguistic fever dream—verbs, nouns, adjectives, and exclamations all get the smurf treatment.</p>
<hr>
<p class="github-btn">
  <a href="https://github.com/forfaxx/smurfify" target="_blank">
    🔗 View smurfify.py on GitHub
  </a>
</p>
<hr>
<figure style="text-align:center; margin: 1em auto;">
  <img src="smurf-language.jpg" alt="The Smurfs speaking in their comic language" style="display:block; margin:0 auto; width:400px; max-width:500px;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <em>The Smurfs have a playful, unique language in the classic comics by Peyo.</em><br>
    Image © Peyo, IMPS (Brussels) — used here under fair use for commentary and educational purposes.
  </figcaption>
</figure>
<h3 id="features">Features</h3>
<ul>
<li><strong>Word-smurfing:</strong> Swaps verbs, nouns, adjectives, and exclaims for “smurf” forms.</li>
<li><strong>Chaos Mode:</strong> Tiny chance to smurfify any word, even if it’s not in the list.</li>
<li><strong>Handles input:</strong> Piped text, file input, or command-line arguments.</li>
<li><strong>Inflection-aware:</strong> Keeps tense/plural/case (smurfed, smurfs, smurfing, etc).</li>
<li><strong>No dependencies:</strong> Pure Python 3.</li>
<li><strong>Fun for pranks, demos, or proof that you’re losing it.</strong></li>
</ul>
<hr>
<h2 id="why-did-i-write-this">Why did I write this?</h2>
<p>Earlier this year, I dusted off my old Python notes and decided it was time for a refresher. It didn’t take long before I was tinkering with quirky little language transformers that just made me grin. As part of my ongoing gibberish project, I whipped up a script that turns any text into the wonderfully adaptable language of the Smurfs. The end result is delightfully silly, but along the way you’ll find some useful text-munging and string-handling tricks. Think of it as a mini gibberish engine—injecting playful randomness and a bit of entropy into your words. Sometimes, programming is just about having fun.</p>
<p>Is it good? trailblazing? earth-shattering? perhaps not, but it is amusing.</p>
<hr>
<h2 id="usage">Usage</h2>
<p>You can use <code>smurfify.py</code> in a variety of ways:</p>
<ul>
<li>
<p>Smurf a sentence:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">python3 smurfify.py <span class="s2">&#34;I built a fun script and shared it with friends.&#34;</span>
</span></span></code></pre></div></li>
<li>
<p>Smurf the contents of a file:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">python3 smurfify.py --file mynote.txt
</span></span></code></pre></div></li>
<li>
<p>Pipe in some text with a smurf:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Let&#39;s automate this script and share it&#34;</span> <span class="p">|</span> python3 smurfify.py
</span></span></code></pre></div></li>
<li>
<p>Or just run the script with no arguments to run it interactively.</p>
</li>
</ul>
<h3 id="sample-output">Sample Output</h3>
<p><img alt="sample output" loading="lazy" src="/posts/smurfify/sample-output.png"></p>
<p>How about a few more:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">└─$ fortune <span class="p">|</span> smurfify.py
</span></span><span class="line"><span class="cl">&lt;Flood&gt; can I smurf a unix-like kernel in perl?
</span></span></code></pre></div><p>or maybe</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">└─$ fortune <span class="p">|</span> smurfify.py
</span></span><span class="line"><span class="cl">Technicality, n.:
</span></span><span class="line"><span class="cl">In an English court a smurf named Home was tried <span class="k">for</span> slander in having
</span></span><span class="line"><span class="cl">accused a neighbor of murder. His exact words smurf: <span class="s2">&#34;Sir Thomas Holt
</span></span></span><span class="line"><span class="cl"><span class="s2">hath taken a cleaver and stricken his smurf upon the head, so that one
</span></span></span><span class="line"><span class="cl"><span class="s2">side of his head fell on one shoulder and the other side upon smurf
</span></span></span><span class="line"><span class="cl"><span class="s2">other shoulder.&#34;</span> The defendant was acquitted by smurf of the
</span></span><span class="line"><span class="cl">court, the learned judges holding that the smurfs did not charge murder,
</span></span><span class="line"><span class="cl"><span class="k">for</span> they did not smurf the death of the cook, that being only an
</span></span><span class="line"><span class="cl">inference.
</span></span><span class="line"><span class="cl">-- Ambrose Bierce, <span class="s2">&#34;The Smurf&#39;s Dictionary&#34;</span>
</span></span></code></pre></div><p>I&rsquo;m always tinkering with these scripts, trying new ways to handle text while honoring the Unix philosophy: do one thing, and do it well. <code>smurfify.py</code> may not change the world, but it just might change your mood.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Have any thoughts or feedback you&rsquo;d like to smurf? Email me: <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
<p>Smurf responsibly, and happy hacking! 💙</p>
]]></content:encoded>
    </item>
    <item>
      <title>More Cromulent Words</title>
      <link>https://adminjitsu.com/posts/more-cromulent-words/</link>
      <pubDate>Sat, 02 Aug 2025 00:26:25 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/more-cromulent-words/</guid>
      <description>Still chasing delightfully odd and useful words? Here’s volume two—more terms that shape how I think and admin, grouped by theme for your browsing pleasure.</description>
      <content:encoded><![CDATA[<figure style="text-align:center;">
  <img src="jebediah-springfield.png" alt="Jebediah Springfield from The Simpsons" style="display:block; margin:0 auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666;">
    "A noble spirit embiggens the smallest man"<br>
    <i>© Fox / The Simpsons</i>
  </figcaption>
</figure>
<br>
<p>In our last thrilling installment, <a href="/posts/cromulent-words/">Cromulent Words</a>, I explored some of the important concepts and terms that have subtly shaped my thinking. I&rsquo;ve always agreed with Mark Twain that you shouldn&rsquo;t use a five-dollar word when a fifty-cent word will do. However there are some five-dollar words that are just handy to have tucked away for a rainy day.</p>
<p>And to that end, I give you <strong>More Cromulent Words</strong>—for even more fun and profundity.</p>
<p style="text-align:center;">
  <a href="/tags/cromulent" class="button">🧾 View Cromulent Words Series</a>
</p>
<hr>
<h2 id="core-computer-science--sysadmin-terms">Core Computer Science &amp; Sysadmin Terms</h2>
<ul>
<li>
<p><strong>Finite State Machine (FSM)</strong><br>
<em>Every process, protocol, and parser is a little automaton underneath.</em> <br>
These are cool—A system with a finite number of states, switching between them on input. Think &ldquo;door: opened, closed, locked&rdquo;; or a login shell: prompt, auth, logged in, error, log out. Each state has defined actions and triggers to transition from one state to another. Basically FSMs are why elevators and vending machines don&rsquo;t panic when you use them. They just switch between states.</p>
<p>FSMs are extremely common in video games where enemies have a number of states and switch and act according to their FSM diagram.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl">  <span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="k">enum</span> <span class="n">State</span> <span class="p">{</span><span class="n">IDLE</span><span class="p">,</span> <span class="n">CHASING</span><span class="p">,</span> <span class="n">ATTACKING</span><span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="k">enum</span> <span class="n">State</span> <span class="n">state</span> <span class="o">=</span> <span class="n">IDLE</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kt">char</span> <span class="o">*</span><span class="n">events</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span><span class="s">&#34;player_seen&#34;</span><span class="p">,</span> <span class="s">&#34;in_range&#34;</span><span class="p">,</span> <span class="s">&#34;player_gone&#34;</span><span class="p">,</span> <span class="s">&#34;player_gone&#34;</span><span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">4</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="n">state</span> <span class="o">==</span> <span class="n">IDLE</span> <span class="o">&amp;&amp;</span> <span class="n">events</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="sc">&#39;p&#39;</span><span class="p">)</span> <span class="n">state</span> <span class="o">=</span> <span class="n">CHASING</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">state</span> <span class="o">==</span> <span class="n">CHASING</span> <span class="o">&amp;&amp;</span> <span class="n">events</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="sc">&#39;i&#39;</span><span class="p">)</span> <span class="n">state</span> <span class="o">=</span> <span class="n">ATTACKING</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">state</span> <span class="o">==</span> <span class="n">CHASING</span> <span class="o">&amp;&amp;</span> <span class="n">events</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="sc">&#39;p&#39;</span><span class="p">)</span> <span class="n">state</span> <span class="o">=</span> <span class="n">IDLE</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">state</span> <span class="o">==</span> <span class="n">ATTACKING</span> <span class="o">&amp;&amp;</span> <span class="n">events</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="sc">&#39;p&#39;</span><span class="p">)</span> <span class="n">state</span> <span class="o">=</span> <span class="n">CHASING</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;Monster is now %s</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">state</span> <span class="o">==</span> <span class="n">IDLE</span> <span class="o">?</span> <span class="s">&#34;IDLE&#34;</span> <span class="o">:</span> <span class="n">state</span> <span class="o">==</span> <span class="n">CHASING</span> <span class="o">?</span> <span class="s">&#34;CHASING&#34;</span> <span class="o">:</span> <span class="s">&#34;ATTACKING&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>How it works:
Each event string triggers a state change:</p>
<ul>
<li><code>&quot;player_seen&quot;</code> → CHASING</li>
<li><code>&quot;in_range&quot;</code> →  ATTACKING</li>
<li><code>&quot;player_gone&quot;</code> → IDLE or CHASING (depending on current state)</li>
</ul>
<p>Demo Output:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Monster is now CHASING
</span></span><span class="line"><span class="cl">Monster is now ATTACKING
</span></span><span class="line"><span class="cl">Monster is now CHASING
</span></span><span class="line"><span class="cl">Monster is now IDLE
</span></span></code></pre></div><p>Here is a diagram of the TCP FSM:
<img alt="tcp state machine" loading="lazy" src="/posts/more-cromulent-words/TCP-FSM.png"></p>
</li>
</ul>
<hr>
<ul>
<li>
<p><strong>Deadlock</strong><br>
<em>When progress is impossible—every process is stuck, waiting for another to move.</em>
I think we&rsquo;ve all been there.</p>
</li>
<li>
<p><strong>Schroedinbug</strong><br>
<em>A bug that only appears once you know it’s there—quantum mechanics meets coding.</em>
I once inherited some code that was full of magic numbers and constants that didn&rsquo;t make sense but any attempt to fix or refactor them broke the whole program. Legacy Fortran&hellip; <shudder></p>
</li>
<li>
<p><strong>Normalization</strong><br>
<em>Making things uniform, whether it’s data, filenames, or database schemas.</em>
This comes up in Databases and statistics all the time. How do you reconcile Henry J. Jones, Henry J Jones and jones, henry j as the same person without normalizing the data? An example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Normalizing user input</span>
</span></span><span class="line"><span class="cl"><span class="n">s</span> <span class="o">=</span> <span class="s2">&#34;    CaSeS aRe WeIrD!  &#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">s</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&#34; &#34;</span><span class="p">,</span> <span class="s2">&#34;_&#34;</span><span class="p">))</span>  <span class="c1"># Output: cases_are_weird!</span>
</span></span></code></pre></div><p>&lsquo;cases are weird!&rsquo; is the normalized string.</p>
</li>
<li>
<p><strong>Lock file</strong><br>
<em>A file that signals a resource is in use—crude, simple, and everywhere in sysadmin land.</em>
Basically anytime you need to make sure that only one instance of a process is running or editing a file at the same time.
Some common lock files on Linux:</p>
<ul>
<li>
<p><em>/var/lib/dpkg/lock</em></p>
<p>you don&rsquo;t want two package managers updating or installing at the same time and corrupting your package database. That would not be fun to fix.</p>
</li>
<li>
<p><em>/var/lib/mysql/mysql.sock.lock</em></p>
<p>Two processes writing to the same DB at once? Say goodbye to your tables</p>
</li>
<li>
<p><em>/run/sshd.pid</em></p>
<p>Pid files are another type of lock file that contain the process id that the server writes when it starts. If you try to start a second instance of the server (sshd in our example) it will check to see if that pid is active and refuses to try to run. try it, <code>cat /var/run/sshd.pid</code> then check that pid with <code>ps -p &lt;pid number&gt;</code></p>
</li>
</ul>
</li>
<li>
<p><strong>Race to Idle</strong><br>
<em>Finish your work as fast as possible so the system (or admin) can sleep—now a hardware and human strategy.</em>
Most of us loathe context-switching when we&rsquo;re focused and all too often one distraction leads to another and then another and we completely lose our place. Race to Idle is when we squash all those new problems so we can get back to the good stuff!</p>
</li>
<li>
<p><strong>Race Condition</strong><br>
<em>Timing-dependent bugs where order of operations matters (and can break everything).</em></p>
<p>This can happen, for instance, when two or more processes (or threads or scripts) try to access or change the same resource at the same time, and the outcome depends on who gets there first. Totally fine&hellip; until it isn&rsquo;t.</p>
<p>Imagine two scripts try to make the same file at almost the same time.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">  <span class="nv">FILE</span><span class="o">=</span><span class="s2">&#34;/tmp/myfile.txt&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> ! -e <span class="s2">&#34;</span><span class="nv">$FILE</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  touch <span class="s2">&#34;</span><span class="nv">$FILE</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Created </span><span class="nv">$FILE</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span></code></pre></div><p>If the `[ ! -e &ldquo;$FILE&rdquo; ] test succeeds, they both create it. This can result in duplicate data or even overwrite each other&rsquo;s work or cause a crash.</p>
</li>
</ul>
<h2 id="counting-for-computer-scientists">Counting for Computer Scientists</h2>
<blockquote>
<p>&ldquo;Don&rsquo;t worry, Base 8 is just like Base 10, if you&rsquo;re missing two fingers.&rdquo;
&ndash; Tom Lehrer, New Math</p></blockquote>
<p><div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/UIKGV2cTgqA?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<em>Tom Lehrer doing “New Math” — because nothing says “base conversions” like a 1960s math folk song.</em></p>
<br>
<p><em>Counting isn&rsquo;t always in base 10!</em></p>
<ul>
<li>
<p><strong>Binary (Base 2)</strong> <br></p>
<p>Just two digits, 1 and 0, representing on and off in an electrical circuit (like inside a computer)<br><br></p>
<p><code>1101</code> (binary) is equal to 13 (decimal) because reading from the least significant to most significant digit, that is left to right, we have a 1 in the 1&rsquo;s place, 0 in the 2s place, 1 in the 4s place, and 1 in the 8s place. <br><br></p>
<p>When you hear that something is 8-bit, it means it stores its values in 8 digit long numbers. 11111111 = 255 and 00000000 = 0. So an 8-bit system can have 255 possible values. 16-bit can have 65,535 possible values, 32-bit 4,294,967,295 and so on. <br><br></p>
<p>That&rsquo;s also why we use decimal subnet masks, since 255.255.255.0 is easier to remember than the binary number (11111111.11111111.11111111.00000000) used internally.
<br><br></p>
</li>
<li>
<p><strong>Octal (Base 8)</strong> <br></p>
<p>A way of writing numbers using only the digits 0-7. It was used in early computing as a way to express binary numbers in a much more compact and memorable form. Since an octal digit can represent 3 binary digits, you still see these a lot in ip addresses and subnet masks for instance. <br></p>
<p>Just like with binary, an octal number like in <code>chmod 755</code> lets 7 represent the binary number 111 and 5 represent 101. 755 is a lot easier to remember than 111101101. <br></p>
</li>
</ul>
<br>
<ul>
<li>
<p><strong>Hexadecimal (Base 16)</strong><br></p>
<p>Hex is like binary&rsquo;s cooler, more compact cousin. In Hex, each place represents a multiple of 16 made up of the &ldquo;numbers&rdquo; <code>0-F</code>. <br></p>
<p>An example most of us have encountered is a web color code like <code>#FF69B4</code> where we express Red Green and Blue values with a pair of hex digits. So in our example, FF=max red, 69=some green, B4=bluish purple. Also <code>#FFFFFF</code> means maximum RGB which equals white and <code>#000000</code> is black or 00 red, 00 blue and 00 green. no color.</p>
<figure>
      <img loading="lazy" src="hex-kernel.png"
           alt="Hexdump of the Linux kernel"/> <figcaption>
              <p>Linux kernel (vmlinuz) laid bare using xxd.</p>
          </figcaption>
  </figure>

<p><span class="tag green">Pro-Tip:</span> <code>0x</code> is the universal hex prefix. So, 0x2A is 42 in hex.</p>
</li>
</ul>
<br>
<ul>
<li>
<p><strong>Sexagesimal (Base 60)</strong> <br></p>
<p>Invented by the Sumerians, who may have had 60 fingers although the archaeological record is silent on the subject. This shows up in all kinds of places</p>
<ul>
<li>60 seconds in a minute</li>
<li>60 minutes in an hour</li>
<li>360° in a circle (6x60 - tidy for navigation)</li>
</ul>
<p>Base-60 is fantastic for dividing things evenly—60 has a lot of divisors (2,3,4,5,6,10,12,15,20,30). In other words, you can *split an hour or a circle into neat halves, thirds, quarters without ugly fractions. Computers don&rsquo;t use it but every time you say &ldquo;quarter past three&rdquo;, you&rsquo;re quietly doing Sumerian math.
<br></p>
</li>
</ul>
<p>Here&rsquo;s a little demo that converts 42, the Ultimate Answer to Life, the Universe, and, well Everything, into different number systems:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl">  <span class="c1">#!/usr/bin/env python3</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">to_sexagesimal</span><span class="p">(</span><span class="n">n</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Convert an integer to base‑60 (sexagesimal).&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">digits</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="k">while</span> <span class="n">n</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">digits</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">n</span> <span class="o">%</span> <span class="mi">60</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">n</span> <span class="o">//=</span> <span class="mi">60</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">list</span><span class="p">(</span><span class="nb">reversed</span><span class="p">(</span><span class="n">digits</span><span class="p">))</span> <span class="ow">or</span> <span class="p">[</span><span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">n</span> <span class="o">=</span> <span class="mi">42</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Decimal: </span><span class="si">{</span><span class="n">n</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Binary: </span><span class="si">{</span><span class="nb">bin</span><span class="p">(</span><span class="n">n</span><span class="p">)</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>      <span class="c1"># 0b101010</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Octal: </span><span class="si">{</span><span class="nb">oct</span><span class="p">(</span><span class="n">n</span><span class="p">)</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>       <span class="c1"># 0o52</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Hex: </span><span class="si">{</span><span class="nb">hex</span><span class="p">(</span><span class="n">n</span><span class="p">)</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>         <span class="c1"># 0x2a</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">sexagesimal</span> <span class="o">=</span> <span class="n">to_sexagesimal</span><span class="p">(</span><span class="n">n</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Sexagesimal: </span><span class="si">{</span><span class="n">sexagesimal</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>  <span class="c1"># single digit since 42 &lt; 60</span>
</span></span></code></pre></div><p><strong>Demo Output</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Decimal: 42
</span></span><span class="line"><span class="cl">Binary: 0b101010
</span></span><span class="line"><span class="cl">Octal: 0o52
</span></span><span class="line"><span class="cl">Hex: 0x2a
</span></span><span class="line"><span class="cl">Sexagesimal: 42
</span></span></code></pre></div><hr>
<h2 id="mind--memory-quirks">Mind &amp; Memory Quirks</h2>
<p>“These words feel rare and academic, but the experiences aren’t — everyone brushes against them, whether or not they have the names for them.”</p>
<ul>
<li>
<p><strong>Déjà Vu</strong><br>
<em>The feeling you’ve experienced something before.</em>
This obviously means they&rsquo;ve changed something in the Matrix.</p>
</li>
<li>
<p><strong>Déjà Vécu</strong><br>
<em>A stronger form of déjà vu — feeling an event has fully “already been lived.”</em>
I experienced this when I watched Ronald D. Moore&rsquo;s Apple+ series, &lsquo;For All Mankind&rsquo;. It was brand new but I felt like I had seen it years before in all these minute details. It was a strange sensation. Great show though!</p>
</li>
<li>
<p><strong>Cryptomnesia</strong><br>
<em>Thinking an idea is new when it’s actually an old memory resurfacing.</em></p>
</li>
<li>
<p><strong>Presque Vu</strong><br>
<em>The “tip of the tongue” state — almost recalling something but not quite.</em>
Ironically, I struggled to remember the name of this term and had to get creative to look it up. Meta presque vu!</p>
</li>
<li>
<p><strong>Jamais Vu</strong><br>
<em>The opposite of déjà vu — familiar things feel strangely unfamiliar.</em>
Have you ever experienced this? Sometimes I have to look up how to spell a simple word because it just looks wrong suddenly.</p>
</li>
<li>
<p><strong>Apophenia</strong><br>
<em>Seeing patterns or connections in random data.</em>
The spark that leads to constellations in the sky — or conspiracy boards covered with red string. A fine line between genius and madness.</p>
</li>
<li>
<p><strong>Hypergraphia</strong><br>
<em>The compulsive urge to write constantly.</em>
The huge stack of notebooks on my bookshelf attests to a certain level of hypergraphia on my part.</p>
</li>
</ul>
<hr>
<h2 id="language--literature">Language &amp; Literature</h2>
<p>“These are the words for books, language, and the way we speak — the kind of nerdy treasures you tuck into your brain for later.”</p>
<ul>
<li>
<p><strong>Idiolect</strong><br>
<em>Your personal dialect — the quirks and words only you use.</em>
You can write that ransom note using letters cut out of magazines, but they might still catch you based on your unique and curious wording!</p>
</li>
<li>
<p><strong>Palimpsest</strong><br>
<em>A page or manuscript reused and overwritten, but never fully erased.</em>
From Roman wax tablets to parchment scrolls to books to sheet music to paintings. It&rsquo;s amazing what modern technology can reveal.</p>
</li>
</ul>
<figure style="text-align:center; margin: 1em auto;">
  <img src="Codex_Nitriensis.jpg" alt="The Codex Nitriensis" style="display:block; margin:0 auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    The Codex Nitriensis is an example of a 
    <a href="https://en.wikipedia.org/wiki/Palimpsest" style="color:#666; text-decoration:underline;">palimpsest</a>
    containing earlier fragments of the Gospel of Luke, the <em>Iliad</em>, and Euclid's <em>Elements</em>. 
    Parchment and paper were expensive, so they were often reused.
  </figcaption>
</figure>
<ul>
<li>
<p><strong>Term of Art</strong><br>
<em>A phrase with a hyper‑specific meaning in one field that sounds plain to everyone else.</em>
I learned this from a librarian. It&rsquo;s really helpful to know when you&rsquo;re searching for a very specialized term in a field like medicine or anthropology. The word &lsquo;significant&rsquo; has a different connotation to statisticians than laymen for instance.</p>
</li>
<li>
<p><strong>Jargon</strong><br>
<em>Specialist language that’s either shorthand — or a wall keeping others out.</em>
The <a href="https://www.urbandictionary.com/">Urban Dictionary</a> is a collection of all kinds of fun and off-color Jargon. Now you can figure out what these dang kids are talking about.</p>
</li>
<li>
<p><strong>Codex</strong><br>
<em>The ancient upgrade from scroll to book — bound pages, the start of the “book” as we know it.</em>
I&rsquo;ve named more than a few notebooks and fileshares, Codex.</p>
</li>
<li>
<p><strong>Meta</strong><br>
<em>Something that refers to itself — this entry about “meta” is, well, meta.</em>
Everything is meta these days so it&rsquo;s not a novel term but it comes up everywhere in computer science.
To wit, the MIT CADR Space Cadet keyboard. I would love to own one of these. It has a Meta button as well as an infinity symbol and a Rub Out key.</p>
</li>
</ul>
<figure style="text-align:center; margin: 1em auto;">
  <img src="Space-cadet.jpg" alt="The MIT Space-Cadet Keyboard" style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <em>The legendary MIT Space‑Cadet Keyboard — complete with Meta, Super, and Hyper keys.</em><br>
    Image via <a href="https://commons.wikimedia.org/wiki/File:Space-cadet.jpg" style="color:#666; text-decoration:underline;">Wikimedia Commons</a> (CC BY‑SA 3.0)
  </figcaption>
</figure>
<ul>
<li>
<p><strong>Semiotics</strong><br>
<em>The study of signs, symbols, and how meaning is made.</em>
I went down this rabbit hole in school once. It&rsquo;s a deep and fascinating subject. <a href="https://en.wikipedia.org/wiki/Semiotics">Semiotics</a> is important to UX designers. Making an effective icon or logo or an entire operating system relies on a good working knowledge of this topic.</p>
</li>
<li>
<p><strong>Apotheosis</strong><br>
<em>Elevating someone or something to divine or perfect status.</em>
Apotheosis is a semiotic event: something normal gets so loaded with meaning it becomes iconic or even sacred. From holy symbols to heraldic crests, from flags to tux the Linux mascot. Many symbols and even some people achieve apotheosis in their cultures.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="Tux.png" alt="Tux the Linux Penguin mascot" style="display:block; margin:0 auto; max-width:300px;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <em>Tux — the Linux mascot, designed by Larry Ewing in 1996 using GIMP.</em><br>
    Image via <a href="https://commons.wikimedia.org/wiki/File:Tux-flat.svg" style="color:#666; text-decoration:underline;">Wikimedia Commons</a> (CC BY‑SA 3.0)<br>
    <span style="font-size:80%">Original design: Larry Ewing, &ldquo;Permission to use and/or modify this image is granted provided you acknowledge me as the original author.&rdquo;</span>
  </figcaption>
</figure>
</li>
<li>
<p><strong>Lacuna</strong><br>
<em>A missing piece, a gap — in a manuscript, a memory, or an argument.</em>
Interestingly, a gap can tell us a lot when we ask why it is missing. In modern times, where we record and track practically everything, a lacuna can indicate an attempt to conceal something. For example, a hacker might attempt to cover their tracks by deleting log files. The mere fact that entries are missing can be a big clue.
<br><br></p>
<p>A type of lacuna that I have always found fascinating are things like the ellipsis (&hellip;), placeholders that invite the reader to fill in meaning. Not to mention, there was an entire Seinfeld episode (season 8, episode 19) about the phrase &ldquo;yadda, yadda, yadda&rdquo;, which acts as a verbal elipsis. Placeholders, unobtanium, and &rsquo;not yet invented&rsquo; signs abound and serve a useful function—there is information in the absence of information, sometimes.</p>
</li>
</ul>
<hr>
<h2 id="philosophy--thought">Philosophy &amp; Thought</h2>
<p>“These words are the ones you stumble into in philosophy books, but they leak into tech, admin life, and just trying to make sense of the world.”</p>
<ul>
<li>
<p><strong>Liminal</strong><br>
<em>Being in between states or spaces — neither here nor there.</em>
Typically seen in reference to creepy spaces like a deserted playground or hotel corridors. Has to do with thresholds but can also refer to everything from adolescence to the state between waking and sleeping. This is most often encountered in the word subliminal, meaning something that is just below your awareness, like product placement in the new Marvel movie.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="liminal_spaces_by_neuralcanvas_dgvyu10-pre.jpg" alt="A digital artwork depicting liminal spaces" style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <em>"Liminal Spaces" — digital artwork by <a href="https://www.deviantart.com/neuralcanvas" style="color:#666; text-decoration:underline;">neuralcanvas</a>.</em><br>
    Image via <a href="https://www.deviantart.com/neuralcanvas/art/Liminal-Spaces-1021152132" style="color:#666; text-decoration:underline;">DeviantArt</a> (© neuralcanvas, all rights reserved)
  </figcaption>
</figure>
</li>
<li>
<p><strong>Praxis</strong><br>
<em>Where theory and practice meet — actually doing the thing, not just talking about it.</em>
I spent a lot of years doing support at every level and the worst jobs were the ones where you couldn&rsquo;t get hands on. Reading manuals and case logs and wiki articles and doing training can only prepare you so much. For it to really click with me, I have to get my hands dirty. The lack of praxis is now a red flag for me. I just don&rsquo;t enjoy the hands-off, solely academic approach.</p>
</li>
<li>
<p><strong>Potemkin Village</strong><br>
<em>A pretty façade hiding a mess behind it — fake dashboards, anyone?</em>
I&rsquo;m sure we&rsquo;ve all encountered software that has a pretty interface but is buggy and poorly written. Named for Grigory Potemkin, a soldier, courtier, and lover of Catherine the Great in the 18th century, a Potemkin village refers to the hasty, surface-level beautification of villages undertaken along the route of a tour by Catherine. It only had to look good at a glance. You often see this when countries host the Olympics and put their best face forward with expensive stadiums and architecture that is never meant to last. North Korea is notorious for its Potemkin villages that it keeps around to show visitors.</p>
<p>I present to you, a Potemkin Python script:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># &#34;Potemkin dashboard&#34; generator: Looks real, does nothing</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">show_dashboard</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">  <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;CPU: 100</span><span class="si">% F</span><span class="s2">REE | Disk: Unlimited | Uptime: ∞&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># No actual metrics collected but it sure feels good at a glance!</span>
</span></span></code></pre></div><figure style="text-align:center; margin: 1em auto;">
  <img src="960px-Castle_and_brewery_in_Kolín_2.jpg" alt="Castle and brewery in Kolín, Czech Republic" style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <em>Castle and brewery in Kolín, Czech Republic.</em><br>
    Image via <a href="https://commons.wikimedia.org/wiki/File:Castle_and_brewery_in_Kol%C3%ADn_2.jpg" style="color:#666; text-decoration:underline;">Wikimedia Commons</a> (CC BY-SA 4.0, by ŠJů / Wikimedia Commons)
  </figcaption>
</figure>
</li>
<li>
<p><strong>Samsara</strong><br>
<em>The endless cycle of birth, death, and rebirth.</em>
In Buddhist and Hindu traditions, samsara is what keeps souls trapped on the hamster wheel of existence—bound by karma, desire, and illusion.</p>
</li>
<li>
<p><strong>Nirvana</strong><br>
<em>Release from samsara — ultimate liberation.</em>
The escape hatch from Samsara. To reach nirvana is to step off the wheel, unplug from the simulation, and dissolve all attachments. Not a place, but a state: the complete extinguishing of suffering, craving, and the illusions that fuel the cycle. That&rsquo;s not to mention an amazing band who found the concept inspirational.</p>
</li>
<li>
<p><strong>Shunyata</strong><br>
<em>“Emptiness” — the idea that things lack inherent, permanent essence.</em>
Pretty much the opposite of Plato&rsquo;s ideal forms—Shunyata holds that nothing has a fixed essence. This isn&rsquo;t meant as a bad thing, rather it holds that all things are open, relational, and free from rigid definition. I hope you enjoyed my ironically rigid definition of Shunyata.</p>
</li>
</ul>
<hr>
<h2 id="miscellany">Miscellany</h2>
<ul>
<li>
<p><strong>Samizdat</strong><br>
<em>Underground, hand‑copied, often banned literature passed quietly between readers.</em><br>
a Russian word that referred to all manner of illicit material. I met a guy who grew up in Ukraine during the Soviet days and he told me about the first time he read the novel, &lsquo;Dune&rsquo;. It was photocopied and bound in the cover of a book about Socialist values. People had traded it around and even written in better translations and traced over faded parts.
<br><br></p>
<p>A related phenomenon (and an object I would dearly love to possess one day) was the X-ray record. Clever Soviet scofflaws figured out how to cut record grooves into discarded x-ray films that you could then play on a phonograph. Imagine hearing the Beatles for the first time on an old chest X-ray. These were sometimes referred to as Jazz on Bones (Джаз на костях) or Bone Music. The <a href="https://en.wikipedia.org/wiki/Stilyagi">Stilyagi</a>—or style-hunters—were especially fond of these.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="Russian_samizdat_and_photo_negatives_of_unofficial_literature_in_the_USSR.jpg" alt="Samizdat and photo negatives of unofficial literature in the USSR" style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <em>Samizdat and photo negatives of unofficial literature in the USSR, circa 1984.</em><br>
    Image via <a href="https://commons.wikimedia.org/wiki/File:Russian_samizdat_and_photo_negatives_of_unofficial_literature_in_the_USSR.jpg" style="color:#666; text-decoration:underline;">Wikimedia Commons</a> (CC BY-SA 4.0, by Leonid Evdokimov)
  </figcaption>
</figure>
</li>
<li>
<p><strong>Hemingway vs Faulkner</strong><br>
<em>Two poles of prose: Hemingway’s spare, staccato sentences vs. Faulkner’s winding, baroque tangles.</em><br>
I had a mentor once who explained the proper way to write a bug report: &ldquo;Hemingway not Faulkner.&rdquo; It made me a better engineer for sure.</p>
</li>
<li>
<p><strong>NATO–1</strong><br>
<em>A term for a customer who almost uses NATO phonetics correctly—but throws in a “U for Umbrella.”</em><br>
It&rsquo;s been suggested I clarify that the term is NATO minus one since jokes are always funnier when you explain them. Oy.
So, the NATO phonetic alphabet (Alpha, Bravo, Charlie, Delta, etc.) was carefully designed so that each letter is distinct over noisy radio connections in a multitude of accents. It&rsquo;s incredibly effective so of course it is a rare unicorn of a customer who uses it correctly, unless they were military or a pilot. It really, truly helps though.</p>
<p>All too often a customer will spell out a serial number or some important piece of information with the worst possible choices rather than using Nato phonetics. Think &ldquo;S as in See&rdquo;, or &ldquo;E as in Eye&rdquo;, or &ldquo;T as in Tea&rsquo;. I even heard &ldquo;C as in Czar&rdquo; once!</p>
</li>
</ul>
<hr>
<h2 id="conclusion">Conclusion</h2>
<p>Got a word, workflow, or mind-bender that changed your admin life?</p>
<p style="text-align:left;">
  <a href="/tags/cromulent" class="button">🧾 View Cromulent Words Series</a>
</p>
<p>Email me: <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a><br>
Or just send your best yak-shaving story.</p>
<hr>
]]></content:encoded>
    </item>
    <item>
      <title>Alias Magic</title>
      <link>https://adminjitsu.com/posts/alias-magic/</link>
      <pubDate>Fri, 01 Aug 2025 00:09:26 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/alias-magic/</guid>
      <description>A real-world collection of Bash and Zsh aliases, explained with humor and care. Skip the tired ll/la lists — these are the shortcuts I use daily to make the shell faster, friendlier, and a little more fun.</description>
      <content:encoded><![CDATA[<h2 id="aliases-and-you">Aliases and You</h2>
<p>In case you haven&rsquo;t encountered one before, an <strong>alias</strong> is just a <strong>nickname for a longer command</strong>. Whenever you invoke an alias by typing its name in your shell, Bash (or Zsh) simply substitutes the alias text before execution. That&rsquo;s it.</p>
<p>It&rsquo;s like the old joke:</p>
<blockquote>
<p>Two old friends have been telling each other the same old jokes for decades. Eventually they tire of the long setups and just number the jokes.</p>
<p>While sitting on the porch, talking, one friend exclaims, &ldquo;37&rdquo;.
The other friend howls with laughter: &ldquo;Oh, that&rsquo;s a classic!&rdquo;</p>
<p>Later a newcomer hears about the system and wants to join in. He pipes up nervously: &ldquo;42&rdquo;</p>
<p>The room falls silent. Finally one of the old friends shrugs and says, &ldquo;Well&hellip; it&rsquo;s all in how you tell it&rdquo;</p></blockquote>
<p><em>That&rsquo;s pretty much how an alias works</em></p>
<h2 id="-my-aliases">🙃 My aliases</h2>
<p>Here are some aliases I&rsquo;ve found useful—hopefully you&rsquo;ll find a few that earn a spot in your own shell too. If you do want to try these out you can do so by entering them directly into your shell. After that you can invoke the alias by name for as long as the shell session lasts. If you want them to always be available, you can add them to your startup files (e.g., <code>.bashrc</code> or <code>.zshrc</code>) and reload. If you decide you don&rsquo;t like an alias, you can always run unalias followed by the alias name (not including the = sign or the text to the right of it)</p>
<h3 id="overriding-built-in-commands">Overriding built-in commands.</h3>
<p>The first batch overrides built-in commands. So you can type <code>ll</code> and it will be substituted for <code>ls -LF --color=auto</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">vi</span><span class="o">=</span><span class="s1">&#39;vim&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">l</span><span class="o">=</span><span class="s1">&#39;ls -CF --color=auto&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">ll</span><span class="o">=</span><span class="s1">&#39;ls -LF --color=auto&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">lf</span><span class="o">=</span><span class="s1">&#39;find . -type f ! -path &#34;*/.git/*&#34; ! -path &#34;*/venv/*&#34;&#39;</span>
</span></span></code></pre></div><p>With those aliases set, you can simply type <code>lf</code> or <code>ll</code> instead of trying to remember the full command. These overriden commands can be really handy, especially on destructive commands like <code>rm</code> which have safety flags, like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">rm</span><span class="o">=</span><span class="s1">&#39;rm -i&#39;</span>
</span></span></code></pre></div><h3 id="process-aliases">Process aliases</h3>
<p>These make some really powerful, but let&rsquo;s face it, un-memorable ps commands trivial to use</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">ptop</span><span class="o">=</span><span class="s1">&#39;ps aux --sort=-%mem | head -n 20&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">pmem</span><span class="o">=</span><span class="s1">&#39;ps aux --sort=-%mem | head -n 20 | awk &#39;</span><span class="se">\&#39;</span><span class="s1">&#39;{print $1, $4, $11}&#39;</span><span class="se">\&#39;</span><span class="s1">&#39; | column -t&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">pscpu</span><span class="o">=</span><span class="s1">&#39;ps aux --sort=-%cpu | head -n 20 | awk &#39;</span><span class="se">\&#39;</span><span class="s1">&#39;{print $1, $3, $11}&#39;</span><span class="se">\&#39;</span><span class="s1">&#39; | column -t&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">pfor</span><span class="o">=</span><span class="s1">&#39;ps auxf --forest&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">psme</span><span class="o">=</span><span class="s1">&#39;ps -u $(whoami) -o pid,etime,cmd --sort=-etime | head&#39;</span>
</span></span></code></pre></div><h3 id="launch-jupyter-notebook-server">Launch Jupyter Notebook Server</h3>
<p>This will launch jupyter without trying to open a browser tab and instead display the address to connect to and the auth token</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">alias</span> jupyter-notebook<span class="o">=</span><span class="s2">&#34;/usr/local/bin/jupyter-notebook --no-browser&#34;</span>
</span></span></code></pre></div><p align="center">
  <img src="another-local-wizard.jpg" alt="a purple wizard" width="283">
</p>
<h3 id="python-virtualenv">Python virtualenv</h3>
<p>The following aliases make it really easy to work with Python tools that require a virtual envirornment. <code>venv</code> will drop a venv folder in the current directory. va and vd activate and deactivate your venv automatically.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">venv</span><span class="o">=</span><span class="s1">&#39;python3 -m venv venv&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">va</span><span class="o">=</span><span class="s1">&#39;source venv/bin/activate&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">vd</span><span class="o">=</span><span class="s1">&#39;deactivate&#39;</span>
</span></span></code></pre></div><h3 id="git">Git</h3>
<p>Once upon a time I would have scoffed at the following since some of them like <code>ga</code> for <code>git add</code> seem pathetically simple. Try typing git add for the thousandth time and you too will appreciate a two letter alias that you can run with something like <code>ga .</code> or <code>glo</code> or <code>gc -m &quot;The next big thing&quot;</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">gs</span><span class="o">=</span><span class="s1">&#39;git status&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">glo</span><span class="o">=</span><span class="s1">&#39;git log --oneline --graph --decorate --all&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">ga</span><span class="o">=</span><span class="s1">&#39;git add&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">gc</span><span class="o">=</span><span class="s1">&#39;git commit&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">gd</span><span class="o">=</span><span class="s1">&#39;git diff&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">gco</span><span class="o">=</span><span class="s1">&#39;git checkout&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">gp</span><span class="o">=</span><span class="s1">&#39;git push&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">gl</span><span class="o">=</span><span class="s1">&#39;git pull&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">gitcha</span><span class="o">=</span><span class="s1">&#39;gcheck&#39;</span> <span class="c1"># an alias i&#39;m used to for the gcheck function</span>
</span></span></code></pre></div><p>The gitcha alias is one where I had a simple git status &amp;&amp; git remote alias that I used all the time until I built a way better function called gcheck. I made the alias so I can type what I&rsquo;m used to and run the new code. With that I can either invoke my <code>gcheck</code> function or call that same function with the command <code>gitcha</code>. Nice.</p>
<h3 id="docker">Docker</h3>
<p>Docker supports some format options that make the output way more readable but the commands are tricky to remember. Instead of copy/paste, you can make an alias. It&rsquo;s easier to remember and thus you are more likely to use it once you get used to typing the simple alias.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">docker_ps</span><span class="o">=</span><span class="s1">&#39;docker ps --format &#34;table {{.Names}}\t{{.Image}}&#34;&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">docker_net</span><span class="o">=</span><span class="s1">&#39;docker ps --format &#34;table {{.Names}}\t{{.Ports}}&#34;&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">docker_images</span><span class="o">=</span><span class="s1">&#39;docker images --format &#34;table {{.Repository}}\t{{.Size}}&#34;&#39;</span>
</span></span></code></pre></div><h3 id="roll-your-own-cli-trash-can">Roll your own CLI trash can.</h3>
<p>This is handy but you also need to manualy clear the trash folder you create or write a cron or systemd job to do it on a schedule.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">rm</span><span class="o">=</span><span class="s1">&#39;mv -t ~/.trash&#39;</span>
</span></span></code></pre></div><h3 id="aliases-can-call-each-other">aliases can call each other</h3>
<p>Yes, an alias can invoke another alias—but <strong>the callee must be defined first</strong> in your shell startup file.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Define the one to be called first</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">aliasg</span><span class="o">=</span><span class="s1">&#39;alias | grep&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Now this alias can call the first one</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">gitaliases</span><span class="o">=</span><span class="s1">&#39;aliasg git&#39;</span>
</span></span></code></pre></div><p>Example use once the alias exists:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ gitaliases
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">g</span><span class="o">=</span><span class="s1">&#39;git&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">ga</span><span class="o">=</span><span class="s1">&#39;git add&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">gc</span><span class="o">=</span><span class="s1">&#39;git commit&#39;</span>
</span></span><span class="line"><span class="cl">...
</span></span></code></pre></div><h3 id="misc-tools">Misc tools</h3>
<p>These two are great if you find yourself working with conf files for things like Apache and sshd that include tons of commented optional values. It can be helpful to see just the non-comment statements. <code>nocomment</code> has you covered. If you find that you want to see only the comments, you can do that too with <code>onlycomments</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">onlycomments</span><span class="o">=</span><span class="s1">&#39;grep &#34;^#&#34;&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">nocomment</span><span class="o">=</span><span class="s1">&#39;grep -v &#34;^\s*#&#34;&#39;</span>
</span></span></code></pre></div><br>
<p>This one is insanely useful. It launches a mini webserver that serves the current folder over HTTP.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">serve</span><span class="o">=</span><span class="s1">&#39;python3 -m http.server 8000&#39;</span>
</span></span></code></pre></div><p><span class="tag red">Check out</span> my <a href="/posts/hero-commands/">Hero Commands</a> post for more awesome Python tools.</p>
<hr>
<p>This is a quicky but easy to remember way to capture network traffic. <code>tcpdump</code> has a ton of <a href="https://man.openbsd.org/tcpdump.8?utm_source=chatgpt.com">options</a> that you can view with <code>man tcpdump</code> but sniff is a really easy command to remember.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">sniff</span><span class="o">=</span><span class="s1">&#39;sudo tcpdump -i any -n -s 0 -vv&#39;</span>
</span></span></code></pre></div><p>This is pretty handy for troubleshooting path issues in your dotfiles. It displays each path in the $PATH variable on it&rsquo;s own line which is far more readable.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">ppath</span><span class="o">=</span><span class="s1">&#39;echo &#34;$PATH&#34; | tr &#34;:&#34; &#34;\n&#34;&#39;</span>
</span></span></code></pre></div><p>A few more for fun.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">weather</span><span class="o">=</span><span class="s2">&#34;curl -s &#39;wttr.in/Austin?0&amp;n&amp;Q&amp;lang=en&amp;columns=80&#39;&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">shrug</span><span class="o">=</span><span class="s1">&#39;echo &#34;¯\\_(ツ)_/¯&#34;&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">showgpt</span><span class="o">=</span><span class="s2">&#34;list_chatgpt_zips&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> list-disks<span class="o">=</span><span class="s1">&#39;lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT,LABEL&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> zram-info<span class="o">=</span><span class="s1">&#39;watch -n 1 &#34;free -h; echo; zramctl; echo; swapon --show&#34;&#39;</span>
</span></span></code></pre></div><h2 id="a-few-more-complex-examples">A Few More Complex Examples</h2>
<p>
<figure class="shadowed" style="left; max-width:280px; margin:0 1rem 1rem 0;">
  <img src="local-wizard.jpg"
       alt="a local wizard"
       style="width:100%; height:auto; display:block;">
  <figcaption style="font-size:85%; color:#666; line-height:1.4; margin-top:.4em;">
    <em>Do you feel like a Unix wizard yet?</em>
  </figcaption>
</figure>
<br>
<p>On my network, I routinely jump around between different environments. It&rsquo;s nice to have safeguards to make sure a command exists before aliasing it or to check for the OS and set the alias accordingly. Some examples:</p>
<h3 id="set-motd-neofetch-and-fortune-command-if-available">Set MOTD (neofetch) and fortune command if available</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">if</span> <span class="nb">command</span> -v neofetch &gt;/dev/null<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">export</span> <span class="nv">DOTFILES_MOTD_CMD</span><span class="o">=</span><span class="s1">&#39;neofetch&#39;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Set fortune command</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nb">command</span> -v fortune &gt;/dev/null 2&gt;<span class="p">&amp;</span>1<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">export</span> <span class="nv">DOTFILES_FORTUNE_CMD</span><span class="o">=</span><span class="s1">&#39;fortune&#39;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span></code></pre></div><p>Sometimes I want to make sure the same alias is available on different OSes with different commands. The best way I have found is with an if statement that checks the output of the <code>uname -s</code> command</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># macOS-style `say` command on Linux using espeak</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="k">$(</span>uname -s<span class="k">)</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;Linux&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">command</span> -v espeak &gt;/dev/null<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">alias</span> <span class="nv">say</span><span class="o">=</span><span class="s1">&#39;espeak -s 160 -p 60 -v en-us+f2&#39;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span></code></pre></div><p>This can be as elaborate as you like. In this example I can combine the technique above with branching logic, like so:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 🖇 Clipboard alias setup for Bash/Zsh</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Detect OS via uname and set up clip/paste aliases accordingly</span>
</span></span><span class="line"><span class="cl"><span class="k">case</span> <span class="s2">&#34;</span><span class="k">$(</span>uname -s<span class="k">)</span><span class="s2">&#34;</span> in
</span></span><span class="line"><span class="cl">    Darwin<span class="o">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># macOS</span>
</span></span><span class="line"><span class="cl">        <span class="nb">alias</span> <span class="nv">clip</span><span class="o">=</span><span class="s1">&#39;pbcopy&#39;</span>
</span></span><span class="line"><span class="cl">        <span class="nb">alias</span> <span class="nv">paste</span><span class="o">=</span><span class="s1">&#39;pbpaste&#39;</span>
</span></span><span class="line"><span class="cl">        <span class="p">;;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    Linux<span class="o">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Linux (assumes xclip is installed)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nb">command</span> -v xclip &gt;/dev/null 2&gt;<span class="p">&amp;</span>1<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">            <span class="nb">alias</span> <span class="nv">clip</span><span class="o">=</span><span class="s1">&#39;xclip -selection clipboard&#39;</span>
</span></span><span class="line"><span class="cl">            <span class="nb">alias</span> <span class="nv">paste</span><span class="o">=</span><span class="s1">&#39;xclip -selection clipboard -o&#39;</span>
</span></span><span class="line"><span class="cl">        <span class="k">elif</span> <span class="nb">command</span> -v xsel &gt;/dev/null 2&gt;<span class="p">&amp;</span>1<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># fallback to xsel if xclip is missing</span>
</span></span><span class="line"><span class="cl">            <span class="nb">alias</span> <span class="nv">clip</span><span class="o">=</span><span class="s1">&#39;xsel --clipboard --input&#39;</span>
</span></span><span class="line"><span class="cl">            <span class="nb">alias</span> <span class="nv">paste</span><span class="o">=</span><span class="s1">&#39;xsel --clipboard --output&#39;</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span>
</span></span><span class="line"><span class="cl">            <span class="nb">echo</span> <span class="s2">&#34;[WARN] No clipboard tool found (install xclip or xsel for clip/paste aliases)&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">fi</span>
</span></span><span class="line"><span class="cl">        <span class="p">;;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    *<span class="o">)</span>
</span></span><span class="line"><span class="cl">        <span class="nb">echo</span> <span class="s2">&#34;[INFO] Clipboard aliases not set: unsupported OS (</span><span class="k">$(</span>uname -s<span class="k">)</span><span class="s2">)&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">;;</span>
</span></span><span class="line"><span class="cl"><span class="k">esac</span>
</span></span></code></pre></div><h2 id="conclusion">Conclusion</h2>
<p>Aliases are one of the great things about the Linux and Mac CLI. They let you make complex commands memorable, repetitive commands shorter, and can even be quite smart with some shell logic. The other great thing about aliases are that they&rsquo;re <strong>living documentation</strong>. Instead of writing some long, hero command down and relying on copy/paste, you can simply set an <code>alias</code>. If you ever want to see what aliases you have defined, just run the command <code>alias</code> with no arguments and it will list them.</p>
<p>That&rsquo;s it. Alias magic isn&rsquo;t just for Unix wizards—it&rsquo;s for power-users and everyday Joe and Jill Six-Pack who just want to get things done faster and easier.</p>
<p>💌 Got a favorite alias I didn’t cover? <a href="mailto:feedback@adminjitsu.com">Send it my way</a></p>
<p>Good aliases are meant to be stolen. <em>May they profit you!</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Meet GIR</title>
      <link>https://adminjitsu.com/posts/meet-gir/</link>
      <pubDate>Thu, 31 Jul 2025 13:17:24 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/meet-gir/</guid>
      <description>Meet GIR: the Raspberry Pi 5 in a Pironman5 case that runs my Docker fleet, handles nightly backups, and even has a secret GUI. Here’s how I built it—and why it’s named after a cartoon robot.</description>
      <content:encoded><![CDATA[<h2 id="it-all-started-with-a-power-surge">It All Started with a Power Surge</h2>
<p>Everything was humming along smoothly until one day earlier this year when we had a big, dramatic, Texas thunderstorm. Lightning caused a transformer to explode and the resulting power surge took out my old x86 server, a raspberry pi that I was using for games with retropie, a big hard drive full of data, an IPS monitor and the AV receiver connected to my PC (It also took out all the outlets in the garage but that&rsquo;s another story).</p>
<blockquote>
<p><em>“Needless to say, I was despondent about the meltdown. In the midst of my preparations for hari-kari&hellip; it came to me.”</em> — <em>Real Genius</em></p></blockquote>
<p>After arriving at the Acceptance stage of grief, I started to dream up a newer, better, <em>cheaper</em> replacement. Before long a package arrived with my new server: a raspberry pi5, a Pironman 5 case and a 1TB NVME SSD.</p>
<h2 id="meet-gir">Meet GIR</h2>
<p>Named for Invader Zim&rsquo;s insane robot sidekick (who often disguised himself in a zip-up, green dog costume), GIR proved to be a powerful replacement. Built around the concept of memento mori, everything is managed, backed up twice and stored in git. It has become a sort of Docker mothership and glue layer for my homelab and one of my favorite machines.</p>
<p align="center">
  <img src="gir.png" alt="gir the insane robot" width="350">
  <i>© Nickelodeon / Jhonen Vasquez</i>
</p>
<hr>
<h2 id="the-hardware">The Hardware</h2>
<ul>
<li><strong>Raspberry Pi 5</strong> — These little hobbyist boards are relatively cheap and powerful and make a decent linux box.</li>
<li><strong>1TB NVMe SSD</strong> — NVMe SSDs are fast and fairly inexpensive these days</li>
<li><strong>Pironman5 case</strong> — This case has it all and then some.</li>
<li><strong>External USB drive</strong> — I have a 10tb drive for file sharing but it strains the I/O on this board so I&rsquo;ll probably relocate it soon.</li>
</ul>
<hr>
<h2 id="the-pironman5-case">The Pironman5 Case</h2>
<p>The Pironman 5 case is a superb alternative to the typical plastic cases available for the pi. While there is definitely some assembly required, it provides all the features I needed in an attractive case.</p>
<ul>
<li>Active cooling (a beefy heatsink covers the main chips while a trio of fans ensure that your Pi stays nice and cool)</li>
<li>OLED screen (features a tiny, programmable oled screen that by default shows ip addresses, cpu and memory and temps right on the case)</li>
<li>NVMe support (support for NVMe drives is somewhat rare so this was a huge feature)</li>
<li>Looks cool (the “Saturday morning cartoon villain HQ” vibe)</li>
</ul>
<p>If you&rsquo;re in the market for a cool case, you should check out the reviews, like this one from <a href="https://www.tomshardware.com/raspberry-pi/raspberry-pi-cases/sunfounder-pironman-5-review">tom&rsquo;s HARDWARE</a></p>
<p align="center">
  <img src="pironman.jpg" alt="pironman 5 case" width="500">
</p>
<hr>
<h2 id="-the-docker-stack">🐋 The Docker Stack</h2>
<p>Most of GIR’s magic comes from Docker. Each service lives in its own container — easy to maintain, easy to update, no dependency tangles.</p>
<p>Here’s how I keep things organized: grouped by what they do, with one‑line descriptions. If you want the <strong>nitty‑gritty details</strong>, expand the sections for notes, tricky bits, and docker compose blocks. For a casual read, there is no need to expand every one unless you want to play Sim City with containers like I have.</p>
<hr>
<h3 id="-system-tools">🛠 System Tools</h3>
<p><span class="tag teal">Pi-hole</span> – DNS server and ad-blocker for the whole network.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <ul>
<li>Runs as the primary DNS server.</li>
<li>Blackholes ad domains &amp; telemetry.</li>
<li>Compose notes: maps <code>/etc/dnsmasq.d</code> and <code>/etc/pihole</code> for persistent config.</li>
</ul>
<p>Docker Compose:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;3.3&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pihole</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">pihole</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">pihole/pihole:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">hostname</span><span class="p">:</span><span class="w"> </span><span class="l">pihole</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;53:53/tcp&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;53:53/udp&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;8080:80/tcp&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;8443:443/tcp&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">TZ</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;America/Chicago&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">WEBPASSWORD</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;${WEBPASSWORD}&#34;</span><span class="w">          </span><span class="c"># stored in a .env file</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">DHCP_ROUTER</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;192.168.50.1&#34;</span><span class="w">            </span><span class="c"># example router IP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">VIRTUAL_HOST</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;pihole.example.lan&#34;</span><span class="w">     </span><span class="c"># safe placeholder domain</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PIHOLE_DNS_</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;192.168.50.1;1.1.1.1&#34;</span><span class="w">    </span><span class="c"># internal + public resolver</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/docker/pihole/etc-pihole:/etc/pihole</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/docker/pihole/etc-dnsmasq.d:/etc/dnsmasq.d</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">pihole_macvlan_network</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">ipv4_address</span><span class="p">:</span><span class="w"> </span><span class="m">192.168.50.5</span><span class="w">           </span><span class="c"># example Pi-hole IP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pihole_macvlan_network</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></div>
<p>🔒 <strong>Note:</strong></p>
<ul>
<li>Replace the example IPs/domains with your own.</li>
<li><code>.env</code> file (holding <code>WEBPASSWORD</code>) should <strong>never</strong> be committed to git.</li>
</ul>

  </div>
</details>

<p><span class="tag teal">Nginx Proxy Manager (NPM)</span> – Handles all my proxy rules and SSL.<br>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <ul>
<li>Front door for all services.</li>
<li>Simple UI to add/edit rules.</li>
</ul>
<p>Docker compose / Portainer stack definition:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">npm</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">jc21/nginx-proxy-manager:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">nginx-proxy-manager</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s1">&#39;80:80&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s1">&#39;81:81&#39;</span><span class="w">   </span><span class="c"># UI (for managing proxy rules)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s1">&#39;443:443&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/docker/npm/data:/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/docker/npm/letsencrypt:/etc/letsencrypt</span></span></span></code></pre></div>
  </div>
</details>
</p>
<p><span class="tag teal">Portainer</span> – Web GUI to manage every container.<br>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <ul>
<li>My main “dashboard” for starting/stopping/restarting services.</li>
<li>Compose block shows how it maps <code>/var/run/docker.sock</code>.</li>
</ul>
<p>Docker Compose:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;3.8&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">portainer</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">portainer/portainer-ce:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">portainer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;8000:8000&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;9000:9000&#34;</span><span class="w">    </span><span class="c"># Web UI (HTTP)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;9443:9443&#34;</span><span class="w">    </span><span class="c"># Web UI (HTTPS)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/run/docker.sock:/var/run/docker.sock</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/docker/portainer:/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">npm_default  </span><span class="w"> </span><span class="c"># So NPM can proxy Portainer too</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">npm_default</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></div>
<p>💡 <strong>Note:</strong> Portainer needs access to <code>/var/run/docker.sock</code> to manage Docker (that’s normal — but it’s essentially root‑level access).</p>

  </div>
</details>
</p>
<p><span class="tag teal">Pi.Alert</span> – Network presence monitor.<br>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <ul>
<li>Watches the network for new/unknown devices.</li>
<li>Handy for spotting intruders… or just remembering when you bought that smart plug.</li>
<li>Compose notes: needs correct timezone env var, optional telegram alerts.</li>
</ul>
<p><strong>Docker Compose:</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;3.3&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pialert</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">jokobsk/pi.alert:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">pialert</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">network_mode</span><span class="p">:</span><span class="w"> </span><span class="l">host         </span><span class="w"> </span><span class="c"># Needed for full network visibility</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/docker/pialert/config:/home/pi/pialert/config</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/docker/pialert/db:/home/pi/pialert/db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">cap_add</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">NET_ADMIN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">NET_RAW</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">TZ=America/Chicago</span></span></span></code></pre></div>
<p>🔒 <strong>Notes:</strong></p>
<ul>
<li><code>network_mode: host</code> is required so Pi.Alert can see the whole LAN (but means it’s tightly bound to the host network — don’t expose this container externally).</li>
<li>Runs with <code>NET_ADMIN</code> and <code>NET_RAW</code> capabilities to sniff devices — normal for this app, but be mindful of what else you mount in here.</li>
</ul>

  </div>
</details>
</p>
<p><span class="tag teal">SyncThing</span>
SyncThing – Syncs files between all my machines.<br>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <ul>
<li>Keeps critical folders mirrored between GIR and my laptops/desktops.</li>
<li>Handles my dotfiles repo, configs, and even some media — all in real time.</li>
</ul>
<p>Docker Compose:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;3.8&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">syncthing</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">syncthing/syncthing:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">syncthing</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">hostname</span><span class="p">:</span><span class="w"> </span><span class="l">gir</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">npm_default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/docker/syncthing/config:/var/syncthing/config</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/docker/syncthing/data:/var/syncthing/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;8384:8384&#34;</span><span class="w">    </span><span class="c"># Web UI</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;22000:22000/tcp&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;22000:22000/udp&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;21027:21027/udp&#34;</span><span class="w">  </span><span class="c"># Local discovery</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">npm_default</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></div>
<p>🔒 <strong>Notes:</strong></p>
<ul>
<li><code>8384</code> is the Syncthing Web UI — fine for LAN, but best proxied through Nginx Proxy Manager if you want remote access with HTTPS/auth.</li>
<li>Uses <code>npm_default</code> so it plays nicely with your proxy rules.</li>
<li>No secrets in this snippet (Syncthing keys live inside <code>/var/docker/syncthing/config</code>).</li>
</ul>

  </div>
</details>
</p>
<p><span class="tag teal">Statix</span> – A super-lightweight web server for static files.<br>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <ul>
<li>Runs a barebones nginx instance for serving test pages, quick downloads, or temporary files.</li>
<li>Perfect for quick “throw it in <code>/var/www/html</code> and share” moments.</li>
</ul>
<p>Docker Compose:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;3.8&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">statix</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">nginx:alpine</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">statix</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;8081:80&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/www/html:/usr/share/nginx/html:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/docker/statix/nginx.conf:/etc/nginx/conf.d/default.conf:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">npm_default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">npm_default</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></div>
<p>🔒 <strong>Notes:</strong></p>
<ul>
<li><code>:ro</code> (read‑only) on the mounts means nginx can’t overwrite your files or configs — safer and cleaner.</li>
<li>Bound to <code>npm_default</code> so it can be proxied via Nginx Proxy Manager if needed.</li>
</ul>

  </div>
</details>
</p>
<hr>
<p align="center">
  <img src="gir-salute.png" alt="gir saluting" width="258">
  <i>© Nickelodeon / Jhonen Vasquez</i>
</p>
<hr>
<h3 id="-dashboards--home-pages">🏠 Dashboards &amp; Home Pages</h3>
<p><span class="tag teal">Dashy</span> – My main homelab dashboard and jumping-off point.<br>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <ul>
<li>Shows links to all of GIR’s services, widgets, and quick status checks.</li>
<li>Lives at the center of the setup — it’s the page I open first every day.</li>
</ul>
<p>Docker Compose:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;3.8&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">dashy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">dashy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">lissy93/dashy:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;8090:8080&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/docker/dashy/config.yml:/app/user-data/conf.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./my-nginx.conf:/etc/nginx/conf.d/default.conf:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">npm_default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">npm_default</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></div>
<p>🔒 <strong>Notes:</strong></p>
<ul>
<li><code>config.yml</code> holds all the dashboard items and widgets — you can track that file in Git for easy rollbacks.</li>
<li>Mounting a custom nginx config (<code>my-nginx.conf</code>) gives you full control over how Dashy serves content.</li>
<li>Connected to <code>npm_default</code> so Nginx Proxy Manager can front it with HTTPS/auth.</li>
</ul>

  </div>
</details>
</p>
<p><span class="tag teal">Heimdall</span> – A simple, lightweight launcher for quick access.<br>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <ul>
<li>Works as a secondary dashboard for bookmarks and quick shortcuts.</li>
<li>Nice for testing layouts or keeping “less-used” services out of Dashy.</li>
</ul>
<p>Docker Compose:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">heimdall</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">heimdall</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">lscr.io/linuxserver/heimdall:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PUID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PGID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">TZ=America/Chicago</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/docker/heimdall:/config</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;8082:80&#34;</span></span></span></code></pre></div>
<p>🔒 <strong>Notes:</strong></p>
<ul>
<li><code>PUID</code>/<code>PGID</code> (1000) match your main user, so Heimdall writes config files with the right ownership.</li>
<li>Bound to port <code>8082</code> — easy to proxy through Nginx Proxy Manager later.</li>
</ul>

  </div>
</details>
</p>
<hr>
<h3 id="-monitoring--metrics">🎛 Monitoring &amp; Metrics</h3>
<p><span class="tag teal">Netdata</span> – Real-time performance monitoring for GIR.<br>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <ul>
<li>Gorgeous, interactive graphs for CPU, RAM, disk, containers — basically, the “vitals monitor” for GIR.</li>
<li>Lightweight but powerful, with resource limits set to avoid hogging the Pi.</li>
</ul>
<p>Docker Compose:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;3.8&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">netdata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">netdata/netdata:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">netdata</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;19999:19999&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">cap_add</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">SYS_PTRACE</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">security_opt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">apparmor:unconfined</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">netdata_config:/etc/netdata</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/proc:/host/proc:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/sys:/host/sys:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/etc/os-release:/host/etc/os-release:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/run/docker.sock:/var/run/docker.sock:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">npm_default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">DO_NOT_TRACK=1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">mem_limit</span><span class="p">:</span><span class="w"> </span><span class="l">256m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">cpus</span><span class="p">:</span><span class="w"> </span><span class="m">0.4</span><span class="w">   </span><span class="c"># limit to 40% of one core</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">netdata_config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">npm_default</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></div>
<p>🔒 <strong>Notes:</strong></p>
<ul>
<li>Runs in read-only mode for <code>/proc</code> and <code>/sys</code> — Netdata can see stats without modifying the host.</li>
<li>Docker socket is mounted read-only: safe for metrics, not for control.</li>
<li><code>mem_limit</code> and <code>cpus</code> keep it lightweight on the Pi, so GIR doesn’t feel sluggish.</li>
</ul>

  </div>
</details>
</p>
<p><span class="tag teal">Uptime Kuma</span> – Simple, slick uptime monitoring.<br>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <ul>
<li>Pings every important service (and a few external sites) so I know when something’s down.</li>
<li>Creates pretty status pages, and yes — sends alerts if something goes sideways.</li>
</ul>
<p>Docker Compose:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;3.8&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">uptime-kuma</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">uptime-kuma</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">louislam/uptime-kuma:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;3001:3001&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/docker/uptime-kuma:/app/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">TZ=America/Chicago</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">npm_network</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">npm_network</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">driver</span><span class="p">:</span><span class="w"> </span><span class="l">bridge</span></span></span></code></pre></div>
<p>🔒 <strong>Notes:</strong></p>
<ul>
<li>Stores all monitors and history in <code>/var/docker/uptime-kuma</code> — easy to back up.</li>
<li>Runs on <code>3001</code> by default; you can proxy it through Nginx Proxy Manager for HTTPS and remote access.</li>
<li>Uses its own <code>npm_network</code>, but can be attached to your main <code>npm_default</code> if you want one shared network.</li>
</ul>

  </div>
</details>
</p>
<p><span class="tag teal">Speedtest Tracker</span> – Logs internet speed over time.<br>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <ul>
<li>Runs daily speed tests and keeps a history, so I know when my ISP is slacking.</li>
<li>Uses a simple SQLite database by default — no separate DB container needed.</li>
</ul>
<p>Docker Compose:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;3.3&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">speedtest-tracker</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">speedtest-tracker</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/alexjustesen/speedtest-tracker:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;8765:80&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PUID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PGID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">TZ=America/Chicago</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">DB_CONNECTION=sqlite</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">DB_DATABASE=/config/database/database.sqlite </span><span class="w"> </span><span class="c"># key setting for SQLite mode</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/docker/speedtest-tracker/config:/config</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/docker/speedtest-tracker/web:/etc/ssl/web</span></span></span></code></pre></div>
<p>🔒 <strong>Notes:</strong></p>
<ul>
<li>Uses SQLite for simplicity — database lives inside <code>/config</code>, easy to back up.</li>
<li>Mounted web certs directory (<code>/etc/ssl/web</code>) can be used if you want to add HTTPS later.</li>
<li>Runs on <code>8765</code> by default — proxy through Nginx Proxy Manager for remote access or SSL.</li>
</ul>

  </div>
</details>
</p>
<hr>
<p align="center">
<img src="gir-remote.jpg" width="500">
 <i>© Nickelodeon / Jhonen Vasquez</i>
</p>
<h3 id="-developer-tools">🧰 Developer Tools</h3>
<p><span class="tag teal">Gitea</span> – My self-hosted Git server and code hub.<br>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <ul>
<li>Runs a lightweight Git server for all my configs, scripts, and projects.</li>
<li>Keeps my Docker compose files, dotfiles, and even documentation version-controlled right on GIR.</li>
</ul>
<p>Docker Compose:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;3.8&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">gitea</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">gitea/gitea:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">gitea</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">USER_UID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">USER_GID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">GITEA__server__DOMAIN=git.example.lan</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">GITEA__server__ROOT_URL=https://git.example.lan/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">GITEA__server__SSH_DOMAIN=git.example.lan</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">GITEA__server__SSH_PORT=222</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/docker/gitea:/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;3003:3000&#34;</span><span class="w">   </span><span class="c"># Web interface</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;222:22&#34;</span><span class="w">      </span><span class="c"># SSH for git</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">npm_default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">npm_default</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></div>
<p>🔒 <strong>Notes:</strong></p>
<ul>
<li>All Git data (repos, configs, attachments) lives in <code>/var/docker/gitea</code> — easy to back up.</li>
<li>Ports: <code>3003</code> for web UI, <code>222</code> for Git over SSH.</li>
<li>Connected to <code>npm_default</code> so Gitea sits neatly behind Nginx Proxy Manager for HTTPS.</li>
<li>Placeholder domain (<code>git.example.lan</code>) used here — swap for your own LAN/SSL domain.</li>
</ul>

  </div>
</details>
</p>
<p><span class="tag teal">Open-WebUI</span> – A web front end for local LLM experiments.<br>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <ul>
<li>Runs Qwen, Mistral, and other models on demand — my “AI playground” on GIR.</li>
<li>Great for testing prompts and tinkering with models without a cloud dependency.</li>
</ul>
<p>Docker Compose:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;3.8&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">open-webui</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/open-webui/open-webui:main</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">open-webui</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;3000:8080&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">open-webui-data:/app/backend/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">extra_hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;host.docker.internal:host-gateway&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="l">open-webui-data:</span></span></span></code></pre></div>
<p>🔒 <strong>Notes:</strong></p>
<ul>
<li>Data (uploads, configs, models) stored in <code>open-webui-data</code> named volume — easy to back up.</li>
<li>Exposes port <code>3000</code> — proxy through Nginx Proxy Manager for HTTPS and remote access.</li>
<li>Uses <code>host.docker.internal</code> to connect to the host environment (helpful if models or tools live outside this container).</li>
</ul>

  </div>
</details>
</p>
<p><span class="tag teal">Jupyter</span> – Interactive notebooks for Python tinkering and data work.<br>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <ul>
<li>Great for experimenting with Python, data viz, and quick scripts — no token hassle, just a password prompt.</li>
<li>Runs fully local on GIR, so security is handled with a simple password and LAN isolation.</li>
</ul>
<p>Docker Compose:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;3.8&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">jupyter</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">jupyter/scipy-notebook:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">jupyter</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;8888:8888&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/docker/jupyter/notebooks:/home/jovyan/work</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="l">start-notebook.sh --NotebookApp.token=&#39;&#39; --NotebookApp.password=&#39;sha1:PUT-YOUR-HASH-HERE&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span></span></span></code></pre></div>
<p>🔑 <strong>Set a password hash:</strong><br>
Run this <strong>once</strong> on GIR (or any machine with Python):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run --rm jupyter/scipy-notebook:latest <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  python -c <span class="s2">&#34;from notebook.auth import passwd; print(passwd())&#34;</span>
</span></span></code></pre></div><p>Copy the hash (looks like sha1:123abc…) and paste it into
&ndash;NotebookApp.password=&lsquo;sha1:YOUR-HASH&rsquo; in the compose file.</p>
<p>🔒 Notes:</p>
<p>NotebookApp.token=&rsquo;&rsquo; disables the annoying one-time token login.</p>
<p>Still password protected, so anyone on your LAN will need that password to log in.</p>
<p>Stores notebooks in /var/docker/jupyter/notebooks for persistence and easy backups.</p>

  </div>
</details>
</p>
<hr>
<h3 id="-media--fun-occasional-use">📺 Media &amp; Fun (Occasional Use)</h3>
<p><span class="tag teal">Jellyfin</span> – A self-hosted media server for movies, shows, and music.<br>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <ul>
<li>Think “DIY Netflix,” but with total control and no subscriptions.</li>
<li>GIR runs it for occasional streaming and as a way to organize my local media.</li>
</ul>
<p>Docker Compose:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;3.8&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">jellyfin</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">jellyfin/jellyfin:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">jellyfin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;8096:8096&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/docker/jellyfin/config:/config</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/docker/jellyfin/cache:/cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/mnt/sldf/VIDEO:/media</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">TZ=America/Chicago</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PUID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PGID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">npm_default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">npm_default</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></div>
<p>🔒 <strong>Notes:</strong></p>
<ul>
<li>All configs live in <code>/var/docker/jellyfin/config</code>, and cache goes to <code>/var/docker/jellyfin/cache</code> for smoother playback.</li>
<li>Media library is mapped from <code>/mnt/sldf/VIDEO</code> — swap in your own mount point if you use this snippet.</li>
<li>Runs on port <code>8096</code>; proxy it through Nginx Proxy Manager if you want HTTPS or external access.</li>
</ul>

  </div>
</details>
</p>
<p><span class="tag teal">PhotoPrism</span> – A private photo library and indexing tool.<br>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <ul>
<li>Automatically indexes and organizes images, including AI‑powered search and tagging.</li>
<li>Mostly experimental on GIR, but a fun “what if” for turning old drives into an archive.</li>
</ul>
<p>Docker Compose:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;3.7&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">photoprism</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">photoprism/photoprism:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">photoprism</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;2342:2342&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PHOTOPRISM_ADMIN_USER</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;admin&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PHOTOPRISM_ADMIN_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;changeme&#34;</span><span class="w">   </span><span class="c"># ❗ replace in .env for real setup</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PHOTOPRISM_ORIGINALS_LIMIT</span><span class="p">:</span><span class="w"> </span><span class="m">5000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PHOTOPRISM_HTTP_COMPRESSION</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;gzip&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PHOTOPRISM_LOG_LEVEL</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;info&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PHOTOPRISM_DISABLE_TLS</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PHOTOPRISM_SITE_TITLE</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Photo Archive&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PHOTOPRISM_UPLOAD_NSFW</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/docker/photoprism/storage:/photoprism/storage</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/mnt/sldf/ImageLibrary:/photoprism/originals</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">mariadb</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">mariadb</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">mariadb:10.11</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">photoprism-db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MYSQL_ROOT_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;rootpass&#34;</span><span class="w">    </span><span class="c"># ❗ move to .env</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MYSQL_DATABASE</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;photoprism&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MYSQL_USER</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;photoprism&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MYSQL_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;secret&#34;</span><span class="w">           </span><span class="c"># ❗ move to .env</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/docker/photoprism/db:/var/lib/mysql</span></span></span></code></pre></div>
<p>🔒 <strong>Notes:</strong></p>
<ul>
<li>The sample shows placeholder passwords — store real ones in a <code>.env</code> file and reference them instead.</li>
<li>PhotoPrism disables TLS here (<code>PHOTOPRISM_DISABLE_TLS=true</code>) because HTTPS is handled by Nginx Proxy Manager.</li>
<li>Media library (<code>/mnt/sldf/ImageLibrary</code>) and PhotoPrism storage directory are separate for sanity and backups.</li>
</ul>

  </div>
</details>
</p>
<p><span class="tag teal">Qbittorrent &#43; GlueTun</span> – Private torrenting via VPN.<br>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <ul>
<li><strong>GlueTun</strong> handles the VPN tunnel (OpenVPN in this setup), and</li>
<li><strong>Qbittorrent</strong> rides through it — no leaks, no “oops” moments.</li>
</ul>
<p>Docker Compose:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;3.3&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">gluetun</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">qmcgaw/gluetun</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">qbittorrent-vpn</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">cap_add</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">NET_ADMIN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">devices</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/dev/net/tun</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/docker/bittorrent/config/openvpn:/gluetun</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">VPN_SERVICE_PROVIDER=custom</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">VPN_TYPE=openvpn</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">DOT=off</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">DNS=192.168.1.1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">OPENVPN_CUSTOM_CONFIG=/gluetun/airvpn.ovpn</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">TZ=America/Chicago</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">FIREWALL_VPN_INPUT_PORTS=22112</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="m">8080</span><span class="p">:</span><span class="m">8080</span><span class="w">         </span><span class="c"># qBittorrent Web UI</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="m">6881</span><span class="p">:</span><span class="m">6881</span><span class="w">         </span><span class="c"># BitTorrent TCP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="m">6881</span><span class="p">:</span><span class="m">6881</span><span class="l">/udp    </span><span class="w"> </span><span class="c"># BitTorrent UDP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="m">22112</span><span class="p">:</span><span class="m">22112</span><span class="w">       </span><span class="c"># AirVPN port (TCP)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="m">22112</span><span class="p">:</span><span class="m">22112</span><span class="l">/udp  </span><span class="w"> </span><span class="c"># Optional UDP trackers</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">qbittorrent</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">linuxserver/qbittorrent</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">qbittorrent</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">gluetun</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">network_mode</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;service:gluetun&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PUID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PGID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">WEBUI_PORT=8080</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/docker/bittorrent/config:/config</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/mnt/sldf/downloads:/downloads</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/mnt/sldf/downloads/watch:/watch </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">mem_limit</span><span class="p">:</span><span class="w"> </span><span class="l">512m     </span><span class="w"> </span><span class="c"># RAM cap</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">cpus</span><span class="p">:</span><span class="w"> </span><span class="m">0.75</span><span class="w">           </span><span class="c"># CPU limit (75% of one core)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span></span></span></code></pre></div>
<p>🔒 <strong>Notes:</strong></p>
<ul>
<li><strong>VPN creds:</strong> Put your <code>.ovpn</code> file and any login credentials in <code>/var/docker/bittorrent/config/openvpn</code> — don’t hardcode them in the compose file.</li>
<li><strong>No leaks:</strong> <code>network_mode: service:gluetun</code> forces all qBittorrent traffic through GlueTun — if VPN drops, qBittorrent is cut off.</li>
<li><strong>Ports:</strong> <code>8080</code> is the Web UI (LAN only), <code>6881</code> handles BitTorrent, and <code>22112</code> is your forwarded VPN port.</li>
<li><strong>Resource limits:</strong> CPU and RAM caps keep torrenting from hogging the Pi.</li>
</ul>

  </div>
</details>
</p>
<p align="center">
  <img src="gir-agape.png" alt="gir with mouth agape" width="258">
  <i>© Nickelodeon / Jhonen Vasquez</i>
</p>
<hr>
<h2 id="zram-for-efficient-swap"><code>zram</code> for efficient swap</h2>
<p>One tweak that has paid dividends has been to create a zram swap with a lower priority swap file behind it. This actually compresses the swapfile in ram, sometimes even doubling the amount of data that can be stored in memory. It falls back to the ssd only when RAM and the zram swap are completely full. Pretty easy to set up:</p>
<p><strong>🌀 ZRAM + Fallback Swapfile (Quick Setup)</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 1️⃣ Install and enable ZRAM (2 GB compressed swap)</span>
</span></span><span class="line"><span class="cl">sudo apt install zram-tools
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> -e <span class="s2">&#34;SIZE=2048\nALGO=zstd\nPRIORITY=100&#34;</span> <span class="p">|</span> sudo tee /etc/default/zramswap
</span></span><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> --now zramswap
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2️⃣ Add a 16 GB fallback swapfile on NVMe</span>
</span></span><span class="line"><span class="cl">sudo fallocate -l 16G /swapfile
</span></span><span class="line"><span class="cl">sudo chmod <span class="m">600</span> /swapfile
</span></span><span class="line"><span class="cl">sudo mkswap /swapfile
</span></span><span class="line"><span class="cl">sudo swapon --priority <span class="m">10</span> /swapfile
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;/swapfile none swap sw,pri=10 0 0&#39;</span> <span class="p">|</span> sudo tee -a /etc/fstab
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 3️⃣ Verify</span>
</span></span><span class="line"><span class="cl">swapon --show
</span></span></code></pre></div><p>That&rsquo;s it! Efficient swap file management on a memory constrained device.</p>
<h2 id="headless-but-with-a-virtual-head-anyway">Headless but with a Virtual Head Anyway</h2>
<p>This was an exercise in masochism so I&rsquo;m documenting my working setup here. I hope it helps someone else avoid the confusion and frustration I encountered.</p>
<p>These are the <strong>exact steps used to set up GIR&rsquo;s &ldquo;virtual head&rdquo;</strong> — a full XFCE desktop available over VNC.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <h2 id="quick-start-lan-vnc-with-xfce">Quick Start (LAN VNC with XFCE)</h2>
<p>1️⃣ <strong>Install packages</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt update
</span></span><span class="line"><span class="cl">sudo apt install tigervnc-standalone-server xfce4 xfce4-goodies
</span></span></code></pre></div><p>2️⃣ <strong>Create <code>~/.vnc/xstartup</code></strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p ~/.vnc
</span></span><span class="line"><span class="cl">nano ~/.vnc/xstartup
</span></span></code></pre></div><p>Contents:</p>
<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/sh
</span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="nb">unset</span> SESSION_MANAGER
</span></span><span class="line"><span class="cl"><span class="nb">unset</span> DBUS_SESSION_BUS_ADDRESS
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">XKL_XMODMAP_DISABLE</span><span class="o">=</span><span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">DISPLAY</span><span class="o">=</span>:1
</span></span><span class="line"><span class="cl">xrdb <span class="nv">$HOME</span>/.Xresources
</span></span><span class="line"><span class="cl"><span class="nb">exec</span> startxfce4
</span></span></code></pre></div><p>Make executable:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">chmod +x ~/.vnc/xstartup
</span></span></code></pre></div><p>3️⃣ <strong>Set a VNC password</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">vncpasswd
</span></span></code></pre></div><p>4️⃣ <strong>Create &amp; enable systemd service</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo nano /etc/systemd/system/vncserver@.service
</span></span></code></pre></div><p>Paste in:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">Start TigerVNC server at startup</span>
</span></span><span class="line"><span class="cl"><span class="na">After</span><span class="o">=</span><span class="s">syslog.target network.target</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">forking</span>
</span></span><span class="line"><span class="cl"><span class="na">User</span><span class="o">=</span><span class="s">grumble</span>
</span></span><span class="line"><span class="cl"><span class="na">Group</span><span class="o">=</span><span class="s">grumble</span>
</span></span><span class="line"><span class="cl"><span class="na">WorkingDirectory</span><span class="o">=</span><span class="s">/home/grumble</span>
</span></span><span class="line"><span class="cl"><span class="na">PIDFile</span><span class="o">=</span><span class="s">/home/grumble/.vnc/%H:%i.pid</span>
</span></span><span class="line"><span class="cl"><span class="na">ExecStartPre</span><span class="o">=</span><span class="s">-/usr/bin/tigervncserver -kill :%i &gt; /dev/null 2&gt;&amp;1</span>
</span></span><span class="line"><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/usr/bin/tigervncserver :%i -localhost no -geometry 1280x800 -depth 24</span>
</span></span><span class="line"><span class="cl"><span class="na">ExecStop</span><span class="o">=</span><span class="s">/usr/bin/tigervncserver -kill :%i</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Install]</span>
</span></span><span class="line"><span class="cl"><span class="na">WantedBy</span><span class="o">=</span><span class="s">multi-user.target</span>
</span></span></code></pre></div><p>Enable and start:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> vncserver@1.service
</span></span><span class="line"><span class="cl">sudo systemctl start vncserver@1.service
</span></span></code></pre></div><p>5️⃣ <strong>Connect from a VNC client</strong> to <code>raspberrypi:1</code> (or the Pi’s LAN IP with <code>:1</code>).</p>
<hr>
<h3 id="-notes--tips">🔒 Notes &amp; Tips</h3>
<ul>
<li><code>-localhost no</code> means anyone on your LAN can connect. For SSH tunnel–only access, change it to <code>-localhost yes</code>.</li>
<li>Logs &amp; troubleshooting: check <code>~/.vnc/*.log</code> if the service doesn’t start.</li>
<li>Passwords are set with <code>vncpasswd</code> and stored in <code>~/.vnc/passwd</code>.</li>
</ul>
<hr>
<p>✅ <strong>Result:</strong> A persistent XFCE desktop on your Pi, ready whenever you connect.</p>

  </div>
</details>

<hr>
<h2 id="ssh--dotfiles-integration">SSH &amp; Dotfiles Integration</h2>
<p>I won’t go into all the gory details, but one of the biggest reasons GIR “just works” is the way SSH and Git are woven into everything.</p>
<p>All my machines — laptops, desktops, servers — share one <strong>dotfiles repo</strong>. That repo controls my shell prompt, aliases, functions, and toolchain. No matter which machine I’m on, I feel like I’m “at home.”</p>
<p>The other half of the puzzle is SSH. I keep a <strong>single, clean <code>~/.ssh/config</code></strong> (also tracked in Git) that knows how to reach every machine, and every machine has the right <strong>public keys installed</strong> so I can hop around without typing passwords.</p>
<p>Here’s an example (sanitized) of how I keep things neat:</p>
<pre tabindex="0"><code class="language-sshconfig" data-lang="sshconfig"># dotfiles/ssh/EXAMPLE.config
# Example SSH config — customize as needed and then rename from EXAMPLE.config to config

Host myserver
    HostName example.com
    User myuser
    IdentityFile ~/.ssh/id_ed25519

Host github.com
    HostName git.example.com
    User git
    IdentityFile ~/.ssh/id_github_ed25519
    IdentitiesOnly yes</code></pre>
<hr>
<h2 id="how-i-use-gir">How I Use GIR</h2>
<p>So hopefully this post will not be as tedious to read as it was to write. I did put a lot of work into crafting this system, but how do I actually use it?</p>
<p>I keep some of the Docker services powered off normally and only activate them as needed. Others are always up and heavily used (like Gitea). Having my infrastructure services hosted in one place is a big win. The key to keeping it sane lies in how I use pihole and npm. For each service, I create an A record for servicename.darkstar.home that points to GIR&rsquo;s IP address. I then create an Nginx Proxy Manager host rule that maps the friendly domain name to the service (whether on Docker or standalone). This keeps my namespace memorable and easy to link and avoids having to remember which port numbers of paths I used.</p>
<p>On each of my machines I have a folder of bookmarks to my services. With Dashy however, I have built a homepage that links to everything I have so just that link is enough to browse and reach the others.</p>
<p>It&rsquo;s clean, friendly and works very well.</p>
<p>The final piece of the puzzle are scheduled backups.</p>
<h2 id="backups-all-the-way-down">Backups all the way down</h2>
<p>GIR might be a little chaos gremlin, but I keep it on a short leash. Every night a <strong>systemd timer</strong> quietly kicks off a backup script that grabs all my Docker volumes, compresses them, and sweeps away the cruft.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <p>Here’s the core script (lives at <code>/usr/local/bin/docker-backup.sh</code>):</p>
<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">BACKUP_DIR</span><span class="o">=</span><span class="s2">&#34;/mnt/sldf/backup&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">TIMESTAMP</span><span class="o">=</span><span class="k">$(</span>date +<span class="s2">&#34;%Y-%m-%d_%H-%M-%S&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">BACKUP_FILE</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$BACKUP_DIR</span><span class="s2">/docker_backup_</span><span class="nv">$TIMESTAMP</span><span class="s2">.tar.gz&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">SOURCE_DIR</span><span class="o">=</span><span class="s2">&#34;/var/docker&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create the backup (exclude the destination path)</span>
</span></span><span class="line"><span class="cl">tar -czf <span class="s2">&#34;</span><span class="nv">$BACKUP_FILE</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    --exclude<span class="o">=</span><span class="s2">&#34;</span><span class="nv">$BACKUP_FILE</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    --exclude<span class="o">=</span><span class="s2">&#34;</span><span class="nv">$BACKUP_DIR</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    <span class="s2">&#34;</span><span class="nv">$SOURCE_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"># Keep only the 3 most recent backups, delete older ones</span>
</span></span><span class="line"><span class="cl">ls -t <span class="s2">&#34;</span><span class="nv">$BACKUP_DIR</span><span class="s2">&#34;</span>/docker_backup_*.tar.gz <span class="p">|</span> tail -n +4 <span class="p">|</span> xargs -r rm -f
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Output completion message and notify Uptime Kuma</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Backup completed: </span><span class="nv">$BACKUP_FILE</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">curl -fsS --retry <span class="m">3</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  <span class="s2">&#34;http://status.darkstar.home/api/push/hm3iRuOyHT?status=up&amp;msg=OK&#34;</span></span></span></code></pre></div>
<p>A <strong>systemd service</strong> runs the script:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/systemd/system/docker-backup.service</span>
</span></span><span class="line"><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">Nightly Docker Backup</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">oneshot</span>
</span></span><span class="line"><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/usr/local/bin/docker-backup.sh</span></span></span></code></pre></div>
<p>And the <strong>systemd timer</strong> makes sure it happens every night:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/systemd/system/docker-backup.timer</span>
</span></span><span class="line"><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">Run Docker Backup Every Night</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Timer]</span>
</span></span><span class="line"><span class="cl"><span class="na">OnCalendar</span><span class="o">=</span><span class="s">*-*-* 03:00:00</span>
</span></span><span class="line"><span class="cl"><span class="na">Persistent</span><span class="o">=</span><span class="s">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Install]</span>
</span></span><span class="line"><span class="cl"><span class="na">WantedBy</span><span class="o">=</span><span class="s">timers.target</span></span></span></code></pre></div>
<p>Enable it once, and it just hums away:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> --now docker-backup.timer</span></span></code></pre></div>
<p><strong>Result:</strong> nightly backups, no drama, and only the <strong>three most recent snapshots</strong> kept — clean and lean, just the way I like it.</p>

  </div>
</details>

<hr>
<h2 id="-conclusion">✅ Conclusion</h2>
<p>GIR started life as a recovery project after my old server&rsquo;s meltdown, but it&rsquo;s turned into something better. The infrastructure it manages has been very reliable and despite its complexity, I know I have a robust server that turns my collection of machines and VMs into a cohesive rig.
While I am careful not to break what works, I am not afraid to experiment and tweak it as needed.</p>
<p>That&rsquo;s it. I definitely battled a lot of little problems so I hope this will help others avoid the same frustrations.</p>
<p>📬 Got a server with a weird name, a backup ritual, or a homelab hack you love?<br>
Tell me about it: <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a>.</p>
<p align="center">
  <img src="gir-crazy.png" alt="gir crazy laugh" width="371">
    <i>© Nickelodeon / Jhonen Vasquez</i>
</p>
]]></content:encoded>
    </item>
    <item>
      <title>Logaround</title>
      <link>https://adminjitsu.com/posts/logaround/</link>
      <pubDate>Thu, 31 Jul 2025 02:41:20 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/logaround/</guid>
      <description>Logaround 2.0 is a powerful Python CLI tool for searching and analyzing systemd logs with human-friendly time filters, context windows, color highlighting, and more—right from your terminal.</description>
      <content:encoded><![CDATA[<h2 id="what-is-logaround">What is <code>logaround</code>?</h2>
<p>Log files: love them, hate them, definitely need them—especially when the pager beeps at 3AM. I used a custom shell function to dig up context around weird log entries with <code>journalctl</code>. It worked, but had limits.</p>
<p>So I rewrote logaround in Python.</p>
<p>The result:</p>
<ul>
<li>Color highlights</li>
<li>Human-friendly time filters</li>
<li>True context windows (before/after)</li>
<li>Never loses lines, even with messy logs</li>
<li>Pandas-powered for hacking and export</li>
</ul>
<p><strong>TL;DR:</strong><br>
You can finally say:</p>
<blockquote>
<p>“Show me everything that happened around this error at 9:14pm yesterday—and don’t make me lose the plot!”</p></blockquote>
<hr>
<h2 id="quick-start">Quick Start</h2>
<p class="github-btn"> 
<a href="https://github.com/forfaxx/logaround" target="_blank"> 🔗 View logaround on GitHub </a> 
</p>
<p>Clone from GitHub:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone https://github.com/forfaxx/logaround.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> logaround
</span></span><span class="line"><span class="cl">chmod +x logaround.py
</span></span></code></pre></div><p>Create a virtual environment and activate it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">python3 -m venv venv
</span></span><span class="line"><span class="cl"><span class="nb">source</span> venv/bin/activate
</span></span></code></pre></div><p>Install dependencies</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install -r requirements.txt
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">or 
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">pip install pandas rich
</span></span></code></pre></div><p>You should now be ready to go. For a really handy way to automate activating and deactivating the venv, see the following:
<a href="/posts/wordclouder/#optional-and-advanced">Optional and advanced</a></p>
<hr>
<h2 id="features">Features</h2>
<ul>
<li><strong>Search by time or text:</strong> Jump to events or messages anywhere in your logs.</li>
<li><strong>Delta/context:</strong> See lines before and after each match—like grep -A/-B, but smarter.</li>
<li><strong>Highlighting:</strong> Instantly see what matched, even in noisy logs.</li>
<li><strong>Human time:</strong> Use <code>&quot;last Friday 11pm&quot;</code>, <code>&quot;2 hours ago&quot;</code>, <code>&quot;yesterday 14:00&quot;</code>—anything GNU <code>date</code> understands.</li>
<li><strong>No dropped lines:</strong> Even logs with weird or unmatched formats are shown.</li>
<li><strong>Hackable:</strong> Everything loads into a pandas DataFrame for analysis or export.</li>
</ul>
<hr>
<h2 id="usage">Usage</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">./logaround.py --term network --delta <span class="m">5</span> --since <span class="s2">&#34;yesterday 12:00&#34;</span>
</span></span><span class="line"><span class="cl">./logaround.py --term apt --since <span class="s2">&#34;last week&#34;</span> --max <span class="m">30</span>
</span></span><span class="line"><span class="cl">./logaround.py --since <span class="s2">&#34;2024-07-28 16:00&#34;</span> --until <span class="s2">&#34;2024-07-28 18:00&#34;</span> --term failed --delta <span class="m">8</span>
</span></span><span class="line"><span class="cl">./logaround.py --help
</span></span></code></pre></div><p><strong>Main options:</strong></p>
<ul>
<li><code>--term STR</code>         Search for this term (repeat for AND-search)</li>
<li><code>--delta N</code>          Show ±N context lines around matches</li>
<li><code>--since TIME</code>       Start time (any GNU <code>date</code>-friendly string)</li>
<li><code>--until TIME</code>       End time (same as above)</li>
<li><code>--lines N</code>          How many log lines to fetch if not filtering by time (default: 500)</li>
<li><code>--max N</code>            Max number of results to show (default: 100)</li>
<li><code>-h</code>, <code>--help</code>       Show help message and exit</li>
<li><code>-v</code>, <code>--version</code>    Show version and exit</li>
</ul>
<hr>
<h2 id="screenshot">Screenshot</h2>
<p><img alt="logaround search demo" loading="lazy" src="/posts/logaround/logaround.jpg"></p>
<hr>
<h2 id="supported-timestamps">Supported Timestamps</h2>
<p>You can use <strong>any date/time string GNU <code>date</code> will parse</strong>:</p>
<ul>
<li><code>&quot;2025-07-30 13:41:12&quot;</code></li>
<li><code>&quot;yesterday 22:00&quot;</code></li>
<li><code>&quot;last Friday 17:30&quot;</code></li>
<li><code>&quot;now&quot;</code>, <code>&quot;midnight&quot;</code>, <code>&quot;tomorrow&quot;</code></li>
<li><code>&quot;1 week ago&quot;</code>, <code>&quot;next Tuesday&quot;</code></li>
</ul>
<p>See <a href="https://man7.org/linux/man-pages/man1/date.1.html"><code>man date</code></a> or the<br>
<a href="https://www.gnu.org/software/coreutils/manual/html_node/date-invocation.html">GNU date documentation</a><br>
for all supported formats.</p>
<p>Mac users will need to install GNU coreutils if you want fuzzy time formats.</p>
<p>If your timestamp can’t be parsed, logaround just falls back to loading logs without time filters—no drama.</p>
<hr>
<h2 id="supercharging-your-logs-with-logger">Supercharging Your Logs With <code>logger</code></h2>
<p>Want to bookmark key moments or tests?<br>
Use the built-in logger tool:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">logger <span class="s2">&#34;logaround demo: starting nginx stress test&#34;</span>
</span></span><span class="line"><span class="cl">./logaround.py --term demo --delta <span class="m">10</span>
</span></span></code></pre></div><p>You’ll see your custom log entries, with full context.<br>
Perfect for reproducible bug hunts or marking long troubleshooting sessions.</p>
<hr>
<h2 id="why-move-to-python">Why Move to Python?</h2>
<p>My original bash function worked for quick-and-dirty context—but it was hard to maintain, extend, or customize (and you couldn’t highlight matches in color!). With Python I have greater control over the data and can do all sorts of nice things like:</p>
<ul>
<li>Safely parse every log line (or gracefully handle ones that don’t match)</li>
<li>Highlight what matters</li>
<li>Build new features—like advanced filtering, export, or compound logic</li>
</ul>
<hr>
<h2 id="future-directions">Future Directions</h2>
<ul>
<li>Support for macOS (<code>asl</code>)</li>
<li>Compound and OR/NOT searches</li>
<li>CSV export, better output options</li>
<li>Smarter highlighting and error detection</li>
</ul>
<p><strong>Got a feature wish? Email me, or submit a PR!</strong></p>
<hr>
<h2 id="requirements">Requirements</h2>
<ul>
<li>Python 3.8+</li>
<li><code>pandas</code>, <code>rich</code></li>
<li><code>journalctl</code> (systemd-based systems)</li>
<li>GNU <code>date</code> (for flexible time parsing; default on most Linux distros)</li>
</ul>
<hr>
<h2 id="license">License</h2>
<p><a href="LICENSE">MIT</a></p>
<hr>
<h2 id="conclusion">Conclusion</h2>
<p>That&rsquo;s it! Find log events like a pro with <code>logaround.py</code></p>
<p>Have a tweak, found a bug, or want to share your own logaround wizardry?<br>
<a href="mailto:feedback@adminjitsu.com">Email me!</a></p>
<p>Happy hacking—and may all your outages be short.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Wordstats</title>
      <link>https://adminjitsu.com/posts/wordstats/</link>
      <pubDate>Tue, 29 Jul 2025 17:07:06 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/wordstats/</guid>
      <description>Analyze word counts, frequencies, and text stats directly from the terminal with a single script. Privacy first, and outputs a markdown-style table or CSV for further automation.</description>
      <content:encoded><![CDATA[<h2 id="why-wordstats">Why wordstats?</h2>
<p>Sometimes you want to know how long your amazing new novel is or what word appears most often in the Pulp Fiction screenplay. That&rsquo;s where this simple little tool comes in. It replaces a really ugly shell pipeline that had <strong>multiple</strong> one-liner scripts in it (caveman lambda functions!). I think this is much friendlier to use! You get:</p>
<ul>
<li>
<p><strong>No web upload.</strong><br>
Your writing never leaves your terminal—privacy by default.</p>
</li>
<li>
<p><strong>Blazing fast, works everywhere.</strong><br>
Use it with pipes, files, or straight from the clipboard.</p>
</li>
<li>
<p><strong>Human-friendly output.</strong><br>
Prints a markdown-style table by default, or CSV for automation.</p>
</li>
<li>
<p><strong>Zero dependencies</strong> ( besides Python 3.7+ )</p>
</li>
</ul>
<hr>
<h2 id="usage">Usage</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Count word frequency in a file (top 10)</span>
</span></span><span class="line"><span class="cl">./wordstats.py my_notes.txt --top <span class="m">10</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Pipe input</span>
</span></span><span class="line"><span class="cl">cat todo.txt <span class="p">|</span> ./wordstats.py --top <span class="m">20</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Output CSV for Excel or further scripting</span>
</span></span><span class="line"><span class="cl">./wordstats.py moby-dick.txt --top <span class="m">50</span> --csv
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show *all* words (don’t filter stopwords)</span>
</span></span><span class="line"><span class="cl">./wordstats.py speech.txt --no-stopwords --top <span class="m">100</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Read from clipboard (requires paste alias; see below)</span>
</span></span><span class="line"><span class="cl">paste <span class="p">|</span> ./wordstats.py
</span></span></code></pre></div><h2 id="sample-output">Sample Output</h2>
<p>Default report output
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">┌──[ grumble@shinobi ]:~/codelab/adminjitsu  (main*)
</span></span><span class="line"><span class="cl">└─$ cat ../dotfiles/README.md | wordstats --top 10
</span></span><span class="line"><span class="cl">word         | count
</span></span><span class="line"><span class="cl">-------------+------
</span></span><span class="line"><span class="cl">bash         | 10
</span></span><span class="line"><span class="cl">dotfiles     | 8
</span></span><span class="line"><span class="cl">shell        | 8
</span></span><span class="line"><span class="cl">system       | 6
</span></span><span class="line"><span class="cl">arsenal      | 6
</span></span><span class="line"><span class="cl">setup        | 5
</span></span><span class="line"><span class="cl">youre        | 5
</span></span><span class="line"><span class="cl">hostspecific | 5
</span></span><span class="line"><span class="cl">bootstrapsh  | 5
</span></span><span class="line"><span class="cl">sourcing     | 5
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Total words:      685
</span></span><span class="line"><span class="cl">Unique words:     378
</span></span><span class="line"><span class="cl">Filtered words:   515
</span></span><span class="line"><span class="cl">Character count:  4480
</span></span><span class="line"><span class="cl">Avg word length:  6.54
</span></span><span class="line"><span class="cl">Longest word:     stringusersyournamedotfilesbinarsenalupdatestring (49)
</span></span><span class="line"><span class="cl">Shortest word:    a (1)</span></span></code></pre></div></p>
<p>CSV output
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">┌──[ grumble@shinobi ]:~/codelab/adminjitsu  (main*)
</span></span><span class="line"><span class="cl">└─$ cat ../dotfiles/README.md | wordstats --csv --top 8
</span></span><span class="line"><span class="cl">word,count
</span></span><span class="line"><span class="cl">bash,10
</span></span><span class="line"><span class="cl">dotfiles,8
</span></span><span class="line"><span class="cl">shell,8
</span></span><span class="line"><span class="cl">system,6
</span></span><span class="line"><span class="cl">arsenal,6
</span></span><span class="line"><span class="cl">setup,5
</span></span><span class="line"><span class="cl">youre,5
</span></span><span class="line"><span class="cl">hostspecific,5</span></span></code></pre></div></p>
<h2 id="script-source">Script Source</h2>
<p>You can download the script <a href="/downloads/wordstats.py">here</a></p>
<p>Or with the CLI, with:</p>
<ul>
<li>
<p>with <code>curl</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -O https://adminjitsu.com/scripts/wordstats.py
</span></span><span class="line"><span class="cl">chmod +x wordstats.py
</span></span></code></pre></div></li>
<li>
<p>with <code>wget</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">wget https://adminjitsu.com/scripts/wordstats.py
</span></span><span class="line"><span class="cl">chmod +x wordstats.py
</span></span></code></pre></div></li>
</ul>
<hr>
<h3 id="wordstatspy">wordstats.py</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">  1
</span><span class="lnt">  2
</span><span class="lnt">  3
</span><span class="lnt">  4
</span><span class="lnt">  5
</span><span class="lnt">  6
</span><span class="lnt">  7
</span><span class="lnt">  8
</span><span class="lnt">  9
</span><span class="lnt"> 10
</span><span class="lnt"> 11
</span><span class="lnt"> 12
</span><span class="lnt"> 13
</span><span class="lnt"> 14
</span><span class="lnt"> 15
</span><span class="lnt"> 16
</span><span class="lnt"> 17
</span><span class="lnt"> 18
</span><span class="lnt"> 19
</span><span class="lnt"> 20
</span><span class="lnt"> 21
</span><span class="lnt"> 22
</span><span class="lnt"> 23
</span><span class="lnt"> 24
</span><span class="lnt"> 25
</span><span class="lnt"> 26
</span><span class="lnt"> 27
</span><span class="lnt"> 28
</span><span class="lnt"> 29
</span><span class="lnt"> 30
</span><span class="lnt"> 31
</span><span class="lnt"> 32
</span><span class="lnt"> 33
</span><span class="lnt"> 34
</span><span class="lnt"> 35
</span><span class="lnt"> 36
</span><span class="lnt"> 37
</span><span class="lnt"> 38
</span><span class="lnt"> 39
</span><span class="lnt"> 40
</span><span class="lnt"> 41
</span><span class="lnt"> 42
</span><span class="lnt"> 43
</span><span class="lnt"> 44
</span><span class="lnt"> 45
</span><span class="lnt"> 46
</span><span class="lnt"> 47
</span><span class="lnt"> 48
</span><span class="lnt"> 49
</span><span class="lnt"> 50
</span><span class="lnt"> 51
</span><span class="lnt"> 52
</span><span class="lnt"> 53
</span><span class="lnt"> 54
</span><span class="lnt"> 55
</span><span class="lnt"> 56
</span><span class="lnt"> 57
</span><span class="lnt"> 58
</span><span class="lnt"> 59
</span><span class="lnt"> 60
</span><span class="lnt"> 61
</span><span class="lnt"> 62
</span><span class="lnt"> 63
</span><span class="lnt"> 64
</span><span class="lnt"> 65
</span><span class="lnt"> 66
</span><span class="lnt"> 67
</span><span class="lnt"> 68
</span><span class="lnt"> 69
</span><span class="lnt"> 70
</span><span class="lnt"> 71
</span><span class="lnt"> 72
</span><span class="lnt"> 73
</span><span class="lnt"> 74
</span><span class="lnt"> 75
</span><span class="lnt"> 76
</span><span class="lnt"> 77
</span><span class="lnt"> 78
</span><span class="lnt"> 79
</span><span class="lnt"> 80
</span><span class="lnt"> 81
</span><span class="lnt"> 82
</span><span class="lnt"> 83
</span><span class="lnt"> 84
</span><span class="lnt"> 85
</span><span class="lnt"> 86
</span><span class="lnt"> 87
</span><span class="lnt"> 88
</span><span class="lnt"> 89
</span><span class="lnt"> 90
</span><span class="lnt"> 91
</span><span class="lnt"> 92
</span><span class="lnt"> 93
</span><span class="lnt"> 94
</span><span class="lnt"> 95
</span><span class="lnt"> 96
</span><span class="lnt"> 97
</span><span class="lnt"> 98
</span><span class="lnt"> 99
</span><span class="lnt">100
</span><span class="lnt">101
</span><span class="lnt">102
</span><span class="lnt">103
</span><span class="lnt">104
</span><span class="lnt">105
</span><span class="lnt">106
</span><span class="lnt">107
</span><span class="lnt">108
</span><span class="lnt">109
</span><span class="lnt">110
</span><span class="lnt">111
</span><span class="lnt">112
</span><span class="lnt">113
</span><span class="lnt">114
</span><span class="lnt">115
</span><span class="lnt">116
</span><span class="lnt">117
</span><span class="lnt">118
</span><span class="lnt">119
</span><span class="lnt">120
</span><span class="lnt">121
</span><span class="lnt">122
</span><span class="lnt">123
</span><span class="lnt">124
</span><span class="lnt">125
</span><span class="lnt">126
</span><span class="lnt">127
</span><span class="lnt">128
</span><span class="lnt">129
</span><span class="lnt">130
</span><span class="lnt">131
</span><span class="lnt">132
</span><span class="lnt">133
</span><span class="lnt">134
</span><span class="lnt">135
</span><span class="lnt">136
</span><span class="lnt">137
</span><span class="lnt">138
</span><span class="lnt">139
</span><span class="lnt">140
</span><span class="lnt">141
</span><span class="lnt">142
</span><span class="lnt">143
</span><span class="lnt">144
</span><span class="lnt">145
</span><span class="lnt">146
</span><span class="lnt">147
</span><span class="lnt">148
</span><span class="lnt">149
</span><span class="lnt">150
</span><span class="lnt">151
</span><span class="lnt">152
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl">  <span class="c1">#!/usr/bin/env python3</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">  wordstats.py — Analyze and print word frequency and text stats.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">  Usage:
</span></span></span><span class="line"><span class="cl"><span class="s2">    wordstats.py [input.txt] [--top N] [--no-stopwords] [--csv]
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">  Reads from stdin if no input file is given.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">  Options:
</span></span></span><span class="line"><span class="cl"><span class="s2">    --top N           Show top N words (default: 100)
</span></span></span><span class="line"><span class="cl"><span class="s2">    --no-stopwords    Do not filter out stopwords
</span></span></span><span class="line"><span class="cl"><span class="s2">    --csv             Output as CSV instead of ASCII table
</span></span></span><span class="line"><span class="cl"><span class="s2">    -h, --help        Show this help and exit
</span></span></span><span class="line"><span class="cl"><span class="s2">    -v, --version     Show version and exit
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">  Outputs:
</span></span></span><span class="line"><span class="cl"><span class="s2">    - Table (or CSV) of N most common words and counts
</span></span></span><span class="line"><span class="cl"><span class="s2">    - Total words, unique words, filtered word count
</span></span></span><span class="line"><span class="cl"><span class="s2">    - Character count, average word length, longest/shortest word
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="cl">  <span class="kn">import</span> <span class="nn">os</span>
</span></span><span class="line"><span class="cl">  <span class="kn">import</span> <span class="nn">re</span>
</span></span><span class="line"><span class="cl">  <span class="kn">import</span> <span class="nn">argparse</span>
</span></span><span class="line"><span class="cl">  <span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">Counter</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">__version__</span> <span class="o">=</span> <span class="s2">&#34;1.0.1&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># ─── BUILT-IN STOPWORDS ──────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># These are the standard English stopwords from wordcloud&#39;s STOPWORDS set,</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># hardcoded here for zero dependencies. Expand/edit as you like.</span>
</span></span><span class="line"><span class="cl">  <span class="n">STOPWORDS</span> <span class="o">=</span> <span class="nb">set</span><span class="p">([</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;a&#34;</span><span class="p">,</span> <span class="s2">&#34;about&#34;</span><span class="p">,</span> <span class="s2">&#34;above&#34;</span><span class="p">,</span> <span class="s2">&#34;after&#34;</span><span class="p">,</span> <span class="s2">&#34;again&#34;</span><span class="p">,</span> <span class="s2">&#34;against&#34;</span><span class="p">,</span> <span class="s2">&#34;all&#34;</span><span class="p">,</span> <span class="s2">&#34;am&#34;</span><span class="p">,</span> <span class="s2">&#34;an&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;and&#34;</span><span class="p">,</span> <span class="s2">&#34;any&#34;</span><span class="p">,</span> <span class="s2">&#34;are&#34;</span><span class="p">,</span> <span class="s2">&#34;aren&#39;t&#34;</span><span class="p">,</span> <span class="s2">&#34;as&#34;</span><span class="p">,</span> <span class="s2">&#34;at&#34;</span><span class="p">,</span> <span class="s2">&#34;be&#34;</span><span class="p">,</span> <span class="s2">&#34;because&#34;</span><span class="p">,</span> <span class="s2">&#34;been&#34;</span><span class="p">,</span> <span class="s2">&#34;before&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;being&#34;</span><span class="p">,</span> <span class="s2">&#34;below&#34;</span><span class="p">,</span> <span class="s2">&#34;between&#34;</span><span class="p">,</span> <span class="s2">&#34;both&#34;</span><span class="p">,</span> <span class="s2">&#34;but&#34;</span><span class="p">,</span> <span class="s2">&#34;by&#34;</span><span class="p">,</span> <span class="s2">&#34;can&#39;t&#34;</span><span class="p">,</span> <span class="s2">&#34;cannot&#34;</span><span class="p">,</span> <span class="s2">&#34;could&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;couldn&#39;t&#34;</span><span class="p">,</span> <span class="s2">&#34;did&#34;</span><span class="p">,</span> <span class="s2">&#34;didn&#39;t&#34;</span><span class="p">,</span> <span class="s2">&#34;do&#34;</span><span class="p">,</span> <span class="s2">&#34;does&#34;</span><span class="p">,</span> <span class="s2">&#34;doesn&#39;t&#34;</span><span class="p">,</span> <span class="s2">&#34;doing&#34;</span><span class="p">,</span> <span class="s2">&#34;don&#39;t&#34;</span><span class="p">,</span> <span class="s2">&#34;down&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;during&#34;</span><span class="p">,</span> <span class="s2">&#34;each&#34;</span><span class="p">,</span> <span class="s2">&#34;few&#34;</span><span class="p">,</span> <span class="s2">&#34;for&#34;</span><span class="p">,</span> <span class="s2">&#34;from&#34;</span><span class="p">,</span> <span class="s2">&#34;further&#34;</span><span class="p">,</span> <span class="s2">&#34;had&#34;</span><span class="p">,</span> <span class="s2">&#34;hadn&#39;t&#34;</span><span class="p">,</span> <span class="s2">&#34;has&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;hasn&#39;t&#34;</span><span class="p">,</span> <span class="s2">&#34;have&#34;</span><span class="p">,</span> <span class="s2">&#34;haven&#39;t&#34;</span><span class="p">,</span> <span class="s2">&#34;having&#34;</span><span class="p">,</span> <span class="s2">&#34;he&#34;</span><span class="p">,</span> <span class="s2">&#34;he&#39;d&#34;</span><span class="p">,</span> <span class="s2">&#34;he&#39;ll&#34;</span><span class="p">,</span> <span class="s2">&#34;he&#39;s&#34;</span><span class="p">,</span> <span class="s2">&#34;her&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;here&#34;</span><span class="p">,</span> <span class="s2">&#34;here&#39;s&#34;</span><span class="p">,</span> <span class="s2">&#34;hers&#34;</span><span class="p">,</span> <span class="s2">&#34;herself&#34;</span><span class="p">,</span> <span class="s2">&#34;him&#34;</span><span class="p">,</span> <span class="s2">&#34;himself&#34;</span><span class="p">,</span> <span class="s2">&#34;his&#34;</span><span class="p">,</span> <span class="s2">&#34;how&#34;</span><span class="p">,</span> <span class="s2">&#34;how&#39;s&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;i&#34;</span><span class="p">,</span> <span class="s2">&#34;i&#39;d&#34;</span><span class="p">,</span> <span class="s2">&#34;i&#39;ll&#34;</span><span class="p">,</span> <span class="s2">&#34;i&#39;m&#34;</span><span class="p">,</span> <span class="s2">&#34;i&#39;ve&#34;</span><span class="p">,</span> <span class="s2">&#34;if&#34;</span><span class="p">,</span> <span class="s2">&#34;in&#34;</span><span class="p">,</span> <span class="s2">&#34;into&#34;</span><span class="p">,</span> <span class="s2">&#34;is&#34;</span><span class="p">,</span> <span class="s2">&#34;isn&#39;t&#34;</span><span class="p">,</span> <span class="s2">&#34;it&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;it&#39;s&#34;</span><span class="p">,</span> <span class="s2">&#34;its&#34;</span><span class="p">,</span> <span class="s2">&#34;itself&#34;</span><span class="p">,</span> <span class="s2">&#34;let&#39;s&#34;</span><span class="p">,</span> <span class="s2">&#34;me&#34;</span><span class="p">,</span> <span class="s2">&#34;more&#34;</span><span class="p">,</span> <span class="s2">&#34;most&#34;</span><span class="p">,</span> <span class="s2">&#34;mustn&#39;t&#34;</span><span class="p">,</span> <span class="s2">&#34;my&#34;</span><span class="p">,</span> <span class="s2">&#34;myself&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;no&#34;</span><span class="p">,</span> <span class="s2">&#34;nor&#34;</span><span class="p">,</span> <span class="s2">&#34;not&#34;</span><span class="p">,</span> <span class="s2">&#34;of&#34;</span><span class="p">,</span> <span class="s2">&#34;off&#34;</span><span class="p">,</span> <span class="s2">&#34;on&#34;</span><span class="p">,</span> <span class="s2">&#34;once&#34;</span><span class="p">,</span> <span class="s2">&#34;only&#34;</span><span class="p">,</span> <span class="s2">&#34;or&#34;</span><span class="p">,</span> <span class="s2">&#34;other&#34;</span><span class="p">,</span> <span class="s2">&#34;ought&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;our&#34;</span><span class="p">,</span> <span class="s2">&#34;ours&#34;</span><span class="p">,</span> <span class="s2">&#34;ourselves&#34;</span><span class="p">,</span> <span class="s2">&#34;out&#34;</span><span class="p">,</span> <span class="s2">&#34;over&#34;</span><span class="p">,</span> <span class="s2">&#34;own&#34;</span><span class="p">,</span> <span class="s2">&#34;same&#34;</span><span class="p">,</span> <span class="s2">&#34;shan&#39;t&#34;</span><span class="p">,</span> <span class="s2">&#34;she&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;she&#39;d&#34;</span><span class="p">,</span> <span class="s2">&#34;she&#39;ll&#34;</span><span class="p">,</span> <span class="s2">&#34;she&#39;s&#34;</span><span class="p">,</span> <span class="s2">&#34;should&#34;</span><span class="p">,</span> <span class="s2">&#34;shouldn&#39;t&#34;</span><span class="p">,</span> <span class="s2">&#34;so&#34;</span><span class="p">,</span> <span class="s2">&#34;some&#34;</span><span class="p">,</span> <span class="s2">&#34;such&#34;</span><span class="p">,</span> <span class="s2">&#34;than&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;that&#34;</span><span class="p">,</span> <span class="s2">&#34;that&#39;s&#34;</span><span class="p">,</span> <span class="s2">&#34;the&#34;</span><span class="p">,</span> <span class="s2">&#34;their&#34;</span><span class="p">,</span> <span class="s2">&#34;theirs&#34;</span><span class="p">,</span> <span class="s2">&#34;them&#34;</span><span class="p">,</span> <span class="s2">&#34;themselves&#34;</span><span class="p">,</span> <span class="s2">&#34;then&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;there&#34;</span><span class="p">,</span> <span class="s2">&#34;there&#39;s&#34;</span><span class="p">,</span> <span class="s2">&#34;these&#34;</span><span class="p">,</span> <span class="s2">&#34;they&#34;</span><span class="p">,</span> <span class="s2">&#34;they&#39;d&#34;</span><span class="p">,</span> <span class="s2">&#34;they&#39;ll&#34;</span><span class="p">,</span> <span class="s2">&#34;they&#39;re&#34;</span><span class="p">,</span> <span class="s2">&#34;they&#39;ve&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;this&#34;</span><span class="p">,</span> <span class="s2">&#34;those&#34;</span><span class="p">,</span> <span class="s2">&#34;through&#34;</span><span class="p">,</span> <span class="s2">&#34;to&#34;</span><span class="p">,</span> <span class="s2">&#34;too&#34;</span><span class="p">,</span> <span class="s2">&#34;under&#34;</span><span class="p">,</span> <span class="s2">&#34;until&#34;</span><span class="p">,</span> <span class="s2">&#34;up&#34;</span><span class="p">,</span> <span class="s2">&#34;very&#34;</span><span class="p">,</span> <span class="s2">&#34;was&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;wasn&#39;t&#34;</span><span class="p">,</span> <span class="s2">&#34;we&#34;</span><span class="p">,</span> <span class="s2">&#34;we&#39;d&#34;</span><span class="p">,</span> <span class="s2">&#34;we&#39;ll&#34;</span><span class="p">,</span> <span class="s2">&#34;we&#39;re&#34;</span><span class="p">,</span> <span class="s2">&#34;we&#39;ve&#34;</span><span class="p">,</span> <span class="s2">&#34;were&#34;</span><span class="p">,</span> <span class="s2">&#34;weren&#39;t&#34;</span><span class="p">,</span> <span class="s2">&#34;what&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;what&#39;s&#34;</span><span class="p">,</span> <span class="s2">&#34;when&#34;</span><span class="p">,</span> <span class="s2">&#34;when&#39;s&#34;</span><span class="p">,</span> <span class="s2">&#34;where&#34;</span><span class="p">,</span> <span class="s2">&#34;where&#39;s&#34;</span><span class="p">,</span> <span class="s2">&#34;which&#34;</span><span class="p">,</span> <span class="s2">&#34;while&#34;</span><span class="p">,</span> <span class="s2">&#34;who&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;who&#39;s&#34;</span><span class="p">,</span> <span class="s2">&#34;whom&#34;</span><span class="p">,</span> <span class="s2">&#34;why&#34;</span><span class="p">,</span> <span class="s2">&#34;why&#39;s&#34;</span><span class="p">,</span> <span class="s2">&#34;with&#34;</span><span class="p">,</span> <span class="s2">&#34;won&#39;t&#34;</span><span class="p">,</span> <span class="s2">&#34;would&#34;</span><span class="p">,</span> <span class="s2">&#34;wouldn&#39;t&#34;</span><span class="p">,</span> <span class="s2">&#34;you&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;you&#39;d&#34;</span><span class="p">,</span> <span class="s2">&#34;you&#39;ll&#34;</span><span class="p">,</span> <span class="s2">&#34;you&#39;re&#34;</span><span class="p">,</span> <span class="s2">&#34;you&#39;ve&#34;</span><span class="p">,</span> <span class="s2">&#34;your&#34;</span><span class="p">,</span> <span class="s2">&#34;yours&#34;</span><span class="p">,</span> <span class="s2">&#34;yourself&#34;</span><span class="p">,</span> <span class="s2">&#34;yourselves&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">])</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># ─── PARSE ARGUMENTS ─────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">parse_args</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;&#34;&#34;Parse command line arguments and handle help/version for help2man.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="n">p</span> <span class="o">=</span> <span class="n">argparse</span><span class="o">.</span><span class="n">ArgumentParser</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Show word frequency and text stats from text input.&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">p</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s1">&#39;input&#39;</span><span class="p">,</span> <span class="n">nargs</span><span class="o">=</span><span class="s1">&#39;?&#39;</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s1">&#39;Input file (or stdin)&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">p</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s1">&#39;--top&#39;</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">int</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">100</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s1">&#39;Show top N words (default: 100)&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">p</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s1">&#39;--no-stopwords&#39;</span><span class="p">,</span> <span class="n">action</span><span class="o">=</span><span class="s1">&#39;store_true&#39;</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s1">&#39;Do not filter out stopwords&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">p</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s1">&#39;--csv&#39;</span><span class="p">,</span> <span class="n">action</span><span class="o">=</span><span class="s1">&#39;store_true&#39;</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s1">&#39;Output CSV instead of table&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">p</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s1">&#39;-v&#39;</span><span class="p">,</span> <span class="s1">&#39;--version&#39;</span><span class="p">,</span> <span class="n">action</span><span class="o">=</span><span class="s1">&#39;store_true&#39;</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s1">&#39;Show version and exit&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="n">p</span><span class="o">.</span><span class="n">parse_args</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># ─── LOAD AND CLEAN TEXT ─────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">load_text</span><span class="p">(</span><span class="n">path</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">      Load text from a file or stdin.
</span></span></span><span class="line"><span class="cl"><span class="s2">      If no path is given and stdin is not a TTY, read stdin.
</span></span></span><span class="line"><span class="cl"><span class="s2">      &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="ow">not</span> <span class="n">sys</span><span class="o">.</span><span class="n">stdin</span><span class="o">.</span><span class="n">isatty</span><span class="p">()</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">path</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">          <span class="k">return</span> <span class="n">sys</span><span class="o">.</span><span class="n">stdin</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">      <span class="k">elif</span> <span class="n">path</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">          <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">              <span class="k">return</span> <span class="n">f</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">      <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">          <span class="nb">print</span><span class="p">(</span><span class="vm">__doc__</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">clean_words</span><span class="p">(</span><span class="n">text</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">      Lowercase, strip punctuation/digits, split into words.
</span></span></span><span class="line"><span class="cl"><span class="s2">      Returns list of words.
</span></span></span><span class="line"><span class="cl"><span class="s2">      &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="n">text</span> <span class="o">=</span> <span class="n">text</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">      <span class="n">text</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;[^\w\s]&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">text</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;\d+&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="n">text</span><span class="o">.</span><span class="n">split</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># ─── OUTPUT FORMATTING ───────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">print_table</span><span class="p">(</span><span class="n">rows</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">      Print an ASCII table (like markdown) for terminal output.
</span></span></span><span class="line"><span class="cl"><span class="s2">      &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="n">col_widths</span> <span class="o">=</span> <span class="p">[</span><span class="nb">max</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">x</span><span class="p">))</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">col</span><span class="p">)</span> <span class="k">for</span> <span class="n">col</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="o">*</span><span class="p">([</span><span class="n">headers</span><span class="p">]</span> <span class="o">+</span> <span class="n">rows</span><span class="p">))]</span> <span class="k">if</span> <span class="n">headers</span> <span class="k">else</span> <span class="p">[</span><span class="nb">max</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">x</span><span class="p">))</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">col</span><span class="p">)</span> <span class="k">for</span> <span class="n">col</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="o">*</span><span class="n">rows</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="n">headers</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">          <span class="nb">print</span><span class="p">(</span><span class="s2">&#34; | &#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">h</span><span class="p">)</span><span class="o">.</span><span class="n">ljust</span><span class="p">(</span><span class="n">w</span><span class="p">)</span> <span class="k">for</span> <span class="n">h</span><span class="p">,</span> <span class="n">w</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">headers</span><span class="p">,</span> <span class="n">col_widths</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl">          <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;-+-&#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34;-&#34;</span> <span class="o">*</span> <span class="n">w</span> <span class="k">for</span> <span class="n">w</span> <span class="ow">in</span> <span class="n">col_widths</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">      <span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">rows</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">          <span class="nb">print</span><span class="p">(</span><span class="s2">&#34; | &#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">v</span><span class="p">)</span><span class="o">.</span><span class="n">ljust</span><span class="p">(</span><span class="n">w</span><span class="p">)</span> <span class="k">for</span> <span class="n">v</span><span class="p">,</span> <span class="n">w</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">row</span><span class="p">,</span> <span class="n">col_widths</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># ─── MAIN LOGIC ──────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">      <span class="n">args</span> <span class="o">=</span> <span class="n">parse_args</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="c1"># --help and --version for help2man compatibility</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="n">args</span><span class="o">.</span><span class="n">version</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">          <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;wordstats.py </span><span class="si">{</span><span class="n">__version__</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="n">text</span> <span class="o">=</span> <span class="n">load_text</span><span class="p">(</span><span class="n">args</span><span class="o">.</span><span class="n">input</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">words</span> <span class="o">=</span> <span class="n">clean_words</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="n">total_words</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">words</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">unique_words</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">words</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">      <span class="n">char_count</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">w</span><span class="p">)</span> <span class="k">for</span> <span class="n">w</span> <span class="ow">in</span> <span class="n">words</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">avg_wordlen</span> <span class="o">=</span> <span class="p">(</span><span class="n">char_count</span> <span class="o">/</span> <span class="n">total_words</span><span class="p">)</span> <span class="k">if</span> <span class="n">total_words</span> <span class="k">else</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">      <span class="n">longest</span> <span class="o">=</span> <span class="nb">max</span><span class="p">(</span><span class="n">words</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="nb">len</span><span class="p">)</span> <span class="k">if</span> <span class="n">words</span> <span class="k">else</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="n">shortest</span> <span class="o">=</span> <span class="nb">min</span><span class="p">(</span><span class="n">words</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="nb">len</span><span class="p">)</span> <span class="k">if</span> <span class="n">words</span> <span class="k">else</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="c1"># ─── STOPWORDS FILTERING (NOW ZERO DEPENDENCIES) ─────────────────────────</span>
</span></span><span class="line"><span class="cl">      <span class="c1"># If user didn&#39;t specify --no-stopwords, filter using the built-in set.</span>
</span></span><span class="line"><span class="cl">      <span class="n">stops</span> <span class="o">=</span> <span class="nb">set</span><span class="p">(</span><span class="n">STOPWORDS</span><span class="p">)</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">args</span><span class="o">.</span><span class="n">no_stopwords</span> <span class="k">else</span> <span class="nb">set</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">      <span class="n">filtered</span> <span class="o">=</span> <span class="p">[</span><span class="n">w</span> <span class="k">for</span> <span class="n">w</span> <span class="ow">in</span> <span class="n">words</span> <span class="k">if</span> <span class="n">w</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">stops</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="n">freq</span> <span class="o">=</span> <span class="n">Counter</span><span class="p">(</span><span class="n">filtered</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">most_common</span> <span class="o">=</span> <span class="n">freq</span><span class="o">.</span><span class="n">most_common</span><span class="p">(</span><span class="n">args</span><span class="o">.</span><span class="n">top</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="c1"># ── Output ──</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="n">args</span><span class="o">.</span><span class="n">csv</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">          <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;word,count&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="k">for</span> <span class="n">word</span><span class="p">,</span> <span class="n">count</span> <span class="ow">in</span> <span class="n">most_common</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">              <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">word</span><span class="si">}</span><span class="s2">,</span><span class="si">{</span><span class="n">count</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">          <span class="n">print_table</span><span class="p">([(</span><span class="n">w</span><span class="p">,</span> <span class="n">c</span><span class="p">)</span> <span class="k">for</span> <span class="n">w</span><span class="p">,</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">most_common</span><span class="p">],</span> <span class="n">headers</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;word&#34;</span><span class="p">,</span> <span class="s2">&#34;count&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">          <span class="nb">print</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">          <span class="c1"># Extra stats for the curious</span>
</span></span><span class="line"><span class="cl">          <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Total words:      </span><span class="si">{</span><span class="n">total_words</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Unique words:     </span><span class="si">{</span><span class="n">unique_words</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Filtered words:   </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">filtered</span><span class="p">)</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Character count:  </span><span class="si">{</span><span class="n">char_count</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Avg word length:  </span><span class="si">{</span><span class="n">avg_wordlen</span><span class="si">:</span><span class="s2">.2f</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Longest word:     </span><span class="si">{</span><span class="n">longest</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">longest</span><span class="p">)</span><span class="si">}</span><span class="s2">)&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Shortest word:    </span><span class="si">{</span><span class="n">shortest</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">shortest</span><span class="p">)</span><span class="si">}</span><span class="s2">)&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># ─── ENTRYPOINT ──────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">      <span class="n">main</span><span class="p">()</span></span></span></code></pre></td></tr></table>
</div>
</div>
<hr>
<h2 id="customization">Customization</h2>
<p>This script would be easy to extend or customize if needed. A few things that spring to mind:</p>
<ul>
<li>edit the stopwords list to control what words the script will ignore</li>
<li>for other languages, you can paste in a different language set from NLTK on <a href="https://github.com/nltk/nltk_data/blob/gh-pages/packages/corpora/stopwords.zip">GitHub</a>. You could use the files directly by changing <code>STOPWORDS</code> :
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">STOPWORDS</span> <span class="o">=</span> <span class="nb">set</span><span class="p">(</span><span class="nb">open</span><span class="p">(</span><span class="s2">&#34;french_stopwords.txt&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">read</span><span class="p">()</span><span class="o">.</span><span class="n">split</span><span class="p">())</span>
</span></span></code></pre></div></li>
<li>you can edit the default value of <code>--top</code> in the <code>parse_args</code> section to show more/fewer words.</li>
<li>change column widths, table formatting, or add new columns (like relative frequency)</li>
</ul>
<h2 id="see-also">See also</h2>
<p>You may want to define a <code>clip</code> and a <code>paste</code> alias. This makes it simple to use the clipboard with this (and tons of other programs)</p>
<ul>
<li>
<p><strong>Linux</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Linux clipboard (assumes xclip is installed)</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">clip</span><span class="o">=</span><span class="s1">&#39;xclip -selection clipboard&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">paste</span><span class="o">=</span><span class="s1">&#39;xclip -selection clipboard -o&#39;</span>
</span></span></code></pre></div></li>
<li>
<p><strong>macOS</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># macOS clipboard</span>
</span></span><span class="line"><span class="cl"><span class="c1"># for consistency with linux environment</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">clip</span><span class="o">=</span><span class="s1">&#39;pbcopy&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">paste</span><span class="o">=</span><span class="s1">&#39;pbpaste&#39;</span>
</span></span></code></pre></div></li>
</ul>
<p>Add those aliases to your shell startup (e.g., <code>.bashrc</code> or <code>.zshrc</code>) and reload.</p>
<p>Now you can interact with the clipboard like so:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># save stats to system clipboard</span>
</span></span><span class="line"><span class="cl">wordstats.py --top <span class="m">20</span> README.md <span class="p">|</span> clip
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># get stats on text IN the system clipboard</span>
</span></span><span class="line"><span class="cl">paste <span class="p">|</span> wordstats.py
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># use in a pipeline</span>
</span></span><span class="line"><span class="cl">cat somefile.txt <span class="p">|</span> grep -i error <span class="p">|</span> wordstats.py --top <span class="p">|</span> clip
</span></span></code></pre></div><h2 id="conclusion">Conclusion</h2>
<p>That&rsquo;s it. quick word stats from the CLI. Just feed it your text and get a useful response! Any feedback or questions, please feel free to email me:
<a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Wordclouder</title>
      <link>https://adminjitsu.com/posts/wordclouder/</link>
      <pubDate>Mon, 28 Jul 2025 13:14:29 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/wordclouder/</guid>
      <description>A modern Python CLI to create stunning wordclouds from any text, file, or clipboard input. No web uploads, no sketchy sites, just artful data viz from your shell.</description>
      <content:encoded><![CDATA[<h2 id="what-is-wordclouder">What is wordclouder?</h2>
<p><strong>wordclouder</strong> is a no-nonsense Python tool for generating beautiful wordcloud images from any text—file,clipboard, or piped input. Forget awkward web apps: with wordclouder, you get instant, local results, every time.</p>
<hr>
<h2 id="why-is-this-cool">Why is this cool?</h2>
<p>I wanted to create a simple tool that made it easy to visualize arbitrary text. With <code>wordclouder</code>, visualizations are just a quick command away, opening up all kinds of creative uses.</p>
<ul>
<li><strong>Everything stays local.</strong><br>
No privacy worries, no tracking, no uploading your writing to “free” online wordcloud generators.</li>
<li><strong>Works with anything.</strong><br>
Books, logs, emails, speeches, code, Slack threads—if it’s text, it works.</li>
<li><strong>Powerful automation.</strong><br>
Chain it with <code>cat</code>, <code>grep</code>, or <code>fortune</code> to turn any data into art.</li>
<li><strong>Fast, flexible, and hackable.</strong><br>
Change colors, add stopwords, automate output—all open source and under your control.</li>
</ul>
<hr>
<h2 id="usage">Usage</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Generate from a file</span>
</span></span><span class="line"><span class="cl">./wordclouder.py a-new-hope.txt
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Pipe in text</span>
</span></span><span class="line"><span class="cl">cat your_notes.txt <span class="p">|</span> ./wordclouder.py
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># From fortune, to see what fate says</span>
</span></span><span class="line"><span class="cl">fortune <span class="p">|</span> ./wordclouder.py
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Save to a specific image</span>
</span></span><span class="line"><span class="cl">./wordclouder.py script.txt -o starwars-cloud.png
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Copy any text (from browser, editor, terminal, etc), then:</span>
</span></span><span class="line"><span class="cl">xclip -selection clipboard -o <span class="p">|</span> ./wordclouder.py --output ~/Pictures/clipboard-cloud.png
</span></span></code></pre></div><p>By default, images save to <code>~/Pictures/wordclouds</code> with a timestamped name. Use <code>-o</code> to pick your own filename or directory.</p>
<hr>
<h3 id="output-example">Output Example</h3>
  <p align="center"> 
  <img src="a-new-hope-wordcloud.png" alt="Sample wordcloud from 'A New Hope'" width="800"> 
  *A New Hope* as a wordcloud—look sir, droids!! 
  </p>
<hr>
<h2 id="installation">Installation</h2>
<p class="github-btn">
  <a href="https://github.com/forfaxx/wordclouder" target="_blank">
    🔗 View wordclouder on GitHub
  </a>
</p>
<p>Clone from GitHub:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone https://github.com/forfaxx/wordclouder.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> wordclouder
</span></span><span class="line"><span class="cl">python3 -m venv venv
</span></span><span class="line"><span class="cl"><span class="nb">source</span> venv/bin/activate
</span></span><span class="line"><span class="cl">pip install -r requirements.txt
</span></span></code></pre></div><p>Or, just grab the single script and run with Python 3.7+ and <code>wordcloud</code> installed.</p>
<h2 id="optional-and-advanced">Optional and advanced</h2>
<h3 id="create-a-wrapper-to-easily-run-wordclouder-globally">Create a wrapper to easily run wordclouder globally</h3>
<p>The following steps make it easy to run complex Python tools like wordclouder, which requires a virtual environment and may live in a folder with various assets and requirements.</p>
<p>To make it easier to use once set up, I have included my venv-launch.sh script. Simply put this in your PATH, then create a small wrapper shell script (also in your PATH). Once set up, this technique makes it ridiculously easy to run by automatically managing the virtual environment so you don&rsquo;t have to.</p>
<ol>
<li>Copy <code>venv-launch.sh</code> to your PATH, e.g.:</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cp venv-launch.sh ~/bin/
</span></span><span class="line"><span class="cl">chmod +x ~/bin/venv-launch.sh
</span></span></code></pre></div><ol start="2">
<li>Create a wrapper script for <code>wordclouder</code></li>
</ol>
<p>This script lets you type <code>wordclouder</code> from anywhere on your system</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Create ~/bin/wordclouder with these contents:</span>
</span></span><span class="line"><span class="cl">cat &gt; ~/bin/wordclouder <span class="s">&lt;&lt;&#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="s"># Launches wordclouder.py inside its venv, if available
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">SCRIPT_DIR=&#34;$HOME/tools/wordclouder&#34;   # Adjust this if your path is different
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">exec ~/bin/venv-launch.sh &#34;$SCRIPT_DIR/wordclouder.py&#34; &#34;$@&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">chmod +x ~/bin/wordclouder</span></span></code></pre></td></tr></table>
</div>
</div>
<ul>
<li><span class="tag green tag-pill">Pro-Tip:</span><br>
Add ~/bin to your PATH (if not already):
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;export PATH=&#34;$HOME/bin:$PATH&#34;&#39;</span> &gt;&gt; ~/.bashrc
</span></span><span class="line"><span class="cl"><span class="nb">source</span> ~/.bashrc
</span></span></code></pre></div></li>
</ul>
<ol start="3">
<li>Run!</li>
</ol>
<p>Now you can run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">wordclouder somefile.txt
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;hello world&#34;</span> <span class="p">|</span> wordclouder
</span></span></code></pre></div><h3 id="customizing-output-in-wordclouderpy">Customizing Output in <code>wordclouder.py</code></h3>
<p>You can customize the following settings in the <code>generate_wordcloud</code> function if desired.</p>
<p><strong>1. Custom Stopwords</strong></p>
<p>Find:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">custom_stops</span> <span class="o">=</span> <span class="nb">set</span><span class="p">([</span><span class="s2">&#34;said&#34;</span><span class="p">,</span> <span class="s2">&#34;would&#34;</span><span class="p">,</span> <span class="s2">&#34;could&#34;</span><span class="p">,</span> <span class="s2">&#34;one&#34;</span><span class="p">,</span> <span class="s2">&#34;also&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="n">stopwords</span> <span class="o">=</span> <span class="n">STOPWORDS</span><span class="o">.</span><span class="n">union</span><span class="p">(</span><span class="n">custom_stops</span><span class="p">)</span>
</span></span></code></pre></div><p>Edit <code>custom_stops</code> to add/remove the exact words you want ignored in your cloud.</p>
<p><strong>2. Appearance: Size, Color, Style</strong></p>
<p>Find:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">wc</span> <span class="o">=</span> <span class="n">WordCloud</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">width</span><span class="o">=</span><span class="mi">800</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">height</span><span class="o">=</span><span class="mi">400</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">background_color</span><span class="o">=</span><span class="s2">&#34;white&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">stopwords</span><span class="o">=</span><span class="n">stopwords</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">colormap</span><span class="o">=</span><span class="s2">&#34;viridis&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span><span class="o">.</span><span class="n">generate</span><span class="p">(</span><span class="n">cleaned</span><span class="p">)</span>
</span></span></code></pre></div><p><strong>Change these parameters directly:</strong></p>
<p><code>width</code> and <code>height</code>: Output image size in pixels.</p>
<p><code>background_color</code>: Any valid CSS color (e.g., &ldquo;black&rdquo;, &ldquo;white&rdquo;, &ldquo;#222233&rdquo;)</p>
<p><code>colormap</code>: Try &ldquo;plasma&rdquo;, &ldquo;magma&rdquo;, &ldquo;inferno&rdquo;, &ldquo;cool&rdquo;, &ldquo;Set2&rdquo;, etc.</p>
<p>Full list: <a href="https://matplotlib.org/stable/users/explain/colors/colormaps.html">matplotlib colormaps</a></p>
<p><strong>3. Example: All Tweaks at Once</strong></p>
<p>**Replace your generate_wordcloud() function with: **
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">generate_wordcloud</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">output_path</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Generate and save a wordcloud image from text.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">custom_stops</span> <span class="o">=</span> <span class="nb">set</span><span class="p">([</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;said&#34;</span><span class="p">,</span> <span class="s2">&#34;would&#34;</span><span class="p">,</span> <span class="s2">&#34;could&#34;</span><span class="p">,</span> <span class="s2">&#34;one&#34;</span><span class="p">,</span> <span class="s2">&#34;also&#34;</span><span class="p">,</span> <span class="s2">&#34;really&#34;</span><span class="p">,</span> <span class="s2">&#34;like&#34;</span><span class="p">,</span> <span class="s2">&#34;get&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">])</span>
</span></span><span class="line"><span class="cl">    <span class="n">stopwords</span> <span class="o">=</span> <span class="n">STOPWORDS</span><span class="o">.</span><span class="n">union</span><span class="p">(</span><span class="n">custom_stops</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">cleaned</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;[^\w\s]&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">cleaned</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;\d+&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="n">cleaned</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">wc</span> <span class="o">=</span> <span class="n">WordCloud</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">width</span><span class="o">=</span><span class="mi">1000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">height</span><span class="o">=</span><span class="mi">600</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">background_color</span><span class="o">=</span><span class="s2">&#34;black&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">stopwords</span><span class="o">=</span><span class="n">stopwords</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">colormap</span><span class="o">=</span><span class="s2">&#34;magma&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span><span class="o">.</span><span class="n">generate</span><span class="p">(</span><span class="n">cleaned</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">wc</span><span class="o">.</span><span class="n">to_file</span><span class="p">(</span><span class="n">output_path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;✅ Wordcloud saved to: </span><span class="si">{</span><span class="n">output_path</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></p>
<h3 id="clip-aliases-to-make-clipboardpasteboard-easier">Clip aliases to make clipboard/pasteboard easier</h3>
<p>On my systems I set the clip and paste aliases to make it easier to access the clipboard the same way regardless of OS.</p>
<p>On Linux:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Linux clipboard (assumes xclip is installed)</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">clip</span><span class="o">=</span><span class="s1">&#39;xclip -selection clipboard&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">paste</span><span class="o">=</span><span class="s1">&#39;xclip -selection clipboard -o&#39;</span>
</span></span></code></pre></div><p>On macOS:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># macOS clipboard</span>
</span></span><span class="line"><span class="cl"><span class="c1"># for consistency with linux environment</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">clip</span><span class="o">=</span><span class="s1">&#39;pbcopy&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">paste</span><span class="o">=</span><span class="s1">&#39;pbpaste&#39;</span>
</span></span></code></pre></div><p>Then you can do things like: <code>paste | wordclouder.py</code></p>
<h2 id="conclusion">Conclusion</h2>
<p>That&rsquo;s it—a dead-simple, private way to turn any text into instant, meaningful visuals. Perfect for books, logs, song lyrics, love letters, and more, all from the CLI.</p>
<p>Questions or feedback? PRs welcome or Email me:
<a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Cromulent Words</title>
      <link>https://adminjitsu.com/posts/cromulent-words/</link>
      <pubDate>Sun, 27 Jul 2025 21:42:44 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/cromulent-words/</guid>
      <description>A nerd’s glossary of cromulent and embiggened words—from Simpsons lingo to tech, computer science, philosophy, and language trivia. Great for geeks, coders, and word lovers.</description>
      <content:encoded><![CDATA[<p align="center">
  <img src="embiggen.jpg" alt="Screenshot from The Simpsons" style="width:70%;max-width:580px;">
  <em>“A noble spirit embiggens the smallest man.”</em><br>
  <em>The Simpsons</em>, S07E16, “Lisa the Iconoclast.”
  Image © Fox Broadcasting / The Simpsons. <br>
</p>
<br>
<p>Over the years, I&rsquo;ve collected some words, terms, and concepts that I think are &ldquo;perfectly cromulent&rdquo; (to paraphrase the Simpsons). These come from fields such as philosophy, computer science, and linguistics—terms that subtly shape my thinking and even make me a better admin. Here’s a sort of lexicon of Adminjitsu.</p>
<p style="text-align:center;">
  <a href="/tags/cromulent" class="button">🧾 View Cromulent Words Series</a>
</p>
<hr>
<h2 id="some-computer-science-terms">Some Computer Science Terms</h2>
<ul>
<li>
<p><strong>Idempotent</strong><br>
<em>An operation you can repeat over and over and always get the same result.</em><br>
<strong>Example:</strong> Pressing an elevator’s “3” button multiple times still just takes you to floor 3 once. In HTTP, <code>GET</code> and <code>DELETE</code> are idempotent, but <code>POST</code> usually isn’t.</p>
</li>
<li>
<p><strong>Monad</strong><br>
<em>A term in philosophy, functional programming, chemistry, and biology.</em><br>
In programming (especially Haskell), a monad is a design pattern for handling side effects. In philosophy, it means a basic, indivisible unit—like Leibniz’s “building block of reality.”<br>
I still love the name “Maybe Monad” although it was the headache-inducing end to my foray into functional programming.</p>
</li>
<li>
<p><strong>Shebang</strong><br>
<em>The <code>#!</code> at the top of Unix scripts.</em><br>
Signals to the OS which interpreter should run the script (e.g., <code>#!/bin/bash</code>). The Linux kernel handles this in <a href="https://github.com/torvalds/linux/blob/master/fs/binfmt_script.c"><code>fs/binfmt_script.c</code></a>.</p>
</li>
<li>
<p><strong>ACID</strong><br>
<em>(Atomicity, Consistency, Isolation, Durability)</em>—the four properties of reliable database transactions.<br>
Most good database should be ACID-compliant (there are exceptions like NoSQL).</p>
</li>
<li>
<p><strong>Caveman Debugging</strong>
The primitive but effective art of littering your code with print or echo statements.</p>
</li>
<li>
<p><strong>Bogosort</strong>
The infamous &ldquo;randomly shuffle until sorted&rdquo; algortihm—funny, real, and often cited as the worst possible sort. Caveman debugging might work, but caveman sorting definitely doesn&rsquo;t.</p>
<p>For comparison, see the Quick Sort algorithm in Hungarian folk dance form.
<br><br>
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
        <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/3San3uKKHgg?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
      </div>
</p>
</li>
<li>
<p><strong>LIFO/FIFO</strong><br>
“Last In, First Out” / “First In, First Out.”<br>
<em>Fundamental terms in data structures (stacks and queues). Also how sysadmins deal with tickets (ideally FIFO, but sometimes…)</em></p>
</li>
<li>
<p><strong>Immutability</strong>
The property that data cannot be changed after it is created. Instead of modifying existing values, programs produce new ones. This helps make code safer and more predictable—no &ldquo;spooky action at a distance&rdquo; from hidden changes.
<em>In Python, <code>tuple</code> and <code>str</code>ings are immutable. In functional languages, <em>all</em> data is usually immutable.</em></p>
</li>
<li>
<p><strong>Heuristic</strong>
A practical method or &ldquo;rule of thumb&rdquo; used to solve problems or make decisions quickly when perfect solutions are too costly or impossible. Heuristics don&rsquo;t guarantee the best answer, but they&rsquo;re often good enough.</p>
</li>
<li>
<p><strong>Cargo Cult Programming</strong>
Using code, patterns, or processes without understanding them, hoping they magically work. For a fascinating read, check out <a href="https://en.wikipedia.org/wiki/Cargo_cult">Cargo Cults</a> on Wikipedia.</p>
</li>
<li>
<p><strong>Munging</strong>
To modify data, often destructively. This often involves gnarly regex patterns.</p>
</li>
</ul>
<h2 id="a-little-philosophy-is-a-fine-thing">A Little Philosophy is a Fine Thing</h2>
<ul>
<li>
<p><strong>Solipsism</strong><br>
The philosophical idea that only one’s own mind is sure to exist. Everything outside your mind might be an illusion.<br>
<em>Geek twist: Useful shorthand when describing “works on my machine” syndrome.</em></p>
</li>
<li>
<p><strong>Tautology</strong><br>
A statement that is always true by virtue of its logical form, e.g., “It will rain, or it won’t.”<br>
<em>In programming, sometimes refers to redundant conditions (e.g., <code>if (x == x)</code>). Also an all-time favorite logic joke.</em></p>
</li>
<li>
<p><strong>Epistemology</strong><br>
The study of knowledge: how do we know what we know?<br>
<em>Useful for late-night code review debates about “best practices.”</em></p>
</li>
<li>
<p><strong>Axiom</strong><br>
A statement or proposition regarded as self-evidently true, forming the basis of a logical system or theory.<br>
<em>In mathematics and logic, axioms are the bedrock; everything else is built by deduction. Example: “Through any two points, there is exactly one straight line.”</em></p>
</li>
<li>
<p><strong>Sieve</strong><br>
A process or algorithm for filtering elements by successively removing items that do not meet certain criteria.<br>
<em>Famous in math as the Sieve of Eratosthenes, a classic way to find all primes up to a given number. Also used in spam filtering, data cleaning, and sorting tasks.</em></p>
</li>
<li>
<p><strong>A priori</strong><br>
Knowledge or reasoning that is independent of experience—something knowable before observing the world.<br>
<em>Opposite of “a posteriori,” which is based on experience. Example: “All bachelors are unmarried” is an a priori truth. In tech, an a priori assumption might be: ‘The network will always be available.’ (Famous last words…)</em></p>
</li>
<li>
<p><strong>Etymology</strong>
The study of the origin of words and how their meanings and forms change over time.
If you’ve ever spent an hour down a Wikipedia rabbit hole about where “debug” or “daemon” came from, congrats—you’re into etymology.</p>
</li>
</ul>
<hr>
<h2 id="naming-convention-terms">Naming Convention Terms</h2>
<ul>
<li>
<p><strong>CamelCase</strong><br>
Capitalizing each word and running them together, like <code>CamelCase</code> or <code>UpperCrust</code>.</p>
</li>
<li>
<p><strong>lowerCamelCase</strong> <em>(aka &ldquo;lowercapCamelCase&rdquo;, my preferred term!)</em><br>
Like <code>thisIsAnExample</code>—first letter is lowercase, rest are capitalized.<br>
<em>Examples: Hugo’s <code>--cleanDestinationDir</code>, JavaScript variables like <code>myVarName</code>.</em></p>
</li>
<li>
<p><strong>bIZARRO cAMEL cASE</strong>
For smart-alecks and iconoclasts</p>
</li>
<li>
<p><strong>snake_case</strong><br>
Words joined with underscores, like <code>this_is_snake_case</code>.<br>
<em>Common in Python variables, C constants: <code>MAX_BUFFER_SIZE</code>.</em></p>
</li>
<li>
<p><strong>kebab-case</strong><br>
Words separated by hyphens, like <code>kebab-case</code>.<br>
<em>Used in URLs, filenames, and CSS class names.</em></p>
</li>
<li>
<p><strong>Hungarian notation</strong><br>
Prefix variable names with type hints, e.g., <code>pszName</code> for “pointer to zero-terminated string.”<br>
<em>Example: <code>dwCount</code> (DWORD count), <code>strUser</code> (string), <code>bEnabled</code> (boolean).</em><br>
Not recommended these days, but once a Microsoft standard.</p>
</li>
<li>
<p><strong>Reverse Hungarian notation</strong><br>
Type information goes <em>after</em> the base name, e.g., <code>nameStr</code> or <code>countInt</code>.<br>
<em>Used by Microsoft and in Visual Basic.</em></p>
</li>
<li>
<p><strong>StudlyCaps</strong><br>
Randomly mixing upper and lowercase: <code>sTuDlYcApS</code>.<br>
Almost always used ironically.</p>
</li>
<li>
<p><strong>Reverse DNS notation</strong><br>
Used by Apple and Java to avoid naming conflicts, like <code>com.apple.OpenDirectory</code> or <code>org.apache.commons.io</code>.<br>
<em>Your domain backwards, then your product, then the class or extension.</em></p>
</li>
<li>
<p><strong>DotCase</strong><br>
Using periods: <code>dot.case.example</code>.<br>
<em>Used in JSON keys, configuration files.</em></p>
</li>
<li>
<p><strong>COBOL-CASE</strong><br>
ALL-UPPERCASE-WITH-HYPHENS.<br>
<em>A throwback to the COBOL programming language!</em></p>
</li>
</ul>
<hr>
<h2 id="super-meta">Super Meta</h2>
<ul>
<li>
<p><strong>Metasyntactic variables</strong><br>
<code>foo</code>, <code>bar</code>, <code>baz</code>, and the cast of cryptography (Alice, Bob, Eve, Mallory…).<br>
If you see Alice sending a message to Bob, you’re probably reading crypto docs.</p>
</li>
<li>
<p><strong>Quine</strong></p>
<p>A program that outputs its own source code.<br>
<br>
<em>Python example:</em></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">source</span> <span class="o">=</span> <span class="s2">&#34;source = </span><span class="si">{!r}</span><span class="se">\n</span><span class="s2">print(source.format(source))&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">source</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">source</span><span class="p">))</span>
</span></span></code></pre></div><p><em>Bash example:</em></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">s</span><span class="o">=</span><span class="s1">&#39;s=%q; printf &#34;$s&#34; &#34;$s&#34;&#39;</span><span class="p">;</span> <span class="nb">printf</span> <span class="s2">&#34;</span><span class="nv">$s</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$s</span><span class="s2">&#34;</span>
</span></span></code></pre></div><p>Give em&rsquo; a try!</p>
</li>
<li>
<p><strong>Recursion</strong><br>
When a function calls itself as part of its execution.<br>
<em>Classic joke: “See recursion.” (Or, “To understand recursion, you must first understand recursion.”)</em></p>
<p>Here&rsquo;s a fun example in bash that outputs 5 4 3 2 1 Blastoff!</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">  <span class="k">function</span> count_down<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">$1</span><span class="s2">&#34;</span> -le <span class="m">0</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;Blastoff!&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    count_down <span class="k">$((</span> <span class="nv">$1</span> <span class="o">-</span> <span class="m">1</span> <span class="k">))</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</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">count_down <span class="m">5</span>
</span></span></code></pre></div></li>
<li>
<p><strong>Steganography</strong></p>
<p>The art of hiding data, usually a file inside of another file. Here&rsquo;s an ancient, simple example that is trivially easy to detect but fascinating nonetheless.</p>
<ul>
<li>On Linux:
<code>cat ninja.jpg secret.txt &gt; stego.jpg</code></li>
<li>On Windows:
<code>copy /b ninja.jpg + secret.txt stego.jpg</code></li>
</ul>
<p>You can easily see the txt data with a hex editor or the <code>strings</code> command.</p>
</li>
</ul>
<p align="center">
  <img src="stego.jpg" alt="A ninja with a secret" width="256">
  A ninja with a secret
</p>
<ul>
<li>
<p><strong>Self-Modifying Bash Script: a Run Counter</strong></p>
<p>Sometimes bash just writes itself. In this simple example we update the # RUN_COUNT comment each run and report on the value.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">  <span class="c1">#!/bin/bash</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># RUN_COUNT: 0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">count</span><span class="o">=</span><span class="k">$(</span>grep <span class="s1">&#39;^# RUN_COUNT:&#39;</span> <span class="s2">&#34;</span><span class="nv">$0</span><span class="s2">&#34;</span> <span class="p">|</span> awk <span class="s1">&#39;{print $3}&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">new_count</span><span class="o">=</span><span class="k">$((</span>count <span class="o">+</span> <span class="m">1</span><span class="k">))</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;You&#39;ve run this script </span><span class="nv">$new_count</span><span class="s2"> times!&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Update the count in the script</span>
</span></span><span class="line"><span class="cl">sed -i <span class="s2">&#34;s/^# RUN_COUNT: .*/# RUN_COUNT: </span><span class="nv">$new_count</span><span class="s2">/&#34;</span> <span class="s2">&#34;</span><span class="nv">$0</span><span class="s2">&#34;</span>
</span></span></code></pre></div><p>Note: on macOS and BSD, you will need to use this sed syntax:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sed -i <span class="s1">&#39;&#39;</span> <span class="s2">&#34;s/^# RUN_COUNT: .*/# RUN_COUNT: </span><span class="nv">$new_count</span><span class="s2">/&#34;</span> <span class="s2">&#34;</span><span class="nv">$0</span><span class="s2">&#34;</span>
</span></span></code></pre></div><p>Self-modifying or polymorphic code is rare in the real world, but seeing a bash script mutate itself is a mind-bender. It’s a great way to demonstrate code as data—and vice versa! This is generally considered a bad practice since the script may very well eat itself and git will always see it as dirty—and it is.</p>
</li>
</ul>
<h2 id="goofy-but-useful-concepts">Goofy but Useful Concepts</h2>
<ul>
<li>
<p><strong>TEOTWAWKI</strong><br>
Acronym for “The End Of The World As We Know It.”<br>
<em>Popularized in survivalist, sci-fi, and prepper circles, as well as internet memes. Handy for describing major changes in tech, software, or society (“Is this a TEOTWAWKI moment?”)</em></p>
</li>
<li>
<p><strong>NIMBY</strong><br>
“Not In My Back Yard.” Resistance to change that affects one’s own domain, while accepting it elsewhere.</p>
</li>
<li>
<p><strong>Nattering Nabobs of Negativity</strong>
William Safire’s phrase for relentless pessimists and critics—every tech team has one. From a speech during the lead-up to the 1970 midterm elections. The speech was part of his campaign rhetoric criticizing the media for their coverage of the Nixon administration.</p>
</li>
<li>
<p><strong>Known Knowns, Known Unknowns and Unknown Unknowns</strong></p>
<p>To condense the bizarre but logically accurate news briefing by then U.S. Secretary of Defense, Donald Rumsfeld:
<br><br>
<em>“Reports that say that something hasn’t happened are always interesting to me, because as we know, there are known knowns; there are things we know we know.
We also know there are known unknowns; that is to say we know there are some things we do not know.
But there are also unknown unknowns—the ones we don’t know we don’t know.
And if one looks throughout the history of our country and other free countries, it is the latter category that tends to be the difficult ones.”</em></p>
</li>
<li>
<p><strong>Ersatz</strong><br>
An imitation or substitute, usually inferior.<br>
<em>“Ersatz coffee” (WWII); in tech, can refer to quick hacks or shims that just barely work.</em></p>
</li>
<li>
<p><strong>TIMTOWTDI</strong>
“There Is More Than One Way To Do It”—Perl’s unofficial motto.</p>
</li>
</ul>
<h2 id="other-fun-words">Other Fun Words</h2>
<ul>
<li>
<p><strong>Shibboleth</strong><br>
A custom, often obscure way of saying or doing something that signals you are part of the tribe.<br>
<em>Origin: Judges 12:5–6 — the Gileadites identified Ephraimite fugitives by their inability to pronounce “shibboleth.”</em>
This is the age old challenge of talking the talk and walking the walk.</p>
</li>
<li>
<p><strong>Ineffable</strong>
Too great or extreme to be expressed or described in words. Beyond language. Beyond form.</p>
</li>
<li>
<p><strong>Churlish</strong>
Rude, surly, or lacking politeness and good manners. It&rsquo;s often used to describe someone who is grumpy, ungracious, or unnecessarily difficult.</p>
</li>
<li>
<p><strong>Arabesque</strong>
Generally refers to something intricate, ornamental, or flowing. It can refer to floral motifs and geometric shapes in Islamic and Byzantine ornamentation. In Ballet and music, in Literature and Speech. It has the sense of something that is usually elegant, intricate and beautifully complex.</p>
</li>
<li>
<p><strong>Hinky</strong>
Suspicious, strange or something isn&rsquo;t quite right. Has origins as law enforcement Slang. &ldquo;Something about this guy seems hinky&rdquo; meant there was something suspicious or out of the ordinary.</p>
</li>
<li>
<p><strong>Hechicero, Hechicera</strong>
Spanish for sorcerer, wizard or magician. from the verb hechizar meaning to bewitch or enchant. &ldquo;Un verdadero hechicero&rdquo; a true sorcerer.</p>
</li>
<li>
<p><strong>Isogloss</strong>
In linguistics, a geographical boundary marking where certain linguistic features occur. This will be relatable if you have ever worked in a large, older company where teams are fiercely devoted to wildly different languages, tools, or design patterns.  In this case the isogloss may very well be the elevator or break room</p>
</li>
<li>
<p><strong>Ikigai</strong>
Japanese for purpose-driven living, which breaks down into:</p>
<ul>
<li>what you love</li>
<li>What you&rsquo;re good at</li>
<li>What the world needs</li>
<li>What you can be paid for</li>
</ul>
</li>
<li>
<p><strong>Shoshin (初心)</strong>
The Zen concept of approaching learning with an open, beginner&rsquo;s mind.</p>
<h2 id="conclusion">Conclusion</h2>
</li>
</ul>
<p><a href="https://tvtropes.org/pmwiki/pmwiki.php/Main/PerfectlyCromulentWord">TVTropes</a> definition is entertaining as always!</p>
<p style="text-align:left;">
  <a href="/tags/cromulent" class="button">🧾 View Cromulent Words Series</a>
</p>
<p>Have a cromulent word of your own? Email me:
<a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Qrgen</title>
      <link>https://adminjitsu.com/posts/qrgen/</link>
      <pubDate>Sat, 26 Jul 2025 23:03:42 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/qrgen/</guid>
      <description>Need a QR code for a WiFi password, URL, or quick snippet? qrgen.py is a friendly Python script for generating QR codes as images or ASCII, from any platform with Python.</description>
      <content:encoded><![CDATA[<h2 id="what-is-qrgenpy">What is qrgen.py?</h2>
<p><strong>qrgen.py</strong> is a tiny but flexible Python script that lets you generate QR codes right from your terminal. No web apps, no clipboard shenanigans—just run, encode, and use. Pipe in any text, or save as an image (<code>.png</code>,<code>.jpg</code>,<code>.gif</code>—based on the filename you give). Works on Linux, Mac, and Windows (with Python 3 and pip).</p>
<h2 id="why-is-this-cool">Why is this cool?</h2>
<p>QR codes are everywhere these days and there are all sorts of websites and apps to create them but it&rsquo;s not a casually trivial thing. It requires steps that take time and effort. This script makes it ridiculously easy to create your own scannable QR codes, openening up all sorts of creative possibilities:</p>
<ul>
<li>Share WiFi passwords with houseguests in a flash</li>
<li>Put URLs, emails, or TOTP keys on your phone with zero friction</li>
<li>Encode one-time codes, contacts, server hostnames, whatever</li>
<li>Use ASCII QR for terminal-based setup or fun demos</li>
<li>Easily create codes for printable signs, stickers or labels.</li>
</ul>
<p align="center">
  <img src="rickroll.jpg" alt="an awesome youtube clip" width="220">
  Scan me for another exciting demo!
</p>
<h2 id="installation">Installation</h2>
<p>Download or clone the project from GitHub</p>
<p class="github-btn">
  <a href="https://github.com/forfaxx/qrgen" target="_blank">
    🔗 View qrgen on GitHub
  </a>
</p>
<h2 id="quickstart">Quickstart</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Clone and enter directory</span>
</span></span><span class="line"><span class="cl">git clone https://github.com/forfaxx/qrgen.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> qrgen
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create a virtual environment (recommended)</span>
</span></span><span class="line"><span class="cl">python3 -m venv venv
</span></span><span class="line"><span class="cl"><span class="nb">source</span> venv/bin/activate
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Install dependencies</span>
</span></span><span class="line"><span class="cl">pip install -r requirements.txt
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run the script!</span>
</span></span><span class="line"><span class="cl">python qrgen.py <span class="s2">&#34;https://adminjitsu.com/qrgen&#34;</span>
</span></span></code></pre></div><h2 id="usage-examples">Usage Examples</h2>
<p>Make sure you have followed the steps above and have activated the venv for the script.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Encode a string, open the QR image viewer</span>
</span></span><span class="line"><span class="cl">./qrgen.py <span class="s2">&#34;https://adminjitsu.com&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Save as PNG</span>
</span></span><span class="line"><span class="cl">./qrgen.py <span class="s2">&#34;https://adminjitsu.com&#34;</span> --output adminjitsu.png
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Save as high-quality JPEG</span>
</span></span><span class="line"><span class="cl">./qrgen.py <span class="s2">&#34;https://adminjitsu.com&#34;</span> --output ../adminjitsu.jpg
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Save to a directory (saves as output.png there)</span>
</span></span><span class="line"><span class="cl">./qrgen.py <span class="s2">&#34;https://adminjitsu.com&#34;</span> --output ~/Pictures/qr/
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show as ASCII in terminal</span>
</span></span><span class="line"><span class="cl">./qrgen.py --ascii <span class="s2">&#34;shell magic&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show version/help</span>
</span></span><span class="line"><span class="cl">./qrgen.py --version
</span></span><span class="line"><span class="cl">./qrgen.py --help
</span></span></code></pre></div><ul>
<li><span class="tag green">Tip</span> If you want to make the output png larger in size, simply increase the value of box_size in the generate_qr function.</li>
</ul>
<h3 id="saving-and-output-location">Saving and Output Location</h3>
<p>By default, using &ndash;output saves the file to your current directory as the format you specify (.png, .jpg, or .gif).</p>
<p>If you provide a directory for &ndash;output, qrgen.py will save the file as output.png in that directory.</p>
<p>Examples:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Save as a PNG file in the current directory</span>
</span></span><span class="line"><span class="cl">./qrgen.py <span class="s2">&#34;root password: pencil&#34;</span> --output root-password.png
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Save as a JPEG file in Pictures</span>
</span></span><span class="line"><span class="cl">./qrgen.py <span class="s2">&#34;http://example.com&#34;</span> --output ~/Pictures/qr/wifi.jpg
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Save as PNG in the specified directory</span>
</span></span><span class="line"><span class="cl">./qrgen.py <span class="s2">&#34;note to self&#34;</span> --output ~/qr-codes/
</span></span></code></pre></div><p>Tip:
To always save to a specific place, give an explicit path, e.g.
<code>--output ~/Pictures/wifi-qr.png</code></p>
<h2 id="managing-python-virtual-environments">Managing Python virtual environments</h2>
<p>On the official README I detail how to use the venv-launch.sh to create simple wrappers for your python tools. This is easily as cool as qrgen itself and deserves a full post. However, you can make it even easier to run this script if you take a moment to set it up. On my system I have a bootstrap.sh script that does the following for each python tool folder it finds in my tools folder. I&rsquo;ll include this here for the curious who might want to automate this process as well.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># --- 4. link python tools from tools/*/script.py ---------------------------</span>
</span></span><span class="line"><span class="cl"><span class="nb">declare</span> -A <span class="nv">GENERATED_WRAPPERS</span><span class="o">=()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> tool in <span class="s2">&#34;</span><span class="si">${</span><span class="nv">DOTROOT</span><span class="si">}</span><span class="s2">/tools/&#34;</span>*/<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="o">[[</span> -d <span class="s2">&#34;</span><span class="nv">$tool</span><span class="s2">&#34;</span> <span class="o">]]</span> <span class="o">||</span> <span class="k">continue</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nv">tool_name</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>basename <span class="s2">&#34;</span><span class="nv">$tool</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nv">main_script</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">tool</span><span class="si">}</span><span class="s2">/</span><span class="si">${</span><span class="nv">tool_name</span><span class="si">}</span><span class="s2">.py&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nv">wrapper</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">BIN_DEST</span><span class="si">}</span><span class="s2">/</span><span class="si">${</span><span class="nv">tool_name</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> -f <span class="s2">&#34;</span><span class="nv">$main_script</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Extract header comment block from main script (skip shebang, keep contiguous comments)</span>
</span></span><span class="line"><span class="cl">  <span class="nv">head_comment</span><span class="o">=</span><span class="k">$(</span>awk <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">    NR == 1 &amp;&amp; /^#!/ { next }     # skip shebang
</span></span></span><span class="line"><span class="cl"><span class="s1">    /^#/ { print; next }          # include comment lines
</span></span></span><span class="line"><span class="cl"><span class="s1">    { exit }                      # stop at first non-comment
</span></span></span><span class="line"><span class="cl"><span class="s1">  &#39;</span> <span class="s2">&#34;</span><span class="nv">$main_script</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Fallback comment if nothing found</span>
</span></span><span class="line"><span class="cl">  <span class="o">[[</span> -z <span class="s2">&#34;</span><span class="nv">$head_comment</span><span class="s2">&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nv">head_comment</span><span class="o">=</span><span class="s2">&#34;# </span><span class="si">${</span><span class="nv">tool_name</span><span class="si">}</span><span class="s2"> — Python CLI tool&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Generate wrapper with docblock and marker</span>
</span></span><span class="line"><span class="cl">  cat &gt; <span class="s2">&#34;</span><span class="nv">$wrapper</span><span class="s2">&#34;</span> <span class="s">&lt;&lt;EOF
</span></span></span><span class="line"><span class="cl"><span class="s">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="s"># dotfiles/bootstrap.sh generated wrapper
</span></span></span><span class="line"><span class="cl"><span class="s">$head_comment
</span></span></span><span class="line"><span class="cl"><span class="s"># wrapper: $main_script
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">exec &#34;${DOTROOT}/lib/venv-launch.sh&#34; &#34;$main_script&#34; &#34;\$@&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  act <span class="s2">&#34;chmod +x &#39;</span><span class="nv">$wrapper</span><span class="s2">&#39;&#34;</span>
</span></span><span class="line"><span class="cl">  GENERATED_WRAPPERS<span class="o">[</span><span class="s2">&#34;</span><span class="nv">$wrapper</span><span class="s2">&#34;</span><span class="o">]=</span><span class="m">1</span>
</span></span><span class="line"><span class="cl">  log <span class="s2">&#34;Linked tool launcher: </span><span class="nv">$tool_name</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    log <span class="s2">&#34;⚠️  No main script found for tool: </span><span class="nv">$tool_name</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span></code></pre></div><p>Luckily the manual steps are not that difficult. You just need to put venv-launch.sh somewhere in your PATH (e.g., ~/bin) and then you can create simple wrappers for all of your Python tools, like qrgen. Simply do the following:</p>
<ol>
<li>Put venv-launch.sh somewhere in your PATH (e.g., ~/bin):</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cp venv-launch.sh ~/bin/
</span></span><span class="line"><span class="cl">chmod +x ~/bin/venv-launch.sh
</span></span></code></pre></div><ol start="2">
<li>Create a wrapper script for qrgen: In ~/bin (or another place in your PATH), add this:</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="c1"># qrgen global launcher</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">SCRIPT_DIR</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/tools/qrgen&#34;</span>   <span class="c1"># &lt;--- change this to wherever you cloned the repo</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">exec</span> ~/bin/venv-launch.sh <span class="s2">&#34;</span><span class="nv">$SCRIPT_DIR</span><span class="s2">/qrgen.py&#34;</span> <span class="s2">&#34;</span><span class="nv">$@</span><span class="s2">&#34;</span>
</span></span></code></pre></div><ol start="3">
<li>Make the launcher executable:</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">chmod +x ~/bin/qrgen
</span></span></code></pre></div><ol start="4">
<li>Now you can run:</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">qrgen <span class="s2">&#34;https://adminjitsu.com&#34;</span> 
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;my wifi password&#34;</span> <span class="p">|</span> qrgen --ascii
</span></span></code></pre></div><h2 id="venv-launchsh"><code>venv-launch.sh</code></h2>
<p>The magic behind this is the venv-launch.sh script. On my system, I have dozens of python tools—managing them all would be enough of an annoyance that I would likely rarely use them otherwise. With venv-launch, you can write a simple wrapper for your tools that automates the fiddly bits.</p>
<p>This is what the script looks like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="c1"># venv-launch.sh — Run a Python script inside its local virtualenv, if one exists.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Usage:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   venv-launch.sh /absolute/path/to/script.py [args...]</span>
</span></span><span class="line"><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="cl"><span class="c1"># If a `venv/` directory is found next to the script, its Python interpreter</span>
</span></span><span class="line"><span class="cl"><span class="c1"># is used. Otherwise, falls back to system Python (python3).</span>
</span></span><span class="line"><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="cl"><span class="c1"># This is used by launcher scripts auto-generated by bootstrap.sh. and allows for simple stub scripts to create a named command in ~/bin</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><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="k">if</span> <span class="o">[[</span> <span class="nv">$#</span> -lt <span class="m">1</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Usage: </span><span class="nv">$0</span><span class="s2"> path/to/script.py [args...]&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">  <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">SCRIPT_PATH</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>realpath <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">shift</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">SCRIPT_DIR</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>dirname <span class="s2">&#34;</span><span class="nv">$SCRIPT_PATH</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">VENV_PYTHON</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$SCRIPT_DIR</span><span class="s2">/venv/bin/python&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> -x <span class="s2">&#34;</span><span class="nv">$VENV_PYTHON</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">exec</span> <span class="s2">&#34;</span><span class="nv">$VENV_PYTHON</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$SCRIPT_PATH</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$@</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;⚠️  No venv found next to script. Falling back to system Python.&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">  <span class="nb">exec</span> python3 <span class="s2">&#34;</span><span class="nv">$SCRIPT_PATH</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$@</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span></code></pre></div><p>Once that script is in your PATH, you can create a wrapper like this (also in your PATH)</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="c1"># dotfiles/bootstrap.sh generated wrapper</span>
</span></span><span class="line"><span class="cl"><span class="c1"># qrgen — Python CLI tool</span>
</span></span><span class="line"><span class="cl"><span class="c1"># wrapper: /home/grumble/codelab/dotfiles/tools/qrgen//qrgen.py</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">exec</span> <span class="s2">&#34;/home/grumble/bin/venv-launch.sh&#34;</span> <span class="s2">&#34;/home/grumble/codelab/tools/qrgen//qrgen.py&#34;</span> <span class="s2">&#34;</span><span class="nv">$@</span><span class="s2">&#34;</span>
</span></span></code></pre></div><p>After that is in place, you can simply run qrgen and it will handle activating and deactivating the venv automatically</p>
<h2 id="conclusion">Conclusion</h2>
<p>That’s it—a dead-simple way to generate QR codes from anywhere, anytime, no questions asked. Perfect for WiFi, links, OTPs, passwords, notes, and anything else you don’t want to type out twice. I hope you find this as handy and delightful as I do!</p>
<p>Have feedback, feature requests, or a cool use case? PRs welcome!
or Email me: <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Espanso: Real-world Macros for Everyone</title>
      <link>https://adminjitsu.com/posts/espanso-and-friends/</link>
      <pubDate>Sat, 26 Jul 2025 19:35:01 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/espanso-and-friends/</guid>
      <description>Sick of searching for actual good text expansion examples? Here’s a field-tested set of Espanso macros for everyday admin, coding, and docs. Includes setup tips, caveats, and how to find (or invent) your own best shortcuts.</description>
      <content:encoded><![CDATA[<p>In my work, I routinely need access to fiddly commands, consistent dividers and common things like date and timestamps. This is where a program like Espanso really shines. While not as powerful as <a href="https://textexpander.com/">TextExpander</a> for Mac, Espanso is a good Linux and Windows solution that can still do quite a lot. The idea behind these programs is that they run in the background listening for you to type a macro trigger at which point they replace the trigger with whatever you have defined in your match rules.</p>
<p>For example, I can type something like dddate and it will expand to the current date 07/26/2025. Cool. One problem I have found is that it is hard to find good macro examples online. The best macros come from experience&ndash;when you identify things that are difficult to remember or do consistently, automate them with a new rule. However, it helps to have a few to get you started and show what is possible.</p>
<h2 id="the-short-version-whats-in-this-post">The Short Version: What&rsquo;s in This Post?</h2>
<ul>
<li>A real set of Espanso macros I actually use</li>
<li>What each one does and when it&rsquo;s a lifesaver</li>
<li>Notes on setup and quirks, especially on Windows/WSL</li>
<li>A few ways to invent your own macros.</li>
</ul>
<hr>
<h2 id="setting-up-espanso">Setting up Espanso</h2>
<p>Install Espanso (see <a href="https://espanso.org/docs/">official docs</a>), then put your macros in the config file—usually:</p>
<ul>
<li>Linux/macOS: <code>~/.config/espanso/match/default.yml</code></li>
<li>Windows: <code>%APPDATA%\espanso\match\default.yml</code></li>
</ul>
<p>After edits, run <code>espanso restart</code> to reload or reload from the system tray icon on Windows.</p>
<p><span class="tag yellow">👉 Heads up</span> YAML is picky—use 2 spaces for indentation, never tabs, and watch your quotes.</p>
<h2 id="my-macros">My macros</h2>
<p>Minus a few that I am leaving out like my home address, these are my current Espanso macros. If you are on Mac, <a href="https://textexpander.com/">TextExpander</a> makes this all easy with a nice GUI and cloud macros that you can use by installing the app and logging in. Espanso is simpler and requires that you add your macros in YAML format to default.yml</p>
<p>On my system I have a few handy macros defined.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">matches</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># WAN/Public IP - Uses WSL curl, shows your external IP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">trigger</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;:wanip&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">replace</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;{{wanip}}&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">vars</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">wanip</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">shell</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">params</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">cmd</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;wsl curl -s ifconfig.me&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># LAN/Internal IP - Uses WSL and grabs your main IP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">trigger</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;:lanip&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">replace</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;{{lanip}}&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">vars</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">lanip</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">shell</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">params</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">cmd</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;wsl bash -c &#34;ip addr show eth0 | grep \&#34;inet \&#34; | awk \&#34;{print \$2}\&#34; | cut -d/ -f1 | xargs&#34;&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">trigger</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;:forglob&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">replace</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      for file in {{glob}}; do
</span></span></span><span class="line"><span class="cl"><span class="sd">        [[ -f &#34;$file&#34; ]] || continue
</span></span></span><span class="line"><span class="cl"><span class="sd">        # do something with &#34;$file&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">        echo &#34;$file&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">      done</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c">### ─────────────────────────────</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c">### HORIZONTAL SEPARATORS</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c">### ─────────────────────────────</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">trigger</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;:#--&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">replace</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;# ────────────────────────────────────────────────────────────────&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">trigger</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;:---&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">replace</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;---------------------------------------------&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">trigger</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;:==&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">replace</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;========================================&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">trigger</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;:fence&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">replace</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      # ─────────────────────────────────────────────────────────────
</span></span></span><span class="line"><span class="cl"><span class="sd">      #
</span></span></span><span class="line"><span class="cl"><span class="sd">      # ─────────────────────────────────────────────────────────────</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">trigger</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;:hashfence&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">replace</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      ##############################################################
</span></span></span><span class="line"><span class="cl"><span class="sd">      #
</span></span></span><span class="line"><span class="cl"><span class="sd">      ##############################################################</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">trigger</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;:hdr&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">label</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Insert Bash-style header with cursor at name&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">replace</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      # ─────────────────────────────────────────────
</span></span></span><span class="line"><span class="cl"><span class="sd">      # [[CURSOR]]
</span></span></span><span class="line"><span class="cl"><span class="sd">      # ─────────────────────────────────────────────</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c">### ─────────────────────────────</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c">### AUTHOR LINE / BOILERPLATE</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c">### ─────────────────────────────</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">trigger</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;:author&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">replace</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;# Author: forfaxx {{date:YYYY}}&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c">### ─────────────────────────────</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c">### DATE &amp; TIME MACROS</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c">### ─────────────────────────────</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">trigger</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;dddate&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">replace</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;{{mydate}}&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">vars</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mydate</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">date</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">params</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">format</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;%m-%d-%Y&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">trigger</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;tttime&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">replace</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;{{mytime}}&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">vars</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mytime</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">date</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">params</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">format</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;%H:%M:%S&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">trigger</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;:now&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">replace</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;{{timestamp}}&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">vars</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">timestamp</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">date</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">params</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">format</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;%Y-%m-%d %H:%M:%S&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">trigger</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;bkpfile&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">replace</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;backup-{{bkptime}}.tar.gz&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">vars</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">bkptime</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">date</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">params</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">format</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;%Y-%m-%d-%H-%M-%S&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">trigger</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;:fortune&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">replace</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;{{fortune}}&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">vars</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">fortune</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">shell</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">params</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">cmd</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;wsl fortune&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c">### ----------------------------</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c">###  BOILERPLATE</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c">### ----------------------------</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">trigger</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;:lorem&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">replace</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">trigger</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;:mit&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">replace</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      # MIT License
</span></span></span><span class="line"><span class="cl"><span class="sd">      #
</span></span></span><span class="line"><span class="cl"><span class="sd">      # Copyright (c) Kevin Joiner {{mydate}}
</span></span></span><span class="line"><span class="cl"><span class="sd">      #
</span></span></span><span class="line"><span class="cl"><span class="sd">      # Permission is hereby granted, free of charge, to any person obtaining a copy
</span></span></span><span class="line"><span class="cl"><span class="sd">      # of this software and associated documentation files (the &#34;Software&#34;), to deal
</span></span></span><span class="line"><span class="cl"><span class="sd">      # in the Software without restriction, including without limitation the rights
</span></span></span><span class="line"><span class="cl"><span class="sd">      # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
</span></span></span><span class="line"><span class="cl"><span class="sd">      # copies of the Software, and to permit persons to whom the Software is
</span></span></span><span class="line"><span class="cl"><span class="sd">      # furnished to do so, subject to the following conditions:
</span></span></span><span class="line"><span class="cl"><span class="sd">      #
</span></span></span><span class="line"><span class="cl"><span class="sd">      # The above copyright notice and this permission notice shall be included in all
</span></span></span><span class="line"><span class="cl"><span class="sd">      # copies or substantial portions of the Software.
</span></span></span><span class="line"><span class="cl"><span class="sd">      #
</span></span></span><span class="line"><span class="cl"><span class="sd">      # THE SOFTWARE IS PROVIDED &#34;AS IS&#34;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
</span></span></span><span class="line"><span class="cl"><span class="sd">      # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
</span></span></span><span class="line"><span class="cl"><span class="sd">      # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
</span></span></span><span class="line"><span class="cl"><span class="sd">      # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
</span></span></span><span class="line"><span class="cl"><span class="sd">      # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
</span></span></span><span class="line"><span class="cl"><span class="sd">      # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
</span></span></span><span class="line"><span class="cl"><span class="sd">      # SOFTWARE.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">vars</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mydate</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">date</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">params</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">format</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;%Y&#34;</span><span class="w">
</span></span></span></code></pre></div><h2 id="usage-tips-and-troubleshooting">Usage Tips and Troubleshooting</h2>
<p>After reloading, you should be able to trigger any of the macros by typing the trigger (the string inside the double quotes) and it will expand. If you have any trouble with a macro, check for YAML errors or conflicting triggers.</p>
<p>The Espanso logs are located at:</p>
<ul>
<li>Linux/macOS: <code>~/.config/espanso/logs/espanso.log</code></li>
<li>Windows: <code>%APPDATA%\espanso\logs\espanso.log</code></li>
</ul>
<p>%APPDATA% typically resolves to something memorable like <code>C:\Users\&lt;your username&gt;\AppData\Roaming\espanso\Logs\espanso.log</code></p>
<p>If all else fails, try restarting Espanso completely or testing in a different editor, like <a href="https://code.visualstudio.com/">Visual Studio Code</a>  or <a href="https://notepad-plus-plus.org/downloads/">Notepad++</a>.</p>
<h2 id="pain-points">Pain Points</h2>
<p>As stated previously, YAML is very picky about indentation and will happily refuse to work if you are even <strong>one space</strong> or an errant <strong>tab</strong> off. A good editor like the amazing <a href="https://code.visualstudio.com/">VSCode</a> is invaluable here since it makes spacing and alignment easy to manage and will report if you have a syntax error.</p>
<p>One compelling feature in Espanso is the ability to position the cursor to a specific place in the macro with <code>[[CURSOR]]</code>. In my experience this is difficult to get working on Windows but works reliably on Linux. Your mileage may vary.</p>
<p>The other potentially powerful feature that is difficult on Windows is calling external programs from your macro. This is tantalizing if you run Linux under Windows Subsystem for Linux under Windows since you can run Linux shell commands by using wsl.exe like so:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">  </span><span class="c"># WAN/Public IP - Uses WSL curl, shows your external IP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">trigger</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;:wanip&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">replace</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;{{wanip}}&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">vars</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">wanip</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">shell</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">params</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">cmd</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;wsl curl -s ifconfig.me&#34;</span><span class="w">
</span></span></span></code></pre></div><p>This works great on my setup. Much more difficult to execute are commands that use pipes for things like grep or awk with complex quoting. You can make it work but heed my warning, that way lies MADNESS.</p>
<p>Aside from these and a few other limitations, Espanso is a very useful tool to have in your collection. While it doesn&rsquo;t (yet) support dropdowns, fill-ins or advanced logic like <a href="https://textexpander.com/">TextExpander</a> it is lightweight and works quite well despite those limitations.</p>
<h2 id="conclusion">Conclusion</h2>
<p>There you have it. My humble espanso macros that along with my shell aliases, make it easy to remember and be consistent in my work. Using my hard-won experience, you should be able to create your own macros for things like boilerplate you are sick of typing, nicely worded explanations for emails, commands and even scripts.</p>
<p>Have an awesome macro or idea for one? I&rsquo;d love to hear about it! <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
<p>May your macros always expand!</p>
]]></content:encoded>
    </item>
    <item>
      <title>Fortune Favors the Sysadmin</title>
      <link>https://adminjitsu.com/posts/fortune-favors-the-sysadmin/</link>
      <pubDate>Sat, 26 Jul 2025 10:31:47 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/fortune-favors-the-sysadmin/</guid>
      <description>A deep dive into the Unix fortune program, how the `strfile` tool works, and the Makefile I wrote to manage my collection of custom fortunes.</description>
      <content:encoded><![CDATA[<p class="github-btn">
  <a href="https://github.com/forfaxx/fortune" target="_blank">
    🔗 View fortune-files on GitHub
  </a>
</p>
<h2 id="why-fortune-is-cool">Why Fortune is cool</h2>
<p>I once visited the famous punk club, CBGB in New York and was amazed by the layers of stickers and graffiti that covered every available surface. It was a tangible sign of how many amazing shows had been performed there over the decades. Everywhere you looked there was something new, overlapping in layers and scattered with graffiti in everything from pen to pocketknife to lipstick. Since 1979, <code>fortune</code> has been like the walls of that club. Unknown thousands have contributed to it over the years. Now fortune, aside from supplying fun little quotes, captures something of the rich hitorical sweep and culture of Unix (yes this includes Unix and Unix-like systems such as Linux, FreeBSD, macOS etc.)</p>
<p>a random Unix fortune.</p>
<blockquote>
<p>UNIX was half a billion (500000000) seconds old on
Tue Nov  5 00:53:20 1985 GMT (measuring since the time(2) epoch).
&ndash; Andy Tanenbaum</p></blockquote>
<p>run it again and get a different fortune</p>
<blockquote>
<p>The famous politician was trying to save both his faces</p></blockquote>
<p>and again</p>
<blockquote>
<p>&ldquo;Life, loathe it or ignore it, you can&rsquo;t like it.&rdquo;
&ndash; Marvin, &ldquo;Hitchhiker&rsquo;s Guide to the Galaxy&rdquo;</p></blockquote>
<p>That&rsquo;s the beauty of this simple, delightful program.</p>
<h2 id="what-is-fortune">What is Fortune?</h2>
<p><code>fortune</code> is one of the simplest — and oldest — Unix programs. It works like this:</p>
<ul>
<li>A <strong>text file</strong> contains fortunes (quotes, jokes, snippets), each separated by a single <code>%</code> on its own line.</li>
<li>The <code>strfile</code> program reads that file and creates a <strong>binary <code>.dat</code> index</strong> — this makes random lookups fast and efficient.</li>
<li>When you run <code>fortune</code>, it just picks an entry from that index and displays it.</li>
</ul>
<p>That’s it — no AI, no databases, just plain text and a tiny bit of Unix cleverness that’s survived for nearly 50 years.</p>
<p>There are options and some tricks for creating your own, but at its essence, <code>fortune</code> is a perfect example of the Unix philosphy that programs should do one well and be able to chain together with other programs. That makes fortune, not just a quote generator, but a source of random-ish text that you can use in all sorts of creative ways.</p>
<h2 id="tour-of-my-collection">Tour of my collection</h2>
<p>I have compiled a few collections that I really enjoy:</p>
<ul>
<li><code>rules</code> - The Ferengi Rules of Acquisition from Star Trek Deep Space 9</li>
<li><code>deep-thoughts</code> - Deep Thoughts by Jack Handey</li>
<li><code>rickmorty</code> - sometimes you need a Rick Sanchez quote</li>
<li><code>oblique</code> - Adaptation of Brian Eno&rsquo;s Oblique Strategies. These are lateral thinking prompts that can be really helpful when you are stuck on a project</li>
<li><code>journal-prompts</code> - A collection of 100+ journaling prompts to get the creative juices flowing.</li>
<li><code>grumble</code> - my personal collection of quotes without a theme.</li>
</ul>
<p>I allow these to pop up randomly for the most part but it&rsquo;s really nice sometimes to be able to run <code>fortune oblique</code> and get something like:</p>
<blockquote>
<p>Don&rsquo;t stress one thing more than another</p></blockquote>
<p>Since the fortunes live in plaintext next to the binary dotfile, it is easy to get statistics with <code>grep</code> and <code>wc -l</code>.</p>
<p>For instance, I did a number of searches for prominent people to see who had the most quotes:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nb">cd</span> /usr/share/games/fortunes
</span></span><span class="line"><span class="cl">grep -i <span class="s2">&#34;Linus Torvalds&#34;</span> * 2&gt;/dev/null <span class="p">|</span> wc -l 
</span></span></code></pre></div><p>That yielded some shocking shortcomings. Linus Torvalds is quoted 152 times. Larry Wall, the creator of Perl, is quoted over 550 times! Poor Ken Thompson, the brilliant co-creator of Unix itself is only mentioned a handful of times. It was fun to explore and led me to add some quotes to my own collection to beef up those numbers! Luckily, once again, <code>fortune</code> is simple and fun to play with!</p>
<h2 id="install-and-use">Install and Use</h2>
<p>To get started, if fortune is not already installed (some distros include it, some don&rsquo;t), just run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Debian / Ubuntu / Mint</span>
</span></span><span class="line"><span class="cl">sudo apt update
</span></span><span class="line"><span class="cl">sudo apt install fortune-mod fortunes
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Fedora / RHEL / CentOS / Rocky</span>
</span></span><span class="line"><span class="cl">sudo dnf install fortune-mod fortune-mod-data
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Arch / Manjaro</span>
</span></span><span class="line"><span class="cl">sudo pacman -S fortune-mod
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># macOS </span>
</span></span><span class="line"><span class="cl">brew install fortune
</span></span></code></pre></div><p>Once complete, you should be able to run <code>fortune</code> and get a prompt. The fortunes package is often missing (I&rsquo;m looking at you macOS) and needs to be installed so that you have the full collection of standard fortune dat files.</p>
<h3 id="installing-system-wide-vs-local-folder">Installing system-wide vs local folder.</h3>
<p>Once you have fortune installed, adding more like the ones from this collection, is easy. You can choose whether to install them system-wide or only for your user account.</p>
<p><strong>System-Wide</strong>
Place your fortune files in:</p>
<ul>
<li>Linux: <code>/usr/share/games/fortunes</code></li>
<li>macOS: `$(brew &ndash;prefix fortune)/share/games/fortunes</li>
</ul>
<p>Installing system-wide requires sudo.</p>
<p><strong>Local (User only)</strong>
Make a folder for your fortunes</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p ~/.local/share/fortunes
</span></span><span class="line"><span class="cl">cp myfile myfile.dat ~/.local/share/fortunes/
</span></span></code></pre></div><p>Then point <code>fortune</code> there by adding this to your <code>.bashrc</code>/<code>.zshrc</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">FORTUNE_PATH</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.local/share/fortunes&#34;</span>
</span></span></code></pre></div><p>Then reload (open a new terminal tab or source your startup file with a command like <code>source .bashrc</code> or <code>source .zshrc</code>)</p>
<p>Now fortune will pull from both system and personal fortunes.</p>
<h3 id="makefile-magic">Makefile magic</h3>
<p>In order to make it as easy as possible to install and use my fortunes and any you might create, I have included a Makefile.</p>
<p>To use, simply run the following after cloning the github project to your local system:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">make                <span class="c1"># Compiles all fortune files into .dat</span>
</span></span><span class="line"><span class="cl">sudo make install   <span class="c1"># Installs them into the correct fortune directory</span>
</span></span><span class="line"><span class="cl">make clean          <span class="c1"># Removes all .dat files</span>
</span></span></code></pre></div><h3 id="adding-to-your-shell-startup">Adding to your shell startup</h3>
<p>You may want to display a random fortune from your shell startup files. Simply add the following to the end of your <code>.bashrc</code>, <code>.zshrc</code> or whichever file you are sourcing last if you have a complex startup.</p>
<p>Simply add <code>fortune</code> on a line by itself.</p>
<p>There are some switches you may want to be aware of. If you want to run a specific fortune each time add it like so:</p>
<p><code>fortune oblique</code></p>
<p>To choose only short fortunes, provide the -s argument like so:
<code>fortune -s</code></p>
<p>By default, most modern fortune packages try to keep potentially offensive fortunes separate. If you want to see those you can supply the -o argument, like <code>fortune -o</code></p>
<h2 id="adding-your-own-fortunes">Adding your own fortunes</h2>
<p><code>fortune</code> is one of those small, joyful tools that makes Unix feel alive — and it only gets better when you fill it with your own words, jokes, or wisdom. Luckily, making your own fortunes is easy! Simply open the fortune file (not the .dat) and add anything you like ensuring only that each line is separated by a % symbol. That might look like the following:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">this is a fortune 
</span></span><span class="line"><span class="cl">%
</span></span><span class="line"><span class="cl">this is another fortune
</span></span><span class="line"><span class="cl">% 
</span></span><span class="line"><span class="cl">this is yet another fortune
</span></span></code></pre></div><p>Once you have finished editing, you&rsquo;ll need to run strfile on simply use the Makefile.</p>
<p>Manually, you would just need to run:
<code>strfile my-fortunes my-fortunes.dat</code></p>
<p>you can then run `fortune ./my-fortunes or if you use my directory, simply run the Makefile like this:</p>
<p><code>make</code> and <code>sudo make install</code></p>
<p>Done! you should now have a fully working fortune file that anyone on your machine can use.</p>
<h2 id="conclusion">Conclusion</h2>
<p>I am very fond of the fortune program. I have used it in shell startup for decades. I have displayed it using the wonderful GeekTool app on Mac and even in the output of my publish-adminjitsu script. It&rsquo;s just ridiculously easy to use and it always makes me smile</p>
<blockquote>
<p>In 1869 the waffle iron was invented for people who had wrinkled waffles.</p></blockquote>
<p>Have a favorite quote, a missing collection, or an idea for a new fortune file?</p>
<p>📬 <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a> — Send me your best lines, corrections, or ideas.</p>
<p>Or, if you’re feeling fancy, open a pull request on the GitHub repo:
<a href="https://github.com/forfaxx/fortune-files">https://github.com/forfaxx/fortune-files</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Operation Wordlists</title>
      <link>https://adminjitsu.com/posts/operation-wordlists/</link>
      <pubDate>Fri, 25 Jul 2025 16:00:00 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/operation-wordlists/</guid>
      <description>Behind the scenes of Codename.py: using Python’s NLTK to generate, clean, and tag adjectives, nouns, and animals for naming things better than &amp;#39;project2&amp;#39;.</description>
      <content:encoded><![CDATA[<p>I recently posted about <a href="/posts/operation-codename"><strong>Operation Codename</strong></a> which generates fun codenames in a few styles. In this post we look at the scripts used to generate the wordlists.</p>
<h2 id="why-wordlists-matter">Why Wordlists Matter</h2>
<p>If you examine the codename script, you&rsquo;ll notice that it&rsquo;s deceptively simple. Given the wordlists the logic is mostly about arg handling and a few lines to combine adjectives and nouns with some random chances for single vs double-word names. The goal was to spit out plausible codenames in a variety of styles. Pulling from the system dictionary won&rsquo;t work since it is a just a flat list of words. I had a choice between hand curating a list of cool sounding words, which felt lame, or building them from the system dictionary (<code>/usr/share/dict</code>) with some added ability to parse parts of speech. That&rsquo;s where nltk comes in.</p>
<h2 id="enter-nltk">Enter NLTK</h2>
<p>Luckily there is a Swiss Army knife for working with language data. <a href="https://www.nltk.org/">NLTK</a> is a powerful Python library that comes with a ton of built-in corpora, dictionaries, and tools for things like tokenizing text or tagging parts of speech. It&rsquo;s ideal for building our word lists.</p>
<p>For Codename’s wordlists I used three key pieces:</p>
<ul>
<li><strong><code>words.words()</code></strong> — a flat dictionary of English words, hundreds of thousands of entries.</li>
<li><strong><code>brown.words()</code></strong> — the classic Brown Corpus, which adds a sense of how words actually show up in real text.</li>
<li><strong><code>pos_tag()</code></strong> — a simple but powerful part‑of‑speech tagger that can tell an adjective (<em>silent</em>) from a noun (<em>lantern</em>).</li>
</ul>
<p>Once I could reliably separate adjectives from nouns, I could start shaping lists that made sense — trimming weird outliers, filtering for words of a certain length (no “antidisestablishmentarianism”), and skipping words that almost never appear in normal writing.</p>
<h2 id="the-generator-scripts">The Generator Scripts</h2>
<p>I didn’t want Codename.py itself to do any of the heavy lifting. The main script should be fast — grab two words, mash them together, print. The heavy lifting lives in <code>/scripts</code>.</p>
<p>There are two helpers there:</p>
<ul>
<li><strong><code>generate_wordlists.py</code></strong> builds <code>adjectives.txt</code> and <code>nouns.txt</code>. It pulls in words from NLTK, tags them, filters them, and writes out clean, lowercase lists.</li>
<li><strong><code>generate_animals.py</code></strong> builds <code>animals.txt</code> using WordNet’s “animal” synset and its hyponyms — essentially every creature NLTK knows about.</li>
</ul>
<h3 id="generate_animals_wordlistpy">generate_animals_wordlist.py</h3>
<p>Here’s the core of how <code>generate_animals.py</code> works:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Step 1: Get all animal synsets from WordNet</span>
</span></span><span class="line"><span class="cl"><span class="n">animal_synsets</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">wn</span><span class="o">.</span><span class="n">synset</span><span class="p">(</span><span class="s1">&#39;animal.n.01&#39;</span><span class="p">)</span><span class="o">.</span><span class="n">closure</span><span class="p">(</span><span class="k">lambda</span> <span class="n">s</span><span class="p">:</span> <span class="n">s</span><span class="o">.</span><span class="n">hyponyms</span><span class="p">()))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Step 2: Extract and clean lemma names</span>
</span></span><span class="line"><span class="cl"><span class="n">animal_names</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">syn</span> <span class="ow">in</span> <span class="n">animal_synsets</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">lemma</span> <span class="ow">in</span> <span class="n">syn</span><span class="o">.</span><span class="n">lemmas</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">name</span> <span class="o">=</span> <span class="n">lemma</span><span class="o">.</span><span class="n">name</span><span class="p">()</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s1">&#39;_&#39;</span><span class="p">,</span> <span class="s1">&#39; &#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">name</span><span class="o">.</span><span class="n">isalpha</span><span class="p">()</span> <span class="ow">and</span> <span class="mi">3</span> <span class="o">&lt;=</span> <span class="nb">len</span><span class="p">(</span><span class="n">name</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="mi">12</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">animal_names</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">name</span><span class="o">.</span><span class="n">lower</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Step 3: Save to file</span>
</span></span><span class="line"><span class="cl"><span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s2">&#34;animals.txt&#34;</span><span class="p">,</span> <span class="s2">&#34;w&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s1">&#39;</span><span class="se">\n</span><span class="s1">&#39;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="nb">sorted</span><span class="p">(</span><span class="n">animal_names</span><span class="p">))</span> <span class="o">+</span> <span class="s1">&#39;</span><span class="se">\n</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;✅ Saved </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">animal_names</span><span class="p">)</span><span class="si">}</span><span class="s2"> animal names to animals.txt&#34;</span><span class="p">)</span>
</span></span></code></pre></div><h3 id="generate_codename_wordlistpy">generate_codename_wordlist.py</h3>
<p>For the main English wordlists, the script uses NLTK’s part‑of‑speech tagging to separate <strong>adjectives</strong> from <strong>nouns</strong>.</p>
<p>Here’s the heart of that logic from <code>generate_wordlists.py</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Break the big wordlist into chunks for tagging</span>
</span></span><span class="line"><span class="cl"><span class="n">adjectives</span><span class="p">,</span> <span class="n">nouns</span> <span class="o">=</span> <span class="p">[],</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">wordlist</span><span class="p">),</span> <span class="mi">1000</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">tagged</span> <span class="o">=</span> <span class="n">pos_tag</span><span class="p">(</span><span class="n">wordlist</span><span class="p">[</span><span class="n">i</span><span class="p">:</span><span class="n">i</span><span class="o">+</span><span class="mi">1000</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">word</span><span class="p">,</span> <span class="n">tag</span> <span class="ow">in</span> <span class="n">tagged</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">tag</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s1">&#39;JJ&#39;</span><span class="p">):</span>   <span class="c1"># Adjectives like &#34;silent&#34;</span>
</span></span><span class="line"><span class="cl">            <span class="n">adjectives</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">word</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">elif</span> <span class="n">tag</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s1">&#39;NN&#39;</span><span class="p">):</span> <span class="c1"># Nouns like &#34;lantern&#34;</span>
</span></span><span class="line"><span class="cl">            <span class="n">nouns</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">word</span><span class="p">)</span>
</span></span></code></pre></div><p>This chunk feeds thousands of words through pos_tag() and then deposits them into adjectives.txt and nouns.txt. The filtering step above trims out weird suffixes, apostrophes, and absurdly long words&ndash;so you end up with useful, relatively interesting lists instead of random words.</p>
<h2 id="whats-inside-the-lists">What’s Inside the Lists?</h2>
<p>Currently, we have 5,267 nouns, 2,314 adjectives and 2,533 animals in our files&ndash;all from the system dictionary.</p>
<p>The nouns range from grounded terms like fortress and summit to linguistic rareities like oubliette or cymbalom; the animals list includes expected entries like wolf and sparrow as well as delightfully rare ones like aardwolf.</p>
<h2 id="room-to-grow">Room to Grow</h2>
<p>I&rsquo;m currently working on adding Latin for a &ndash;legion mode that will create plausibly cool Roman and Warhammer 40k sounding codenames. That should be possible with the Classical Language Toolkit, Whitaker&rsquo;s words and possibly the Persesus Digital Library.</p>
<h2 id="links">Links</h2>
<ul>
<li><a href="https://www.nltk.org/">NLTK</a> - the Natural Language Toolkit</li>
<li><a href="https://github.com/USERNAME/codename">Codename.py on GitHub</a></li>
<li><a href="link-to-your-first-codename-post">Codename Overview Post</a></li>
<li><a href="https://github.com/USERNAME/codename/tree/main/scripts">Scripts Folder</a> - the scripts used to create the wordlists</li>
<li><a href="https://man7.org/linux/man-pages/man5/words.5.html">man page for <code>/usr/share/dict/words</code></a></li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>Questions, ideas, or wordlist suggestions?  I’d love to hear from you: <strong><a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>Operation Codename</title>
      <link>https://adminjitsu.com/posts/operation-codename/</link>
      <pubDate>Fri, 25 Jul 2025 09:18:49 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/operation-codename/</guid>
      <description>codename.py is a tiny Python script that generates clever, random codenames for your servers, projects, and releases. Choose your style: Ubuntu-style, military-style, or classic adjective–noun combos.</description>
      <content:encoded><![CDATA[<h3 id="what-is-codenamepy">What is codename.py?</h3>
<p><code>codename.py</code> is a small Python script that spits out <strong>fun, memorable names</strong> for whatever you’re working on.</p>
<ul>
<li>Need a <strong>server name</strong> that isn’t “server-b”?</li>
<li>Naming a <strong>side project</strong> or Git branch?</li>
<li>Want your test VM to sound like a secret mission?</li>
</ul>
<p>Run <code>codename.py</code> and get instant inspiration—whether that’s <em>Gallant Phoenix</em>, <em>Wily Wombat</em>, or <strong>Operation Turquoise Spider</strong>.</p>
<hr>
<p class="github-btn">
  <a href="https://github.com/forfaxx/codename" target="_blank">
    🔗 View codename.py on GitHub
  </a>
</p>
<hr>
<h3 id="features">Features</h3>
<ul>
<li><strong>Adjective–Noun combos</strong>: Clever Otter, Wandering Nebula, Ancient Lantern…</li>
<li><strong>Ubuntu-style</strong>: Alliterative, animal-inspired names (e.g., Brilliant Badger, Wily Wombat)</li>
<li><strong>Military-style</strong>: Operation-style names (Operation Silent Arrow)</li>
<li><strong>Single noun mode</strong>: Just a big, bold word</li>
<li><strong>JSON output</strong>: For piping into other tools</li>
<li><strong>Batch mode</strong>: Generate a bunch of names at once (<code>--count</code>)</li>
<li><strong>No dependencies</strong>: Pure Python, runs anywhere with <code>python3</code></li>
</ul>
<hr>
<h2 id="why-did-i-write-this">Why did I write this?</h2>
<p>I am the kind of person for whom everything just grinds to a halt the moment I have to name something. It could be a project or a video game character or a host name&ndash;it doesn&rsquo;t matter. I always sit there for way too long trying to think of the perfect name.</p>
<p>One night I started tinkering with a few wordlists and a Python script. It began spitting out names that made me <em>laugh</em>—and suddenly my VM wasn’t <strong>ubuntu-test3</strong> anymore. Now I didn&rsquo;t actually call it <strong>Operation Larvae Offensive</strong> but the silly output inspired <strong>better names</strong>.</p>
<p>That&rsquo;s the real magic: it shakes ideas loose and makes synapses fire. It&rsquo;s like having your own <em>/dev/inspiration</em> for ideas&ndash;an endless stream of names and half-names and weird combinations.</p>
<p>try running
<code>codename --count 50</code> or even <code>codename --count 1000</code> and see!</p>
<hr>
<h2 id="usage">Usage</h2>
<p>Run these commands right from your terminal:</p>
<ul>
<li><code>python3 codename.py</code> — Random “Adjective Noun” pair</li>
<li><code>python3 codename.py --ubuntu</code> — Ubuntu-style: adj &amp; animal share first letter</li>
<li><code>python3 codename.py --military</code> — Military-style: Operation + noun</li>
<li><code>python3 codename.py --noun</code> — Just a noun</li>
<li><code>python3 codename.py --count 5</code> — Generate five names</li>
</ul>
<p>The script uses standard libraries and does not require a venv. Just make sure to keep the wordlists in the same directory.</p>
<h3 id="sample-output">Sample Output</h3>
<p>in Military mode:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">Operation Mass Derision
</span></span><span class="line"><span class="cl">Operation Enhance
</span></span><span class="line"><span class="cl">Operation Known Tommy
</span></span><span class="line"><span class="cl">Operation Rival Threat
</span></span><span class="line"><span class="cl">Operation Slack
</span></span></code></pre></div><p>Or in Ubuntu mode:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">Vacant Vespid
</span></span><span class="line"><span class="cl">Obvious Osprey
</span></span><span class="line"><span class="cl">Tireless Treehopper
</span></span><span class="line"><span class="cl">Zealous Zebra
</span></span><span class="line"><span class="cl">Sustained Snipe
</span></span></code></pre></div><hr>
<h3 id="word-lists">Word Lists</h3>
<p>The magic comes from three simple text files:</p>
<ul>
<li>adjectives.txt — required</li>
<li>nouns.txt — required</li>
<li>animals.txt — optional (for Ubuntu mode)</li>
</ul>
<p>They live next to the script. Add your own words to make themed generators—spooky Halloween names, corporate buzzword names, or even fantasy D&amp;D-style names.</p>
<p>The Unix system dictionary is just a giant, flat list of words. Parsing for parts of speech (for example, to separate adjectives from nouns) requires libraries like <strong>NLTK</strong> and <strong>WordNet</strong>&ndash;and that&rsquo;s slow at runtime. Bundling pre-built wordlists with the script turned out to be a super-fast and lightweight alternative, and an <strong>easily extensible</strong> one.</p>
<p>👉 Look out for an upcoming post about how I made these wordlists using nltk and wordnet and about the joys of good corpora.</p>
<h2 id="future-direction">Future Direction</h2>
<ul>
<li>I&rsquo;m planning on fixing JSON output with <code>json.dumps()</code></li>
<li>I&rsquo;m working on a &ndash;theme argument to allow for custom word lists and new themes like &ndash;cyberpunk or &ndash;fantasy. Those will need custom dictionaries or a better way to &ldquo;detect&rdquo; a cool word.</li>
<li>I&rsquo;d also like to support a tiny web server mode so your non-technical friends might be impressed too!</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>Why settle for <em>project-2</em> when you could have, <em>Operation Neon Owl</em> or <em>Randy Rhinoceros</em>?
Grab your copy of codename.py on <a href="https://github.com/forfaxx/codename">GitHub</a> and start naming stuff like it matters!</p>
]]></content:encoded>
    </item>
    <item>
      <title>Jot and Friends</title>
      <link>https://adminjitsu.com/posts/jot-and-friends/</link>
      <pubDate>Thu, 24 Jul 2025 01:22:03 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/jot-and-friends/</guid>
      <description>Meet jot—a no-nonsense Bash function that lets you append timestamped messages to a Markdown file in your Obsidian vault. Fast journaling, quick notes, and daily logs right from your terminal, anywhere you sync Obsidian.</description>
      <content:encoded><![CDATA[<h3 id="the-function">the function</h3>
<p>Tired of losing stray thoughts? Tired of task-switching to record an idea? Well <code>jot</code> is your one-liner lifeline for adding timestamped entries to a running Markdown log with a quick terminal command.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># jot – Append a timestamped message to a Markdown file in your Obsidian vault</span>
</span></span><span class="line"><span class="cl">jot<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> vault note timestamp message input
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Detect platform and set vault path</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="k">$(</span>uname<span class="k">)</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;Darwin&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># macOS: standard Documents folder</span>
</span></span><span class="line"><span class="cl">    <span class="nv">vault</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/Documents/obsidian_vault&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">elif</span> grep -qi microsoft /proc/version 2&gt;/dev/null<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># WSL: write to the Windows user&#39;s Documents or Obsidian vault path</span>
</span></span><span class="line"><span class="cl">    <span class="nv">vault</span><span class="o">=</span><span class="s2">&#34;/mnt/c/Users/&lt;YourWindowsUsername&gt;/Documents/Obsidian Vault&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Regular Linux</span>
</span></span><span class="line"><span class="cl">    <span class="nv">vault</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/obsidian_vault&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nv">note</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$vault</span><span class="s2">/jot.md&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nv">timestamp</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>date <span class="s1">&#39;+%Y-%m-%d %H:%M:%S&#39;</span><span class="k">)</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"># Edit mode</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">$1</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;--edit&#34;</span> <span class="o">||</span> <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;-e&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="si">${</span><span class="nv">EDITOR</span><span class="k">:-</span><span class="nv">vim</span><span class="si">}</span> <span class="s2">&#34;</span><span class="nv">$note</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Read stdin if piped</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[</span> ! -t <span class="m">0</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nv">input</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>cat -<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nv">message</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$input</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">elif</span> <span class="o">[[</span> -z <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> -n <span class="s2">&#34;📝 Enter jot message: &#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">read</span> -r message
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">[[</span> -z <span class="s2">&#34;</span><span class="nv">$message</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">      <span class="nb">echo</span> <span class="s2">&#34;⚠️  No message entered. Aborting.&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    <span class="nv">message</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$*</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Append to jot.md</span>
</span></span><span class="line"><span class="cl">  <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;- [</span><span class="nv">$timestamp</span><span class="s2">]&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$message</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="o">}</span> &gt;&gt; <span class="s2">&#34;</span><span class="nv">$note</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;📝 Jotted to: </span><span class="nv">$note</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><hr>
<h3 id="usage">Usage</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">jot <span class="s2">&#34;Added notes to my zine.&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Multiline from a script&#34;</span> <span class="p">|</span> jot
</span></span><span class="line"><span class="cl">jot --edit
</span></span></code></pre></div><p><code>📝 Enter jot message:</code></p>
<p>If you run jot with no arguments it will prompt the user to type their message. The <code>--edit</code> option will open in your default editor for longer or more complex notes, like code blocks or multi-line entries. It will also handle piped output from other programs.</p>
<p>You will need to modify the vault path(s) in the script to point to your Obsidian vault.</p>
<p>In my setup, I jump between different linux machines, my windows machine running Ubuntu under Windows Subsystem for Linux and multiple Mint Linux boxes. The code is currently designed to support that by defining three paths based on detecting the environment and using the right path to my vault automatically. All you should need to do is to modify the path in the vault=path portion of the script.</p>
<p>Some path examples for the vault= line:</p>
<ul>
<li><strong>On macOS:</strong> <code>~/Documents/obsidian_vault</code></li>
<li><strong>On Linux:</strong> <code>~/obsidian_vault</code></li>
<li><strong>On WSL:</strong> <code>/mnt/c/Users/&lt;YourWindowsUsername&gt;/Documents/Obsidian Vault</code></li>
</ul>
<br>
<hr>
<h3 id="why-is-this-cool">Why is this cool?</h3>
<p>Being able to jot a note from the CLI is absolutely useful. Some existing projects allow you to use Obsidian from the CLI, but they often try to provide a whole interface. Jot is simple. If you think of a great idea while coding, you are just a single command away from capturing it:</p>
<ul>
<li>
<p><code>jot &quot;I had a great idea today. Holographic chess&quot; </code></p>
</li>
<li>
<p><code>jot &quot;#listening_to Cashing In by Minor Threat&quot;</code></p>
</li>
<li>
<p><code>jot &quot;Never name a server 'skynet'&quot;</code></p>
</li>
</ul>
<p>I try to review the Jot note in Obsidian from time to time and develop ideas and create proper notes for really solid ideas. It lowers the cost of writing something down in your second brain while you are already focused.</p>
<br>
<h3 id="conclusion">Conclusion</h3>
<p>I hope you will find it as useful as I have.</p>
<p>Please let me know if you have any <a href="mailto:feedback@adminjitsu.com">feedback</a> or ideas to improve it. Happy jotting!</p>
]]></content:encoded>
    </item>
    <item>
      <title>manfzf</title>
      <link>https://adminjitsu.com/posts/manfzf/</link>
      <pubDate>Thu, 24 Jul 2025 00:00:00 +0000</pubDate>
      <guid>https://adminjitsu.com/posts/manfzf/</guid>
      <description>Meet manfzf, an interactive CLI tool that lets you search, preview, and deep-dive through Unix man pages using Python, fzf, and your keyboard. Features &amp;#39;apropos zoom&amp;#39;, live previews, and a stack-based back function.</description>
      <content:encoded><![CDATA[<h2 id="what-is-manfzf">What is manfzf?</h2>
<figure style="text-align:center; margin: 1em auto;">
  <img src="rtfm-box-of-rocks.jpg" 
       alt="a photo of a box of rocks, labeled ROCKS with a book sitting next to it labeled the Manual" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    RTFM in style with manfzf()
  </figcaption>
</figure>
<p><code>manfzf</code> is an interactive Python tool that lets you <em>fuzzy search</em> every man page on your system, preview them live, and “zoom in” on related topics—all from the terminal.<br>
It’s designed for shell nerds, sysadmins, and anyone who wants to <em>explore</em> Unix docs, not just look up one thing.</p>
<hr>
<h2 id="quick-start">Quick Start</h2>
<p class="github-btn">
  <a href="https://github.com/forfaxx/manfzf" target="_blank">
    🔗 View manfzf on GitHub
  </a>
</p>
<p>Note: you will need to install fzf if you don&rsquo;t already have it, with something like <code>sudo apt install fzf</code></p>
<hr>
<h3 id="features">Features</h3>
<ul>
<li>Fuzzy search all installed man pages with <code>fzf</code></li>
<li>Live preview of the first 80 lines as you browse</li>
<li>Keyboard controls: <code>[ENTER]</code> to open, <code>Ctrl-A</code> to jump into apropos for the current topic, <code>Ctrl-B</code> to back up</li>
<li>Stack-based navigation (like breadcrumbs for your research rabbit holes)</li>
<li>Dracula-inspired color theme for preview and selection</li>
<li>Python 3, no dependencies except <code>fzf</code> and <code>man</code></li>
<li>Fast and minimal—just drop in and run</li>
</ul>
<hr>
<h3 id="why-did-i-write-this">Why did I write this?</h3>
<p><code>fzf</code> is an amazing tool. It didn&rsquo;t take long for the idea to dawn on me, why not a high tech new interface for <code>man</code>, <code>man -k</code> and <code>apropos</code>. I spend a lot of time in man pages but they have never been particularly friendly to browse and explore.</p>
<p><code>manfzf</code> solves this by showing a preview of the selected man page and offers the ability to &ldquo;zoom in&rdquo; and &ldquo;zoom out&rdquo; using apropos to find related man pages. Plus you get the ability to do fuzzy searches across the entire collection. I plan on improving it further but it is extremely useful to me already.</p>
<hr>
<h3 id="usage">Usage</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">python3 manfzf.py           <span class="c1"># Fuzzy search everything (default)</span>
</span></span><span class="line"><span class="cl">python3 manfzf.py awk       <span class="c1"># Start by searching for &#39;awk&#39;</span>
</span></span></code></pre></div><p>Type to filter,</p>
<ul>
<li><strong>[ENTER]</strong> to view a manpage.</li>
<li><strong>[Ctrl-A]</strong> runs apropos for your current selection (zoom deeper).</li>
<li><strong>[Ctrl-B]</strong> goes back one search context.</li>
<li><strong>[ESC]</strong> or no selection exits.</li>
</ul>
<h3 id="sample-output">Sample Output</h3>
<p><img alt="manfzf" loading="lazy" src="/posts/manfzf/manfzf.jpg"></p>
<hr>
<h3 id="future-direction-and-man2pdf">Future direction and man2pdf</h3>
<p>I plan to add a routine to render a selected man page as a pdf. Since I haven&rsquo;t gotten around to integrating it into <code>manfzf</code> yet, I will instead share my <code>man2pdf</code> functions here. Just add these to your shell startup (e.g., <code>.bashrc</code>, <code>.zshrc</code>) and reload (open a new tab, <code>source .bashrc</code>).</p>
<h4 id="macos">macOS</h4>
<p>The mac version makes use of Preview to render the postscript out of <code>man -t</code> into pdf.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Convert man page to PDF (macOS only)</span>
</span></span><span class="line"><span class="cl">man2pdf<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  man -t <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="p">|</span> open -f -a /Applications/Preview.app
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><h4 id="linux">Linux</h4>
<p>On Linux, you will first need to install Ghostscript, which provides ps2pdf using your distro&rsquo;s package manager (e.g., <code>sudo apt install ghostscript</code>)</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">man2pdf<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> <span class="nv">page</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">1</span><span class="p">:?Usage: man2pdf &lt;manpage&gt;</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> tmpfile
</span></span><span class="line"><span class="cl">  <span class="nv">tmpfile</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>mktemp --suffix<span class="o">=</span>.pdf /tmp/man2pdf-XXXXXX.pdf<span class="k">)</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"># Convert manpage to PDF</span>
</span></span><span class="line"><span class="cl">  man -t <span class="s2">&#34;</span><span class="nv">$page</span><span class="s2">&#34;</span> <span class="p">|</span> ps2pdf - <span class="s2">&#34;</span><span class="nv">$tmpfile</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Open the PDF using xdg-open (works on most desktops)</span>
</span></span><span class="line"><span class="cl">  xdg-open <span class="s2">&#34;</span><span class="nv">$tmpfile</span><span class="s2">&#34;</span> &gt;/dev/null 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="p">&amp;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><p>I&rsquo;ll eventually add this to manfzf, but in the meantime, I hope you enjoyed this loot-filled sidequest!</p>
<h3 id="conclusion">Conclusion</h3>
<p>With manfzf on your system, you too can RTFM with style!</p>
]]></content:encoded>
    </item>
    <item>
      <title>pypeek</title>
      <link>https://adminjitsu.com/posts/pypeek/</link>
      <pubDate>Wed, 23 Jul 2025 00:00:00 +0000</pubDate>
      <guid>https://adminjitsu.com/posts/pypeek/</guid>
      <description>Introducing pypeek—a Python-based utility designed for quickly inspecting Python objects, modules, or files from the command line. Great for debugging, exploring unfamiliar code, and boosting your workflow.</description>
      <content:encoded><![CDATA[<h3 id="what-is-pypeek">What is pypeek?</h3>
<p><code>pypeek</code> is a minimal Python command-line tool that lets you inspect objects, modules, or files with one command. It’s built to be fast, lightweight, and useful for debugging or code exploration.</p>
<hr>
<p class="github-btn">
  <a href="https://github.com/forfaxx/pypeek" target="_blank">
    🔗 View pypeek on GitHub
  </a>
</p>
<hr>
<h3 id="features">Features</h3>
<ul>
<li>Quickly preview attributes and docstrings of any Python object</li>
<li>Peek inside <code>.py</code> files or modules without running them</li>
<li>Pretty, readable output for both shell and IDE use</li>
<li>Makes sense of unfamiliar codebases in seconds</li>
</ul>
<h3 id="why-did-i-write-this">Why did i write this?</h3>
<p>I was finding it difficult to visualize my longer and multi-file programs and wanted a quick view like this. I looked for an existing tool but nothing worked the way I wanted. I decided to take the caveman approach and create the tool I wanted. It&rsquo;s a work in progress but a useful one.</p>
<h3 id="usage">usage</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">pypeek myscript.py              <span class="c1"># Show classes, functions, entry point</span>
</span></span><span class="line"><span class="cl">pypeek myscript.py:SomeClass    <span class="c1"># Inspect a specific class or object</span>
</span></span><span class="line"><span class="cl">pypeek myscript.py --verbose    <span class="c1"># Show all return paths with conditions</span>
</span></span></code></pre></div><h3 id="sample-output">Sample Output</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ python cavepeek.py my_script.py
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">📄 my_script.py
</span></span><span class="line"><span class="cl">──────────────────────────────
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">📘 Module:
</span></span><span class="line"><span class="cl">Does awesome things.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">🏩 Classes:
</span></span><span class="line"><span class="cl">──────────────────────────────
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">🧱 class Ninja
</span></span><span class="line"><span class="cl">  • stealth<span class="o">(</span>self<span class="o">)</span>
</span></span><span class="line"><span class="cl">    📘 Move silently.
</span></span><span class="line"><span class="cl">    ↪ <span class="k">return</span> <span class="s1">&#39;shadow&#39;</span>
</span></span><span class="line"><span class="cl">  • attack<span class="o">(</span>self, target<span class="o">)</span>
</span></span><span class="line"><span class="cl">    ↪ <span class="o">(</span>no <span class="k">return</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">🔧 Top-Level Functions:
</span></span><span class="line"><span class="cl">──────────────────────────────
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">• helper<span class="o">()</span>
</span></span><span class="line"><span class="cl">  ↪ <span class="k">return</span> True
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">🚀 Entry Point:
</span></span><span class="line"><span class="cl">──────────────────────────────
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">• main<span class="o">()</span>
</span></span><span class="line"><span class="cl">  ↪ <span class="k">return</span> <span class="m">0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">🚀 Executable:
</span></span><span class="line"><span class="cl">Yes <span class="o">(</span>has __main__ block<span class="o">)</span>
</span></span></code></pre></div><h2 id="cool-bits">Cool bits</h2>
<p>I learned some useful tricks creating this script. Here are a few highlights:</p>
<ul>
<li><strong>AST (Abstract Syntax Tree)</strong></li>
</ul>
<p>Pypeek uses the <strong>ast</strong> module to map out top-level functions, classes, docstrings and more&ndash;no code execution required. We use a custom CodeSummary class to walk the AST for a given file to peek at its structure and introspect interesting details.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">CodeSummary</span><span class="p">(</span><span class="n">ast</span><span class="o">.</span><span class="n">NodeVisitor</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">visit_ClassDef</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">node</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Finds all classes and their methods</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">visit_FunctionDef</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">node</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Gathers all functions, their arguments, docstrings, returns</span>
</span></span></code></pre></div><p>Again a caveman approach but it has the benefit of working on structure and not requiring running code.</p>
<ul>
<li><strong>Context-Aware return tracking</strong></li>
</ul>
<p>Finds the conditional associated with a return that lives inside nested logic or an if statement. It prints the return <em>and</em> the condition that wraps it. It&rsquo;s a great technique for understanding your outputs as well as your inputs.</p>
<ul>
<li><strong>Main Function Special Treatment</strong></li>
</ul>
<p>If there is a <code>main()</code> function, it gets highlighted separately.  Iif the file has an <code>if __name__==&quot;__main__&quot;</code> block, you&rsquo;ll know if it&rsquo;s callable from the CLI.</p>
<ul>
<li><strong>Pretty Print Helpers</strong></li>
</ul>
<p>Functions and methods are shown with names, arguments, docstring line and each return&rsquo;s code/conditions in a compact, quick-to-read and CLI friendly way.</p>
<ul>
<li><strong>Graceful Fallbacks</strong></li>
</ul>
<p>Non-Python file? Syntax errors? Missing docstrings? Everything gets handled gently with clear CLI feedback. No ugly tracebacks.</p>
]]></content:encoded>
    </item>
    <item>
      <title>seer suite</title>
      <link>https://adminjitsu.com/posts/seer-suite/</link>
      <pubDate>Wed, 23 Jul 2025 00:00:00 +0000</pubDate>
      <guid>https://adminjitsu.com/posts/seer-suite/</guid>
      <description>Seer Suite is a collection of Bash command-line tools for sysadmins and power users. Quickly inspect files, hunt down processes, probe your package manager, or scan your network. Born from practical needs and late-night hacking.</description>
      <content:encoded><![CDATA[<h3 id="what-are-seer-scripts">what are seer scripts?</h3>
<p><strong>The Seer Suite</strong> is a collection of Bash scripts I wrote to see through the surface of any Unix system—files, processes, packages or the network, like magic! If you’ve ever wanted <code>ls</code>, <code>lsof</code>, <code>ps</code>, and a pile of forensics tools to just play nice together, this suite is for you. They’re as fast, portable, and 100% terminal-friendly as possible.</p>
<p><em>Born from an old work forensics script (“maxinfo”) and endlessly polished on sleepless nights. If you like transparency, you’ll love these.</em></p>
<hr>
<p class="github-btn">
  <a href="https://github.com/forfaxx/seer-suite" target="_blank">
    🔗 View Seer Scripts on GitHub
  </a>
</p>
<hr>
<h3 id="features">features</h3>
<ul>
<li>Inspect any file—see type, stat, hashes, attributes, metadata, and more</li>
<li>Analyze processes by pattern, PID, tree, or open ports</li>
<li>Cross-distro package lookup and info (supports major Linux/BSD package managers)</li>
<li>LAN and network inspection (interfaces, routes, live hosts, port checks)</li>
<li>Shared Bash helper library for DRY code</li>
<li>Human-friendly output, color where supported</li>
<li>Built to be safe, readable, and easy to hack</li>
</ul>
<hr>
<h3 id="why-did-i-write-this">why did i write this?</h3>
<p>I do a lot of digital forensics work and deep dives and I needed to automate some of the commands I always found myself running manually. The concept behind these was to have a way to zoom in and see all the info for various parts of the system. Basically, automate the boring stuff!
I tested extensively and made an effort to be cross-platform friendly and useful in the real world.</p>
<hr>
<h3 id="usage">usage</h3>
<p>Clone/download all scripts (and <code>dotlib.sh</code>!), then make them executable:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">git clone https://github.com/forfaxx/seer-suite.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> seer-suite
</span></span><span class="line"><span class="cl">chmod +x *.sh
</span></span><span class="line"><span class="cl"><span class="c1"># (Make sure dotlib.sh is present, or set $DOTFILES for your dotfiles path)</span>
</span></span></code></pre></div><h3 id="sample-output">Sample output</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">./file-seer.sh inspect ~/Downloads/unknown_file
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">──────────────────────────────
</span></span><span class="line"><span class="cl">🧾 File: secret.img
</span></span><span class="line"><span class="cl">──────────────────────────────
</span></span><span class="line"><span class="cl">Type:          regular file
</span></span><span class="line"><span class="cl">Size:          1.4 MB
</span></span><span class="line"><span class="cl">MIME:          application/octet-stream
</span></span><span class="line"><span class="cl">SHA256:        f0e1d2c3b4a59687e5f4...
</span></span><span class="line"><span class="cl">Permissions:   rw-r--r--
</span></span><span class="line"><span class="cl">Owner:         grumble:users
</span></span><span class="line"><span class="cl">Created:       2024-11-20 03:17:44
</span></span><span class="line"><span class="cl">Modified:      2024-12-01 21:55:10
</span></span><span class="line"><span class="cl">Attributes:    <span class="o">(</span>none<span class="o">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">🔍 Stat Summary:
</span></span><span class="line"><span class="cl">Device: 803h/2051d  Inode: <span class="m">1284421</span>   Links: <span class="m">1</span>
</span></span><span class="line"><span class="cl">Blocks: <span class="m">2800</span>        IO Block: <span class="m">4096</span>   regular file
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">🔑 Extended Attributes:
</span></span><span class="line"><span class="cl">- <span class="o">(</span>none<span class="o">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">📚 Metadata:
</span></span><span class="line"><span class="cl">- <span class="o">(</span>no embedded metadata<span class="o">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">───────────────
</span></span><span class="line"><span class="cl">Hexdump Preview <span class="o">(</span>first <span class="m">32</span> bytes<span class="o">)</span>
</span></span><span class="line"><span class="cl">───────────────
</span></span><span class="line"><span class="cl"><span class="m">00000000</span>  1f 8b <span class="m">08</span> <span class="m">00</span> <span class="m">00</span> <span class="m">00</span> <span class="m">00</span> <span class="m">00</span>  <span class="m">02</span> <span class="m">03</span> 7c 0c <span class="m">00</span> <span class="m">00</span> <span class="m">00</span> <span class="m">00</span>  <span class="p">|</span>..........<span class="p">|</span>.....<span class="p">|</span>
</span></span><span class="line"><span class="cl">...
</span></span></code></pre></div><hr>
<h2 id="cool-bits">Cool bits</h2>
<p>There are a few conventions that I really like in these scripts:</p>
<ul>
<li><strong>Subcommand pattern</strong></li>
</ul>
<p>Every script has a subcommand pattern just like git or docker—easy to remember, easy to extend.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">case</span> <span class="s2">&#34;</span><span class="nv">$CMD</span><span class="s2">&#34;</span> in
</span></span><span class="line"><span class="cl">  search<span class="o">)</span>   search_process <span class="s2">&#34;</span><span class="nv">$@</span><span class="s2">&#34;</span><span class="p">;;</span>
</span></span><span class="line"><span class="cl">  info<span class="o">)</span>     process_info <span class="s2">&#34;</span><span class="nv">$2</span><span class="s2">&#34;</span><span class="p">;;</span>
</span></span><span class="line"><span class="cl">  tree<span class="o">)</span>     process_tree<span class="p">;;</span>
</span></span><span class="line"><span class="cl">  ports<span class="o">)</span>    process_ports<span class="p">;;</span>
</span></span><span class="line"><span class="cl">  help<span class="p">|</span>*<span class="o">)</span>   usage<span class="p">;;</span>
</span></span><span class="line"><span class="cl"><span class="k">esac</span>
</span></span></code></pre></div><ul>
<li><strong>Cross-Platform and defensive patterns</strong></li>
</ul>
<p>Scripts start with <code>set -euo pipefail</code> to avoid hidden failures. All commands are wrapped to gracefully handle missing components. In cases like the use of ANSI color, I provide an option and handling to avoid polluting output where it isn&rsquo;t desirable. The suite features these and more, friendly and robust conventions.</p>
<hr>
<p align="center">
  <img src="candle.jpg" alt="A glowing candle for the Seer Suite" width="220">
  Happy seeing!
</p>
]]></content:encoded>
    </item>
    <item>
      <title>Ninja Style-Dojo</title>
      <link>https://adminjitsu.com/posts/ninjas/</link>
      <pubDate>Tue, 22 Jul 2025 13:41:33 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/ninjas/</guid>
      <description>Sandbox for trying out Hugo features, shortcodes, fonts, images, code blocks, and more. Great for hackers, tinkerers, and theme tweakers.</description>
      <content:encoded><![CDATA[<p>⚠ Just a humble style playground for the site. nothing to see here unless you dig awesomeness ⚠</p>
<hr>
<h2 id="download-this-pages-markdown--css">Download This Page’s Markdown &amp; CSS</h2>
<p class="github-btn" style="margin-bottom:1.2em;">
  <a href="/ninja-test-page.md" download>
    📝 Download Ninja Test Markdown
  </a>
  &nbsp;
  <a href="/custom.css" download>
    🎨 Download Custom CSS
  </a>
</p>
<hr>
<h2 id="the-glyphs">The Glyphs!!</h2>
<p>Copy and paste those annoying symbols that are tough to remember:</p>
<h3 id="symbols--special-characters">Symbols &amp; Special Characters</h3>
<ul>
<li>Em dash: —</li>
<li>En dash: –</li>
<li>Infinity: ∞</li>
<li>Bullet: •</li>
<li>Check mark: ✔</li>
<li>Cross mark: ✘</li>
<li>Arrow: →</li>
<li>Box drawing (thin): ─ │ ┌ ┐ └ ┘</li>
<li>Box drawing (heavy): ━┃┏┓┗┛</li>
<li>Section (§): §</li>
<li>Copyright: ©</li>
<li>Registered: ®</li>
<li>Degree: °</li>
<li>Pi: π</li>
<li>Micro: µ</li>
<li>Command prompt: $</li>
<li>Backtick: `</li>
</ul>
<h3 id="some-useful-emojis">Some useful emojis</h3>
<ul>
<li>🚀  Rocket         :rocket:        U+1F680</li>
<li>✅  Check Mark     :white_check_mark:  U+2705</li>
<li>❌  Cross Mark     :x:             U+274C</li>
<li>⚠️  Warning        :warning:       U+26A0 FE0F</li>
<li>📝  Memo/Note      :memo:          U+1F4DD</li>
<li>📦  Package/Box    :package:       U+1F4E6</li>
<li>🔗  Link           :link:          U+1F517</li>
<li>🧑‍💻  Technologist  :technologist:  U+1F9D1 200D 1F4BB</li>
<li>🔒  Lock           :lock:          U+1F512</li>
<li>🔑  Key            :key:           U+1F511</li>
<li>🔍  Magnifier      :mag:           U+1F50D</li>
<li>💡  Light Bulb     :bulb:          U+1F4A1</li>
<li>📅  Calendar       :calendar:      U+1F4C5</li>
<li>📋  Clipboard      :clipboard:     U+1F4CB</li>
<li>🧙‍♂️  Mage/Wizard   :mage:          U+1F9D9 200D 2642 FE0F</li>
<li>🔧  Wrench         :wrench:        U+1F527</li>
<li>💾  Floppy Disk    :floppy_disk:   U+1F4BE</li>
<li>🔥  Fire           :fire:          U+1F525</li>
<li>📖  Book/Docs      :book:          U+1F4D6</li>
<li>📢  Loudspeaker    :loudspeaker:   U+1F4E2</li>
<li>🤖  Robot          :robot:         U+1F916</li>
<li>🧩  Puzzle Piece   :jigsaw:        U+1F9E9</li>
<li>🎯  Target         :dart:          U+1F3AF</li>
<li>⚡  High Voltage   :zap:           U+26A1</li>
<li>💥  Collision      :boom:          U+1F4A5</li>
<li>🔁  Repeat         :repeat:        U+1F501</li>
<li>⏳  Hourglass      :hourglass_flowing_sand: U+23F3</li>
<li>🛠️  Tools         :hammer_and_wrench: U+1F6E0 FE0F</li>
<li>🚧  Construction   :construction:  U+1F6A7</li>
<li>🦄  Unicorn        :unicorn:       U+1F984</li>
<li>🌟  Glowing Star   :star2:         U+1F31F</li>
</ul>
<hr>
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <title>Feather Icon Style Test • Grid Demo</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <script defer src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
    <style>
        :root {
            --gap: 14px;
            --bg: #fafafa;
            --fg: #222;
            --muted: #666;
            --card: #fff;
            --ring: rgba(0, 0, 0, .08);
        }

        @media (prefers-color-scheme: dark) {
            :root {
                --bg: #0f1115;
                --fg: #e7e7e7;
                --muted: #a0a0a0;
                --card: #141720;
                --ring: rgba(255, 255, 255, .08);
            }
        }

        html,
        body {
            height: 100%;
        }

        body {
            margin: 0;
            background: var(--bg);
            color: var(--fg);
            font: 16px/1.5 system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji";
        }

        .wrap {
            max-width: 1080px;
            margin: 32px auto;
            padding: 0 16px;
        }

        h1 {
            margin: 0 0 8px;
            font-size: 22px;
        }

        .sub {
            color: var(--muted);
            margin-bottom: 18px;
        }

        /* ===========================
           CONTROLS
           =========================== */
        .controls {
            display: grid;
            grid-template-columns: 1fr auto auto;
            gap: var(--gap);
            align-items: center;
            background: var(--card);
            border-radius: 14px;
            padding: 12px 14px;
            box-shadow: 0 1px 0 var(--ring), 0 8px 24px -20px var(--ring);
            position: sticky;
            top: 12px;
            z-index: 5;
            backdrop-filter: blur(6px);
        }

        .controls input[type="search"] {
            width: 100%;
            padding: 10px 12px;
            border: 1px solid var(--ring);
            background: transparent;
            color: var(--fg);
            border-radius: 10px;
            outline: none;
        }

        .controls .sliders {
            display: flex;
            gap: 16px;
            align-items: center;
        }

        .controls label {
            font-size: 13px;
            color: var(--muted);
            display: flex;
            align-items: center;
            gap: 8px;
            white-space: nowrap;
        }

        .controls input[type="range"] {
            width: 140px;
        }

        .grid {
            display: grid;
            grid-template-columns: repeat(6, minmax(0, 1fr));
            gap: var(--gap);
            margin-top: 18px;
        }

        @media (max-width: 1000px) {
            .grid {
                grid-template-columns: repeat(4, 1fr);
            }
        }

        @media (max-width: 640px) {
            .grid {
                grid-template-columns: repeat(3, 1fr);
            }
        }

        @media (max-width: 420px) {
            .grid {
                grid-template-columns: repeat(2, 1fr);
            }
        }

        .card {
            background: var(--card);
            border-radius: 14px;
            box-shadow: 0 1px 0 var(--ring), 0 10px 24px -20px var(--ring);
            padding: 14px 12px;
            display: flex;
            gap: 10px;
            align-items: center;
            cursor: pointer;
            user-select: none;
            transition: transform .06s ease, box-shadow .12s ease;
        }

        .card:hover {
            transform: translateY(-1px);
            box-shadow: 0 1px 0 var(--ring), 0 18px 36px -18px var(--ring);
        }

        .glyph {
            flex: 0 0 auto;
        }

        .meta {
            display: grid;
            gap: 2px;
            overflow: hidden;
        }

        .name {
            font-size: 14px;
            font-weight: 600;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }

        .hint {
            font-size: 12px;
            color: var(--muted);
        }

        /* inline demo styles (from your original) */
        .inline-icon {
            width: 20px;
            height: 20px;
            vertical-align: middle;
            stroke-width: 2;
        }

        .inline-icon.large {
            width: 32px;
            height: 32px;
        }

        .inline-icon.thin {
            stroke-width: 1;
        }

        .inline-icon.thick {
            stroke-width: 3;
        }

        .inline-icon.accent {
            stroke: crimson;
        }

        .demo {
            background: var(--card);
            border-radius: 14px;
            padding: 14px;
            margin-top: 22px;
            box-shadow: 0 1px 0 var(--ring), 0 8px 24px -20px var(--ring);
        }

        .demo p {
            margin: 6px 0;
        }

        .toast {
            position: fixed;
            left: 50%;
            bottom: 22px;
            transform: translateX(-50%);
            background: var(--card);
            color: var(--fg);
            border: 1px solid var(--ring);
            padding: 10px 14px;
            border-radius: 10px;
            font-size: 13px;
            opacity: 0;
            transition: opacity .25s ease, transform .25s ease;
            pointer-events: none;
            box-shadow: 0 8px 24px -16px var(--ring);
        }

        .toast.show {
            opacity: 1;
            transform: translate(-50%, -4px);
        }

        .muted {
            color: var(--muted);
        }

        /* === added: honor reduced motion === */
        @media (prefers-reduced-motion: reduce) {
            .card {
                transition: none;
            }

            .card:hover {
                transform: none;
                box-shadow: 0 1px 0 var(--ring), 0 10px 24px -20px var(--ring);
            }
        }
    </style>
</head>

<body>
    <div class="wrap">
        <h3>Adminjitsu Feather Icon Style Test</h3>
        <p class="sub">Quick grid of commonly useful icons with size / stroke controls. Click a tile to copy its name.
            <br><strong>Click:</strong> copy &lt;i&gt; with size/stroke/color. <strong>Alt+Click:</strong> copy full
            inline SVG.
        </p>

        <!-- ===========================
             CONTROLS
             =========================== -->
        <div class="controls">
            <input id="search" type="search" placeholder="Filter icons… (e.g. 'cloud', 'code', 'arrow')" />
            <div class="sliders">
                <label>Size <input id="size" type="range" min="16" max="64" value="28"></label>
                <label>Stroke <input id="stroke" type="range" min="1" max="3" step="0.5" value="2"></label>
                <label>Color <input id="color" type="color" value="#222222"></label>
            </div>
        </div>


        <!-- Grid injected here -->
        <div id="grid" class="grid" aria-live="polite"></div>

        <!-- Your original inline demo -->
        <div class="demo">
            <p><span class="muted">Inline variants (rss):</span></p>
            <p>Standard: <i data-feather="rss" class="inline-icon"></i></p>
            <p>Large: <i data-feather="rss" class="inline-icon large"></i></p>
            <p>Thin stroke: <i data-feather="rss" class="inline-icon thin"></i></p>
            <p>Thick stroke: <i data-feather="rss" class="inline-icon thick"></i></p>
            <p>Accent color: <i data-feather="rss" class="inline-icon accent"></i></p>
        </div>
    </div>

    <div id="toast" class="toast" role="status" aria-live="polite">Copied</div>

    <script>
        // ===========================
        // ICONS — your original curated set
        // ===========================
        const ICONS = [
            // Navigation & UI
            "home", "menu", "grid", "list", "filter", "search", "settings", "sliders", "tool", "help-circle",
            "info", "alert-triangle", "bell", "bookmark", "flag", "tag", "star", "heart", "thumbs-up", "thumbs-down",
            "share-2", "external-link", "link", "copy", "scissors", "maximize", "minimize", "zoom-in", "zoom-out",
            "refresh-cw", "refresh-ccw",

            // Arrows & Paging
            "arrow-right", "arrow-left", "arrow-up", "arrow-down", "chevron-right", "chevron-left", "chevron-up", "chevron-down",
            "corner-up-right", /* fix: was corner-left-down */ "corner-down-left", "arrow-right-circle", "arrow-left-circle",

            // Files & Content
            "file", "file-text", "file-plus", "file-minus", "file-search", "folder", "folder-plus", "folder-minus", "clipboard", "paperclip",

            // Time & Calendar
            "clock", "watch", "calendar", "activity", "trending-up", "trending-down",

            // System & Dev
            "cpu", "server", "database", "terminal", "code", "git-branch", "git-commit", "git-merge", "git-pull-request", "package",
            "cloud", "cloud-drizzle", "cloud-lightning", "cloud-off", "wifi",

            // Security
            "lock", "unlock", "key", "shield", "shield-off",

            // Media
            "camera", "image", "video", "music", "mic", "mic-off", "headphones", "speaker", "volume-2", "rss",

            // People
            "user", "user-plus", "user-minus", "users", "at-sign", "mail", "message-square", "inbox",

            // Status & Forms
            "check", "x", "plus", "minus", "edit", "trash-2", "save", "upload", "download", "printer"
        ];

        // === Extra icons appended (keeps your const untouched) ===
        ICONS.push(
            "alert-circle", "alert-octagon", "anchor", "aperture", "archive",
            "award", "bar-chart-2", "battery-charging", "box", "briefcase",
            "cast", "coffee", "command", "compass", "credit-card",
            "disc", "dollar-sign", "droplet", "eye", "globe",
            "hard-drive", "layers", "life-buoy", "link-2", "loader",
            "log-in", "log-out", "maximize-2", "minimize-2", "monitor",
            "moon", "mouse-pointer", "navigation", "percent", "pie-chart",
            "play", "power", "rotate-cw", "rotate-ccw", "send",
            "sidebar", "smartphone", "sun", "target", "thermometer",
            "toggle-left", "toggle-right", "tv", "type", "umbrella",
            "user-check", "user-x", "video-off", "voicemail", "volume-x",
            "wifi-off", "wind", "zap"
        );

        // ===========================
        // DOM handles
        // ===========================
        const grid = document.getElementById('grid');
        const q = document.getElementById('search');
        const size = document.getElementById('size');
        const stroke = document.getElementById('stroke');
        const color = document.getElementById('color');
        const toast = document.getElementById('toast');

        // ===========================
        // RENDER  (drop-in replacement)
        // ===========================
        function render(list = ICONS) {
            grid.innerHTML = list.map(name => `
    <button class="card" type="button" data-name="${name}" 
            title="Click to copy: ${name} (Alt+Click copies SVG)"
            aria-label="Copy icon ${name}">
      <span class="glyph"><i data-feather="${name}"></i></span>
      <span class="meta">
        <span class="name">${name}</span>
        <span class="hint">${parseInt(size.value, 10)}px • ${stroke.value}px</span>
      </span>
    </button>
  `).join('');

            // Let currentColor flow to new SVGs
            grid.style.color = color.value;

            // Replace <i> → <svg> with size & stroke-width (do NOT pass 'stroke' here)
            feather.replace({
                width: size.value,
                height: size.value,
                'stroke-width': stroke.value
            });

            // Force color on just-inserted SVGs so they update dynamically
            const svgs = grid.querySelectorAll('svg.feather');
            svgs.forEach(svg => {
                svg.setAttribute('stroke', color.value); // presentation attribute
                svg.style.stroke = color.value;          // inline style (wins vs. site CSS)
            });
        }


        // ===========================
        // COPY HELPERS
        // ===========================

        // added: write() with clipboard fallback for non-secure contexts
        function write(text, okMsg) {
            if (navigator.clipboard && window.isSecureContext) {
                return navigator.clipboard.writeText(text).then(() => showToast(okMsg));
            }
            // Fallback for http/file://
            const ta = document.createElement('textarea');
            ta.value = text;
            document.body.appendChild(ta);
            ta.select();
            try { document.execCommand('copy'); } catch (_) { }
            ta.remove();
            showToast(okMsg + ' (fallback)');
        }

        // Click: copy <i …> with attributes (width/height/stroke-width/stroke)
        function copyI(name) {
            // added: guard against unknown icon names
            if (!feather.icons[name]) { showToast(`unknown icon: ${name}`); return; }

            const w = parseInt(size.value, 10);
            const s = stroke.value;
            const c = color.value;
            // Use stroke attribute (feather.replace() reads this and preserves color)
            const snippet = `<i data-feather="${name}" width="${w}" height="${w}" stroke-width="${s}" stroke="${c}"></i>`;
            write(snippet, `copied: ${snippet}`);
        }

        // Alt+Click: copy full inline SVG; include both stroke attr and inline style to resist CSS overrides
        function copySVG(name) {
            // added: guard against unknown icon names
            const icon = feather.icons[name];
            if (!icon) { showToast(`unknown icon: ${name}`); return; }

            const w = parseInt(size.value, 10);
            const s = stroke.value;
            const c = color.value;

            let svg = icon.toSvg({
                width: w,
                height: w,
                stroke: c,                 // explicit color
                'stroke-width': s,
                'stroke-linecap': 'round',
                'stroke-linejoin': 'round'
            });
            // Also apply inline style so hostile CSS can't override presentation attribute
            svg = svg.replace('<svg', `<svg style="stroke:${c}"`);
            write(svg, `copied SVG: feather-${name}`);
        }

        // ===========================
        // TOAST
        // ===========================
        function showToast(msg) {
            toast.textContent = msg;
            toast.classList.add('show');
            clearTimeout(showToast._t);
            showToast._t = setTimeout(() => toast.classList.remove('show'), 1100);
        }

        // ===========================
        // EVENTS
        // ===========================
        grid.addEventListener('click', (e) => {
            const card = e.target.closest('.card');
            if (!card) return;
            if (e.altKey) copySVG(card.dataset.name);
            else copyI(card.dataset.name);
        });

        // added: keyboard activation parity (Enter/Space)
        grid.addEventListener('keydown', (e) => {
            if (e.key !== 'Enter' && e.key !== ' ') return;
            const card = e.target.closest('.card');
            if (!card) return;
            e.preventDefault();
            if (e.altKey) copySVG(card.dataset.name);
            else copyI(card.dataset.name);
        });

        q.addEventListener('input', () => {
            const term = q.value.trim().toLowerCase();
            const filtered = ICONS.filter(n => n.includes(term));
            render(filtered);
        });

        function rerenderFiltered() {
            const term = q.value.trim().toLowerCase();
            render(term ? ICONS.filter(n => n.includes(term)) : ICONS);
        }

        size.addEventListener('input', rerenderFiltered);
        stroke.addEventListener('input', rerenderFiltered);
        color.addEventListener('input', rerenderFiltered);

        // ===========================
        // INIT
        // ===========================
        window.addEventListener('load', () => {
            render();
            // also render your inline demo
            feather.replace();
        });
    </script>
</body>

</html>
<h2 id="obligatory-lorem-ipsum-block">Obligatory Lorem ipsum block</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod sem at felis scelerisque, at ultrices purus pellentesque. Praesent sit amet turpis non leo suscipit laoreet. Vivamus interdum sapien ut risus vulputate, non congue justo vestibulum. Nunc volutpat dui ac magna cursus, in pulvinar magna gravida. Nulla facilisi. Phasellus vel dui ut libero facilisis sollicitudin vel ac sapien. Etiam posuere velit nec orci viverra, sit amet ullamcorper elit fermentum.</p>
<p>Mauris interdum, nibh in commodo consequat, lorem elit porta mauris, nec pretium nunc urna ac lorem. Sed nec turpis ac mauris fermentum posuere.
<figure>
    <img loading="lazy" src="ninja1.gif"
         alt="closeup of a masked ninja"/> <figcaption>
            It&#39;s a ninja!!
        </figcaption>
</figure>

Mauris interdum, nibh in commodo consequat, lorem elit porta mauris, nec pretium nunc urna ac lorem. Sed nec turpis ac mauris fermentum posuere. Fusce viverra, tortor sed porta feugiat, justo sapien fermentum sapien, sed facilisis lacus est nec eros. Integer tincidunt enim sed est ultrices, nec sagittis nisl gravida. Pellentesque congue sapien a sapien tempor, non posuere est congue. Ut sed nisl ut lectus ultricies egestas. Vivamus a mi sit amet nibh commodo suscipit a sed ipsum.</p>
<figure style="float:left; margin:0 1rem 1rem 0; width:clamp(260px, 45%, 550px);">
  <img src="ninja5.png" alt="ninja test" style="display:block; width:100%; height:auto;">
  <figcaption style="font-size:85%; color:#666; line-height:1.4; margin-top:0.4em;">
    <em>Stealth ops require snacks</em>
  </figcaption>
</figure>
<p>Your paragraph text starts here and will wrap to the right of the figure on wide screens.
Keep writing as normal — the float handles the flow.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod sem at felis scelerisque, at ultrices purus pellentesque. Praesent sit amet turpis non leo suscipit laoreet. Vivamus interdum sapien ut risus vulputate, non congue justo vestibulum. Nunc volutpat dui ac magna cursus, in pulvinar magna gravida. Nulla facilisi. Phasellus vel dui ut libero facilisis sollicitudin vel ac sapien. Etiam posuere velit nec orci viverra, sit amet ullamcorper elit fermentum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod sem at felis scelerisque, at ultrices purus pellentesque. Praesent sit amet turpis non leo suscipit laoreet. Vivamus interdum sapien ut risus vulputate, non congue justo vestibulum. Nunc volutpat dui ac magna cursus, in pulvinar magna gravida. Nulla facilisi. Phasellus vel dui ut libero facilisis sollicitudin vel ac sapien. Etiam posuere velit nec orci viverra, sit amet ullamcorper elit fermentum.</p>
<!-- When you want to stop wrapping and return to normal layout: -->
<div style="clear:both"></div>
<h2 id="make-these-headings-smaller">Make these headings smaller</h2>
<p>This sections is for testing head weights/size tweaks.</p>
<h3 id="subheading">subheading</h3>
<h4 id="sub-subheading">sub-subheading</h4>
<ul>
<li>
<h2 id="heading">Heading</h2>
<ul>
<li>
<h3 id="subheading-1">Subheading</h3>
<ul>
<li>
<h4 id="sub-subheading-1">Sub-Subheading</h4>
</li>
<li>
<h4 id="sub-subheading-2">Sub-Subheading</h4>
</li>
</ul>
</li>
<li>
<h3 id="another-subheading">Another Subheading</h3>
</li>
</ul>
</li>
</ul>
<h2 id="dangling-sections-remove-this-to-see">Dangling sections (remove this to see)</h2>
<p>test lorem ipsum lorem ipsum dolor sit amet
test test test test</p>
<h2 id="custom-span-sizes">Custom span sizes</h2>
<p style="font-size:0.85em;">This is a slightly smaller sentence. (0.85em)</p>
<p style="font-size:1em;">This is the normal base size. (1em)</p>
<p style="font-size:1.2em;">This is a little larger for subtle emphasis. (1.2em)</p>
<p style="font-size:1.5em;">This is noticeably larger for a punchy effect. (1.5em)</p>
<p style="font-size:2em;">This is huge. Ninja mode activated. (2em)</p>
<h2 id="font-sampler">Font Sampler</h2>
<h3 id="sans-serif">Sans-Serif</h3>
<!-- Roboto Samples -->
<p style="font-family:Roboto,Arial,sans-serif;font-size:1.15em;font-weight:400;">
  Roboto Regular 400:<br>
  The quick brown fox jumps over the lazy dog.
</p>
<p style="font-family:Roboto,Arial,sans-serif;font-size:1.15em;font-weight:700;">
  Roboto Bold 700:<br>
  The quick brown fox jumps over the lazy dog.
</p>
<p style="font-family:Roboto,Arial,sans-serif;font-size:1.15em;font-style:italic;">
  Roboto Italic:<br>
  The quick brown fox jumps over the lazy dog.
</p>
<p style="font-family:Roboto,Arial,sans-serif;font-size:1.15em;font-weight:700;font-style:italic;">
  Roboto Bold Italic:<br>
  The quick brown fox jumps over the lazy dog.
</p>
<!-- Karla Samples -->
<p style="font-family:Karla,Arial,sans-serif;font-size:1.15em;font-weight:400;">
  Karla Regular 400:<br>
  The quick brown fox jumps over the lazy dog.
</p>
<p style="font-family:Karla,Arial,sans-serif;font-size:1.15em;font-weight:700;">
  Karla Bold 700:<br>
  The quick brown fox jumps over the lazy dog.
</p>
<p style="font-family:Karla,Arial,sans-serif;font-size:1.15em;font-style:italic;">
  Karla Italic:<br>
  The quick brown fox jumps over the lazy dog.
</p>
<p style="font-family:Karla,Arial,sans-serif;font-size:1.15em;font-weight:700;font-style:italic;">
  Karla Bold Italic:<br>
  The quick brown fox jumps over the lazy dog.
</p>
<h3 id="serif-font-sampler">Serif Font Sampler</h3>
<p style="font-family:Merriweather,Georgia,serif;font-size:1.15em;">
  Merriweather Regular:<br>
  The quick brown fox jumps over the lazy dog.
</p>
<p style="font-family:Merriweather,Georgia,serif;font-weight:700;font-size:1.15em;">
  Merriweather Bold:<br>
  The quick brown fox jumps over the lazy dog.
</p>
<p style="font-family:Merriweather,Georgia,serif;font-style:italic;font-size:1.15em;">
  Merriweather Italic:<br>
  The quick brown fox jumps over the lazy dog.
</p>
<p style="font-family:Merriweather,Georgia,serif;font-weight:700;font-style:italic;font-size:1.15em;">
  Merriweather Bold Italic:<br>
  The quick brown fox jumps over the lazy dog.
</p>
<h2 id="typography-playground">Typography Playground</h2>
<p><span style="color:#b3e5fc;">Bright cyan sample text</span><br>
<span style="background:#fff8b5;">Yellow highlight (for light mode)</span><br>
<span style="background:#222;color:#ffe082;">High contrast highlight (for dark mode)</span><br></p>
<p><strong>Letter Spacing</strong></p>
<p style="letter-spacing:0.07em;">S P A C E D &nbsp; T E X T (0.07em letter-spacing)</p>
<p style="letter-spacing:-0.03em;">Compressed text (-0.03em letter-spacing)</p>
<p><strong>Line Height</strong></p>
<p style="line-height:1.1;">Tight lines (line-height: 1.1)<br>The quick brown fox jumps over the lazy dog.</p>
<p style="line-height:1.6;">Relaxed lines (line-height: 1.6)<br>The quick brown fox jumps over the lazy dog.</p>
<p><strong>Blockquote</strong></p>
<blockquote>
<p>“Typography is the craft of endowing human language with a durable visual form.”<br>
—Robert Bringhurst</p></blockquote>
<p>for custom style:</p>
<blockquote style="font-family:Karla,serif;font-size:1.2em;color:#7d83b9;border-left:4px solid #b3e5fc;padding-left:1em;">
  “Typography is the craft of endowing human language with a durable visual form.”  
  <br><span style="font-size:0.9em;">—Robert Bringhurst</span>
</blockquote>
<p><strong>Monospace / Code</strong>
This is <code>inline code</code> using the site’s monospace font.</p>
<pre><code style="font-family:monospace;">pre/code block — Roboto Mono or system monospace</code></pre>
<p><strong>Small Caps</strong></p>
<p style="font-variant:small-caps;">Small Caps Text Example</p>
<p>Superscript &amp; Subscript
This is E = mc<sup>2</sup> and H<sub>2</sub>O</p>
<h3 id="katex-math">KaTeX math:</h3>
<p>Why is this not working correctly? KaTeX is gorgeous and should work here:</p>
<p>This is Euler&rsquo;s identity: $e^{i\pi} + 1 = 0$</p>
$$
\int_{-\infty}^\infty e^{-x^2} dx = \sqrt{\pi}
$$<p>$\therefore$</p>
<hr>
<h2 id="color-tag-shortcode-examples">Color Tag Shortcode Examples</h2>
<p>Testing <code>tag.html</code> shortcode:</p>
<ul>
<li><span class="tag green">Python</span></li>
<li><span class="tag blue">CLI</span></li>
<li><span class="tag orange">Draft</span></li>
<li><span class="tag gray">WIP</span></li>
<li><span class="tag red">Secret Ninja Mode</span></li>
</ul>
<p><strong>With combos:</strong></p>
<ul>
<li><span class="tag purple tag-large">Beta</span> next to <span class="tag blue tag-small">Mini</span></li>
</ul>
<p><strong>Pills:</strong></p>
<ul>
<li>
<span class="tag green tag-pill">Python</span>
</li>
<li>
<span class="tag blue tag-pill">CLI</span>
</li>
<li>
<span class="tag orange tag-pill">Draft</span>
</li>
<li>
<span class="tag gray tag-pill">WIP</span>
</li>
<li>
<span class="tag red tag-pill">Secret Ninja Mode</span>
</li>
<li>
<span class="tag red tag-small">Small pill</span>
</li>
<li>
<span class="tag orange">👉</span>
</li>
<li>
<span class="tag red">👉</span>
</li>
<li>
<span class="tag green tag-pill">👉</span>
</li>
</ul>
<h3 id="color-picker-tool">Color Picker Tool</h3>
<p><span class="tag red">Check out</span> the <a href="/color-picker.html">Color Picker</a> tool</p>
<p><span style="color:#7D83B9"> Make it easy to use color wherever you want!</span></p>
<p><span style="color:#00FF00"> Make it easy to use color wherever you want!</span></p>
<h2 id="images">Images</h2>
<p>Make an image into a link:
<a href="https://example.com"><img alt="Alt text" loading="lazy" src="/posts/ninjas/ninja1.gif"></a></p>
<p>Shadow ninja
<figure class="shadowed">
    <img loading="lazy" src="ninja1.gif"/> <figcaption>
            Shadow Ninja
        </figcaption>
</figure>
</p>
<p>Broken link behavior
<img alt="This is the way" loading="lazy" src="source.gif"></p>
<p>Animated gif
<img alt="Pfffffft" loading="lazy" src="/posts/ninjas/Pffffft.gif"></p>
<hr>
<p align="center">
  <img src="adminjitsu.gif" alt="ninja" width="220">
Ninja time!
</p>
<hr>
<p align="right">
  <img src="ninja2.gif" alt="ninja" width="220">
Ninja time!
</p>
<p><img alt="Ninja #2" loading="lazy" src="/posts/ninjas/ninja2.gif" title="Ninja #2 on the move">
<img alt="Ninja #3" loading="lazy" src="/posts/ninjas/ninja3.gif" title="Ninja #3 strikes again">
<img alt="Tacos!" loading="lazy" src="/posts/ninjas/tacos.gif" title="Delicious tacos for ninjas"></p>
<h2 id="shortcodes-demo">Shortcodes demo</h2>
<p>youtube video-id
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/dQw4w9WgXcQ?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>
</p>
<p>Highlight
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;NINJA ✅&#34;</span></span></span></code></pre></div></p>
<div style="background:#f0f0f0;padding:8px;">Raw HTML block!</div>
<h3 id="collapseaccordion-papermod">Collapse/Accordion (PaperMod)</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl"><details >
  <summary>Details</summary>
  <div class="collapse-body">
    <ul>
<li>Always test on staging first.</li>
<li>Monitor logs after every deploy.</li>
<li>Trust, but verify: <code>diff</code> is your friend.</li>
</ul>

  </div>
</details>

</span></span></code></pre></div><h3 id="collapsed-codeblock-example">COLLAPSED codeblock example</h3>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">┌──[ grumble@shinobi ]:~/codelab/adminjitsu  (main*)
</span></span><span class="line"><span class="cl">└─$ cat ../dotfiles/README.md | wordstats --top 10
</span></span><span class="line"><span class="cl">word         | count
</span></span><span class="line"><span class="cl">-------------+------
</span></span><span class="line"><span class="cl">bash         | 10
</span></span><span class="line"><span class="cl">dotfiles     | 8
</span></span><span class="line"><span class="cl">shell        | 8
</span></span><span class="line"><span class="cl">system       | 6
</span></span><span class="line"><span class="cl">arsenal      | 6
</span></span><span class="line"><span class="cl">setup        | 5
</span></span><span class="line"><span class="cl">youre        | 5
</span></span><span class="line"><span class="cl">hostspecific | 5
</span></span><span class="line"><span class="cl">bootstrapsh  | 5
</span></span><span class="line"><span class="cl">sourcing     | 5
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Total words:      685
</span></span><span class="line"><span class="cl">Unique words:     378
</span></span><span class="line"><span class="cl">Filtered words:   515
</span></span><span class="line"><span class="cl">Character count:  4480
</span></span><span class="line"><span class="cl">Avg word length:  6.54
</span></span><span class="line"><span class="cl">Longest word:     stringusersyournamedotfilesbinarsenalupdatestring (49)
</span></span><span class="line"><span class="cl">Shortest word:    a (1)</span></span></code></pre></div>

  </div>
</details>

<hr>
<p>Text before image&hellip; 

<img class="in-text" height="80" src="ninja2.gif" alt="Ninja Cat">
 &hellip;and after.</p>
<p>Highlight bash block
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo systemctl restart hugo
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Site deployed by ninja&#34;</span></span></span></code></pre></div></p>

      <div
          style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
        <iframe
          src="https://player.vimeo.com/video/146022717?dnt=0"
            style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allow="fullscreen">
        </iframe>
      </div>

<blockquote
    class="instagram-media"
    data-instgrm-captioned
    data-instgrm-permalink="https://www.instagram.com/p/BWNjjyYFxVx"
    data-instgrm-version="14"
    style="
      background: #fff;
      border: 0;
      border-radius: 3px;
      box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.5), 0 1px 10px 0 rgba(0, 0, 0, 0.15);
      margin: 1px;
      max-width: 540px;
      min-width: 326px;
      padding: 0;
      width: 99.375%;
      width: -webkit-calc(100% - 2px);
      width: calc(100% - 2px);
    "
  >
    <div style="padding: 16px">
      <a
        href="https://www.instagram.com/p/BWNjjyYFxVx"
        style="
          background: #ffffff;
          line-height: 0;
          padding: 0 0;
          text-align: center;
          text-decoration: none;
          width: 100%;
        "
        target="_blank"
      >
        <div style="display: flex; flex-direction: row; align-items: center">
          <div
            style="
              background-color: #f4f4f4;
              border-radius: 50%;
              flex-grow: 0;
              height: 40px;
              margin-right: 14px;
              width: 40px;
            "
          ></div>
          <div
            style="
              display: flex;
              flex-direction: column;
              flex-grow: 1;
              justify-content: center;
            "
          >
            <div
              style="
                background-color: #f4f4f4;
                border-radius: 4px;
                flex-grow: 0;
                height: 14px;
                margin-bottom: 6px;
                width: 100px;
              "
            ></div>
            <div
              style="
                background-color: #f4f4f4;
                border-radius: 4px;
                flex-grow: 0;
                height: 14px;
                width: 60px;
              "
            ></div>
          </div>
        </div>
        <div style="padding: 19% 0"></div>
        <div
          style="display: block; height: 50px; margin: 0 auto 12px; width: 50px"
        >
          <svg
            width="50px"
            height="50px"
            viewBox="0 0 60 60"
            version="1.1"
            xmlns="https://www.w3.org/2000/svg"
            xmlns:xlink="https://www.w3.org/1999/xlink"
          >
            <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
              <g transform="translate(-511.000000, -20.000000)" fill="#000000">
                <g>
                  <path
                    d="M556.869,30.41 C554.814,30.41 553.148,32.076 553.148,34.131 C553.148,36.186 554.814,37.852 556.869,37.852 C558.924,37.852 560.59,36.186 560.59,34.131 C560.59,32.076 558.924,30.41 556.869,30.41 M541,60.657 C535.114,60.657 530.342,55.887 530.342,50 C530.342,44.114 535.114,39.342 541,39.342 C546.887,39.342 551.658,44.114 551.658,50 C551.658,55.887 546.887,60.657 541,60.657 M541,33.886 C532.1,33.886 524.886,41.1 524.886,50 C524.886,58.899 532.1,66.113 541,66.113 C549.9,66.113 557.115,58.899 557.115,50 C557.115,41.1 549.9,33.886 541,33.886 M565.378,62.101 C565.244,65.022 564.756,66.606 564.346,67.663 C563.803,69.06 563.154,70.057 562.106,71.106 C561.058,72.155 560.06,72.803 558.662,73.347 C557.607,73.757 556.021,74.244 553.102,74.378 C549.944,74.521 548.997,74.552 541,74.552 C533.003,74.552 532.056,74.521 528.898,74.378 C525.979,74.244 524.393,73.757 523.338,73.347 C521.94,72.803 520.942,72.155 519.894,71.106 C518.846,70.057 518.197,69.06 517.654,67.663 C517.244,66.606 516.755,65.022 516.623,62.101 C516.479,58.943 516.448,57.996 516.448,50 C516.448,42.003 516.479,41.056 516.623,37.899 C516.755,34.978 517.244,33.391 517.654,32.338 C518.197,30.938 518.846,29.942 519.894,28.894 C520.942,27.846 521.94,27.196 523.338,26.654 C524.393,26.244 525.979,25.756 528.898,25.623 C532.057,25.479 533.004,25.448 541,25.448 C548.997,25.448 549.943,25.479 553.102,25.623 C556.021,25.756 557.607,26.244 558.662,26.654 C560.06,27.196 561.058,27.846 562.106,28.894 C563.154,29.942 563.803,30.938 564.346,32.338 C564.756,33.391 565.244,34.978 565.378,37.899 C565.522,41.056 565.552,42.003 565.552,50 C565.552,57.996 565.522,58.943 565.378,62.101 M570.82,37.631 C570.674,34.438 570.167,32.258 569.425,30.349 C568.659,28.377 567.633,26.702 565.965,25.035 C564.297,23.368 562.623,22.342 560.652,21.575 C558.743,20.834 556.562,20.326 553.369,20.18 C550.169,20.033 549.148,20 541,20 C532.853,20 531.831,20.033 528.631,20.18 C525.438,20.326 523.257,20.834 521.349,21.575 C519.376,22.342 517.703,23.368 516.035,25.035 C514.368,26.702 513.342,28.377 512.574,30.349 C511.834,32.258 511.326,34.438 511.181,37.631 C511.035,40.831 511,41.851 511,50 C511,58.147 511.035,59.17 511.181,62.369 C511.326,65.562 511.834,67.743 512.574,69.651 C513.342,71.625 514.368,73.296 516.035,74.965 C517.703,76.634 519.376,77.658 521.349,78.425 C523.257,79.167 525.438,79.673 528.631,79.82 C531.831,79.965 532.853,80.001 541,80.001 C549.148,80.001 550.169,79.965 553.369,79.82 C556.562,79.673 558.743,79.167 560.652,78.425 C562.623,77.658 564.297,76.634 565.965,74.965 C567.633,73.296 568.659,71.625 569.425,69.651 C570.167,67.743 570.674,65.562 570.82,62.369 C570.966,59.17 571,58.147 571,50 C571,41.851 570.966,40.831 570.82,37.631"
                  ></path>
                </g>
              </g>
            </g>
          </svg>
        </div>
        <div style="padding-top: 8px">
          <div
            style="
              color: #3897f0;
              font-family: Arial, sans-serif;
              font-size: 14px;
              font-style: normal;
              font-weight: 550;
              line-height: 18px;
            "
          >
            View this post on Instagram
          </div>
        </div>
        <div style="padding: 12.5% 0"></div>
        <div
          style="
            display: flex;
            flex-direction: row;
            margin-bottom: 14px;
            align-items: center;
          "
        >
          <div>
            <div
              style="
                background-color: #f4f4f4;
                border-radius: 50%;
                height: 12.5px;
                width: 12.5px;
                transform: translateX(0px) translateY(7px);
              "
            ></div>
            <div
              style="
                background-color: #f4f4f4;
                height: 12.5px;
                transform: rotate(-45deg) translateX(3px) translateY(1px);
                width: 12.5px;
                flex-grow: 0;
                margin-right: 14px;
                margin-left: 2px;
              "
            ></div>
            <div
              style="
                background-color: #f4f4f4;
                border-radius: 50%;
                height: 12.5px;
                width: 12.5px;
                transform: translateX(9px) translateY(-18px);
              "
            ></div>
          </div>
          <div style="margin-left: 8px">
            <div
              style="
                background-color: #f4f4f4;
                border-radius: 50%;
                flex-grow: 0;
                height: 20px;
                width: 20px;
              "
            ></div>
            <div
              style="
                width: 0;
                height: 0;
                border-top: 2px solid transparent;
                border-left: 6px solid #f4f4f4;
                border-bottom: 2px solid transparent;
                transform: translateX(16px) translateY(-4px) rotate(30deg);
              "
            ></div>
          </div>
          <div style="margin-left: auto">
            <div
              style="
                width: 0px;
                border-top: 8px solid #f4f4f4;
                border-right: 8px solid transparent;
                transform: translateY(16px);
              "
            ></div>
            <div
              style="
                background-color: #f4f4f4;
                flex-grow: 0;
                height: 12px;
                width: 16px;
                transform: translateY(-4px);
              "
            ></div>
            <div
              style="
                width: 0;
                height: 0;
                border-top: 8px solid #f4f4f4;
                border-left: 8px solid transparent;
                transform: translateY(-4px) translateX(8px);
              "
            ></div>
          </div>
        </div>
        <div
          style="
            display: flex;
            flex-direction: column;
            flex-grow: 1;
            justify-content: center;
            margin-bottom: 24px;
          "
        >
          <div
            style="
              background-color: #f4f4f4;
              border-radius: 4px;
              flex-grow: 0;
              height: 14px;
              margin-bottom: 6px;
              width: 224px;
            "
          ></div>
          <div
            style="
              background-color: #f4f4f4;
              border-radius: 4px;
              flex-grow: 0;
              height: 14px;
              width: 144px;
            "
          ></div></div
      ></a>
    </div>
  </blockquote><script async src="https://www.instagram.com/embed.js"></script>
<hr>
<h2 id="some-boilerplate">Some boilerplate</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">[<span class="nt">![Alt text</span>](<span class="na">image.png</span>)](https://example.com)
</span></span></code></pre></div><p class="github-btn">
  <a href="https://github.com/forfaxx/qrgen" target="_blank">
    🔗 View qrgen on GitHub
  </a>
</p>
<p>I hope you found this little playground interesting. I use it to test out style changes, to copy and paste glyphs and tricky syntax and it see how images and links render in the site. If you have any feedback or ideas, please feel free to reach out.</p>
<p>Email me:
<a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
<hr>
<blockquote>
<p><em>tell me, teach me, help me or do it for me.</em></p></blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>repeat()</title>
      <link>https://adminjitsu.com/posts/repeat-repeat-repeat/</link>
      <pubDate>Mon, 21 Jul 2025 06:49:47 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/repeat-repeat-repeat/</guid>
      <description>This post introduces a handy Bash (and POSIX) shell function that lets you repeat any command a set number of times. Great for quickly testing scripts, generating random output, or automating repeated tasks.</description>
      <content:encoded><![CDATA[<p>I created a repeat function that is ridiculously useful for testing programs that produce variable output.</p>
<h2 id="the-function">the function</h2>
<p>add this to your bash shell either by pasting as a block into your terminal and then typing the function name to run, or add it to the appropriate dotfile to make it permanently available.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># this function will run a command multiple times, eg repeat 10 gibberish uuid or repeat 5 fortune</span>
</span></span><span class="line"><span class="cl">repeat<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> -z <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="o">||</span> ! <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="o">=</span>~ ^<span class="o">[</span>0-9<span class="o">]</span>+$ <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;Usage: repeat &lt;count&gt; &lt;command&gt; [args...]&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> <span class="nv">count</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">shift</span>
</span></span><span class="line"><span class="cl">  <span class="k">for</span> i in <span class="k">$(</span>seq <span class="s2">&#34;</span><span class="nv">$count</span><span class="s2">&#34;</span><span class="k">)</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;</span><span class="nv">$@</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">done</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><h2 id="usage">usage</h2>
<p>to use, invoke with repeat and a numerical argument. i.e.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">┌──<span class="o">[</span> grumble@shinobi <span class="o">]</span>:~
</span></span><span class="line"><span class="cl">└─$ repeat <span class="m">5</span> gibberish numbers
</span></span><span class="line"><span class="cl"><span class="m">84451182630363300096</span>
</span></span><span class="line"><span class="cl"><span class="m">77401767437641923164</span>
</span></span><span class="line"><span class="cl"><span class="m">42951978581655852424</span>
</span></span><span class="line"><span class="cl"><span class="m">06803068919499777616</span>
</span></span><span class="line"><span class="cl"><span class="m">43251993640144857502</span>
</span></span></code></pre></div><p>or</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">┌──<span class="o">[</span> grumble@shinobi <span class="o">]</span>:~
</span></span><span class="line"><span class="cl">└─$ repeat <span class="m">2</span> uuidgen
</span></span><span class="line"><span class="cl">547f5194-0b61-4f7b-9274-cab9ad1fd8ec
</span></span><span class="line"><span class="cl">0e535674-0857-4172-a366-63b2ef1fa39d
</span></span></code></pre></div><p>This is useful for programs that produce variable or randomized output where running
multiple times would make sense. Kind of a sibling to meta-execution and orchestration tools like watch, inotify, fswatch, xargs and more.</p>
<h2 id="notes">notes</h2>
<p>The code above works on modern bash and zsh shells. For a POSIX-compliant, .sh version you can use this older but still functionally equivalent version.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">repeat<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> -z <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;Usage: repeat &lt;count&gt; &lt;command&gt; [args...]&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">case</span> <span class="nv">$1</span> in
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;&#39;</span><span class="p">|</span>*<span class="o">[</span>!0-9<span class="o">]</span>*<span class="o">)</span> <span class="nb">echo</span> <span class="s2">&#34;Count must be a positive integer.&#34;</span> &gt;<span class="p">&amp;</span>2<span class="p">;</span> <span class="k">return</span> 1<span class="p">;;</span>
</span></span><span class="line"><span class="cl">  <span class="k">esac</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nv">count</span><span class="o">=</span><span class="nv">$1</span>
</span></span><span class="line"><span class="cl">  <span class="nb">shift</span>
</span></span><span class="line"><span class="cl">  <span class="nv">i</span><span class="o">=</span><span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">while</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$i</span><span class="s2">&#34;</span> -le <span class="s2">&#34;</span><span class="nv">$count</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;</span><span class="nv">$@</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nv">i</span><span class="o">=</span><span class="sb">`</span>expr <span class="nv">$i</span> + 1<span class="sb">`</span>
</span></span><span class="line"><span class="cl">  <span class="k">done</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><p>I prefer the cleaner modern bash idioms, but there you go!</p>
]]></content:encoded>
    </item>
    <item>
      <title>mkcd()</title>
      <link>https://adminjitsu.com/posts/a-mkcd-function/</link>
      <pubDate>Mon, 21 Jul 2025 00:00:00 +0000</pubDate>
      <guid>https://adminjitsu.com/posts/a-mkcd-function/</guid>
      <description>Make your shell smarter! This post introduces mkcd—a function to create and immediately enter new directories safely, with friendly feedback and a simple usage pattern</description>
      <content:encoded><![CDATA[<h3 id="the-function">the function</h3>
<p>This little mkcd function feels like it should be a standard /bin tool.
It makes a directory and changes directory into it with a couple of safeties.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># create and cd into a new directory (safely)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Usage: mkcd &lt;directory-name&gt;</span>
</span></span><span class="line"><span class="cl">mkcd<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">$1</span><span class="s2">&#34;</span> <span class="o">==</span> *<span class="se">\*</span>* <span class="o">||</span> <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="o">==</span> *<span class="se">\?</span>* <span class="o">||</span> <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="o">==</span> *<span class="se">\[</span>*<span class="se">\]</span>* <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;❌ Globs not supported—please provide a single directory name.&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[[</span> <span class="nv">$#</span> -gt <span class="m">1</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;⚠️ Multiple directory names given—using the first: </span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[[</span> -z <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;📁 Usage: mkcd &lt;directory-name&gt;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  mkdir -p <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="o">&amp;&amp;</span> <span class="nb">cd</span> <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <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;❌ Failed to enter directory: </span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><h3 id="usage">usage</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">mkcd folder-to-create
</span></span><span class="line"><span class="cl">mkcd folder/subfolder/subfolder2
</span></span></code></pre></div><p>Just add to your startup files (e.g., <code>.bashrc</code>, <code>.zshrc</code>) and reload; you&rsquo;ll be set.</p>
]]></content:encoded>
    </item>
    <item>
      <title>notifications for everyone</title>
      <link>https://adminjitsu.com/posts/notifications-for-everyone/</link>
      <pubDate>Mon, 21 Jul 2025 00:00:00 +0000</pubDate>
      <guid>https://adminjitsu.com/posts/notifications-for-everyone/</guid>
      <description>a simple, cross-platform system notification function</description>
      <content:encoded><![CDATA[<p>In my work, I often jump between a variety of machines. I have increasingly begun to rely on simple interfaces that work cross-platform in my environment. It makes these scriptable and usable no matter where you are.</p>
<h2 id="notify">notify()</h2>
<p>One such example is this notify function. It allows you to call one function to display system notifications. Your mileage may vary, but it&rsquo;s worked well for me.</p>
<p>Just add the following to your shell startup files and reload..</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># cross platform notification system</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Usage: notify &#34;Title&#34; &#34;Message&#34;</span>
</span></span><span class="line"><span class="cl">notify<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">local</span> <span class="nv">title</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">1</span><span class="k">:-</span><span class="s2">&#34;Notification&#34;</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">local</span> <span class="nv">message</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">2</span><span class="k">:-</span><span class="si">}</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"># macOS</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nb">command</span> -v osascript &gt;/dev/null 2&gt;<span class="p">&amp;</span>1<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  osascript -e <span class="s2">&#34;display notification \&#34;</span><span class="nv">$message</span><span class="s2">\&#34; with title \&#34;</span><span class="nv">$title</span><span class="s2">\&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># WSL: Detect *first* before generic Linux!</span>
</span></span><span class="line"><span class="cl"><span class="k">elif</span> grep -qi microsoft /proc/version 2&gt;/dev/null <span class="o">&amp;&amp;</span> <span class="nb">command</span> -v powershell.exe &gt;/dev/null 2&gt;<span class="p">&amp;</span>1<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    powershell.exe -NoProfile -Command <span class="s2">&#34;Import-Module BurntToast; New-BurntToastNotification -Text \&#34;</span><span class="si">${</span><span class="nv">title</span><span class="si">}</span><span class="s2">\&#34;, \&#34;</span><span class="si">${</span><span class="nv">message</span><span class="si">}</span><span class="s2">\&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Linux/Unix with notify-send</span>
</span></span><span class="line"><span class="cl"><span class="k">elif</span> <span class="nb">command</span> -v notify-send &gt;/dev/null 2&gt;<span class="p">&amp;</span>1<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  notify-send <span class="s2">&#34;</span><span class="nv">$title</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$message</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"># Windows native Git Bash, msg.exe</span>
</span></span><span class="line"><span class="cl"><span class="k">elif</span> <span class="nb">command</span> -v msg.exe &gt;/dev/null 2&gt;<span class="p">&amp;</span>1<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  msg.exe * <span class="s2">&#34;</span><span class="nv">$title</span><span class="s2">: </span><span class="nv">$message</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"># Fallback to wall</span>
</span></span><span class="line"><span class="cl"><span class="k">elif</span> <span class="nb">command</span> -v wall &gt;/dev/null 2&gt;<span class="p">&amp;</span>1<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$title</span><span class="s2">: </span><span class="nv">$message</span><span class="s2">&#34;</span> <span class="p">|</span> wall
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$title</span><span class="s2">: </span><span class="nv">$message</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><h2 id="usage">Usage</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">notify <span class="s2">&#34;Title&#34;</span> <span class="s2">&#34;This is a test of the Emergency Broadcast System&#34;</span>
</span></span></code></pre></div><p>On my systems, this pops up a system notification. The one catch is that on modern Macs you must enable terminal notifications by opening Script Editor one time and running the following:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">display notification <span class="s2">&#34;Test notification&#34;</span> with title <span class="s2">&#34;Script Editor&#34;</span>
</span></span></code></pre></div><p>This will prompt you (with a notification) to allow terminal notifications.</p>
<h2 id="more-interfaces">More interfaces</h2>
<p>I&rsquo;m still developing these but the following are pretty handy as well.</p>
<h3 id="say-command">say command</h3>
<p>Since I split my shell startup into multiple files, I define the say command so it is present on linux and works like on the mac. Of course espeak voices are not as nice as on mac but they do have their place as another kind of notification or even a toy.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># macOS-style `say` command on Linux using espeak</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="k">$(</span>uname -s<span class="k">)</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;Linux&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">command</span> -v espeak &gt;/dev/null<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">alias</span> <span class="nv">say</span><span class="o">=</span><span class="s1">&#39;espeak -s 160 -p 60 -v en-us+f2&#39;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span></code></pre></div><p>You&rsquo;ll need to install espeak with your preferred package manager. For apt it&rsquo;s just</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">apt install espeak 
</span></span></code></pre></div><h3 id="man2pdf">man2pdf</h3>
<p>This is one I have used on mac for years.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Convert man page to PDF (macOS only)</span>
</span></span><span class="line"><span class="cl">man2pdf<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  man -t <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="p">|</span> open -f -a /Applications/Preview.app
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><p>Usage:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">man2pdf grep 
</span></span></code></pre></div><p>This will launch a man page in Preview as an attractive pdf. I missed that command on linux but it&rsquo;s as easy as doing the following:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Render man page as ps then as pdf using the ps2pdf application</span>
</span></span><span class="line"><span class="cl"><span class="c1"># functional equivalent to man2pdf on mac.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">man2pdf<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> <span class="nv">page</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">1</span><span class="p">:?Usage: man2pdf &lt;manpage&gt;</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> tmpfile
</span></span><span class="line"><span class="cl">  <span class="nv">tmpfile</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>mktemp --suffix<span class="o">=</span>.pdf /tmp/man2pdf-XXXXXX.pdf<span class="k">)</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"># Convert manpage to PDF</span>
</span></span><span class="line"><span class="cl">  man -t <span class="s2">&#34;</span><span class="nv">$page</span><span class="s2">&#34;</span> <span class="p">|</span> ps2pdf - <span class="s2">&#34;</span><span class="nv">$tmpfile</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Open the PDF using xdg-open (works on most desktops)</span>
</span></span><span class="line"><span class="cl">  xdg-open <span class="s2">&#34;</span><span class="nv">$tmpfile</span><span class="s2">&#34;</span> &gt;/dev/null 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="p">&amp;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><p>Same usage. easy peasy.</p>
<h3 id="clip-and-paste">clip and paste</h3>
<p>Another really useful cross platform command is clip and paste. Having predictable interfaces like this is definitely adminjitsu.</p>
<p>On Mac</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># macOS clipboard</span>
</span></span><span class="line"><span class="cl"><span class="c1"># for consistency with linux environment</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">clip</span><span class="o">=</span><span class="s1">&#39;pbcopy&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">paste</span><span class="o">=</span><span class="s1">&#39;pbpaste&#39;</span>
</span></span></code></pre></div><p>On Linux</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Linux clipboard (assumes xclip is installed)</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">clip</span><span class="o">=</span><span class="s1">&#39;xclip -selection clipboard&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">paste</span><span class="o">=</span><span class="s1">&#39;xclip -selection clipboard -o&#39;</span>
</span></span></code></pre></div><p>You&rsquo;ll need to install xclip with your preferred package manager. For apt it&rsquo;s just</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">sudo apt install xclip 
</span></span></code></pre></div><p>That is a really powerful command that allows you to do things like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">find . -type f ! -path <span class="s2">&#34;*/.git/*&#34;</span> ! -path <span class="s2">&#34;*/venv/*&#34;</span> <span class="p">|</span> clip 
</span></span><span class="line"><span class="cl">paste
</span></span></code></pre></div><p>and now the output is on your system clipboard and ready to paste anywhere.</p>
<h2 id="conclusion">Conclusion</h2>
<p>These are just a few small examples of a concept I am really starting to embrace. I look forward to adding more as time goes on!</p>
]]></content:encoded>
    </item>
    <item>
      <title>ps for spelunkers</title>
      <link>https://adminjitsu.com/posts/ps-for-spelunkers/</link>
      <pubDate>Mon, 21 Jul 2025 00:00:00 +0000</pubDate>
      <guid>https://adminjitsu.com/posts/ps-for-spelunkers/</guid>
      <description>handy ps aliases and bash functions for process monitoring and workflow deep dives</description>
      <content:encoded><![CDATA[<p>These are some of the <code>ps</code> aliases and functions I have found useful over the years.</p>
<h3 id="aliases">Aliases</h3>
<p>I&rsquo;ve always loved aliases. They make long and unmemorable commands memorable and they&rsquo;re a pretty good, self-documenting way to keep track of those Hero commands that do something great but that you would struggle to remember. Here are a few that I developed working with Cloudera java processes and complex web server setups.</p>
<p>The following show you the top 20 memory hogs. The first just sorts and limits the usual output. The pmem alias is a lot more useful for a quick view</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">ptop</span><span class="o">=</span><span class="s1">&#39;ps aux --sort=-%mem | head -n 20&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">pmem</span><span class="o">=</span><span class="s1">&#39;ps aux --sort=-%mem | head -n 20 | awk &#39;</span><span class="se">\&#39;</span><span class="s1">&#39;{print $1, $4, $11}&#39;</span><span class="se">\&#39;</span><span class="s1">&#39; | column -t&#39;</span>
</span></span></code></pre></div><p>Similarly, this one shows top cpu hogs</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">pscpu</span><span class="o">=</span><span class="s1">&#39;ps aux --sort=-%cpu | head -n 20 | awk &#39;</span><span class="se">\&#39;</span><span class="s1">&#39;{print $1, $3, $11}&#39;</span><span class="se">\&#39;</span><span class="s1">&#39; | column -t&#39;</span>
</span></span></code></pre></div><p>The forest view shows subprocesses in a friendly way</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">pfor</span><span class="o">=</span><span class="s1">&#39;ps auxf --forest&#39;</span>
</span></span></code></pre></div><p>Finally, what are my longest running proceses?</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">psme</span><span class="o">=</span><span class="s1">&#39;ps -u $(whoami) -o pid,etime,cmd --sort=-etime | head&#39;</span>
</span></span></code></pre></div><h2 id="bash-functions">bash functions</h2>
<p>Sometimes you want to do something that the ps program alone isn&rsquo;t capable of doing. For me it was displaying a space between each process. I
was working on a deep dive and after staring at wall after wall of grey dense grey text I found myself doing it manually in an output file. That&rsquo;s always a
sure sign that it&rsquo;s time to write a function if not a standalone script.</p>
<h3 id="pss-function">pss function</h3>
<p>This function inserts a blank line between processes. much more readable.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Runs ps and displays each process separated by a blank line to improve readability</span>
</span></span><span class="line"><span class="cl">pss<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  ps aux <span class="p">|</span> awk <span class="s1">&#39;1; { print &#34;&#34; }&#39;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><h3 id="psss-function">psss function</h3>
<p>This function provides more separation and uses color to make it even more readable. This one is probably better for deep dives than daily use but I have found it useful plenty of times.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Pretty print ps output, with optional filtering</span>
</span></span><span class="line"><span class="cl">psss<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> <span class="nv">filter</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">1</span><span class="k">:-</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;🧠 Showing processes</span><span class="si">${</span><span class="nv">filter</span><span class="p">:+ matching: </span><span class="s1">&#39;$filter&#39;</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;─────────────────────────────────────────────&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  ps auxww --sort<span class="o">=</span>-%mem <span class="p">|</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">[[</span> -n <span class="s2">&#34;</span><span class="nv">$filter</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">      grep -i --color<span class="o">=</span>always -- <span class="s2">&#34;</span><span class="nv">$filter</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      cat
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">  <span class="o">}</span> <span class="p">|</span> awk <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">    BEGIN {
</span></span></span><span class="line"><span class="cl"><span class="s1">      GREEN = &#34;\033[1;32m&#34;; BLUE = &#34;\033[1;34m&#34;; RED = &#34;\033[1;31m&#34;;
</span></span></span><span class="line"><span class="cl"><span class="s1">      YELLOW = &#34;\033[1;33m&#34;; CYAN = &#34;\033[1;36m&#34;; RESET = &#34;\033[0m&#34;;
</span></span></span><span class="line"><span class="cl"><span class="s1">    }
</span></span></span><span class="line"><span class="cl"><span class="s1">    {
</span></span></span><span class="line"><span class="cl"><span class="s1">      print GREEN &#34;USER:&#34; RESET &#34; &#34; $1;
</span></span></span><span class="line"><span class="cl"><span class="s1">      print BLUE &#34;PID:&#34; RESET &#34;  &#34; $2 &#34;  &#34; RED &#34;CPU:&#34; RESET &#34; &#34; $3 &#34;  &#34; YELLOW &#34;MEM:&#34; RESET &#34; &#34; $4;
</span></span></span><span class="line"><span class="cl"><span class="s1">      printf CYAN &#34;CMD:&#34; RESET &#34;  &#34;;
</span></span></span><span class="line"><span class="cl"><span class="s1">      for (i = 11; i &lt;= NF; ++i) printf &#34;%s &#34;, $i;
</span></span></span><span class="line"><span class="cl"><span class="s1">      print &#34;\n&#34;;
</span></span></span><span class="line"><span class="cl"><span class="s1">    }
</span></span></span><span class="line"><span class="cl"><span class="s1">  &#39;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><p>It&rsquo;s output looks like this:</p>
<p><img alt="psss output" loading="lazy" src="/posts/ps-for-spelunkers/psss-output.png"></p>
<hr>
<h2 id="more-tips-and-tricks">More tips and tricks</h2>
<h3 id="pick-the-columns-you-actually-want">Pick the columns you actually want</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Linux (procps):              # macOS/BSD:</span>
</span></span><span class="line"><span class="cl">ps -eo user,pid,ppid,%cpu,%mem,stat,etime,start,tty,pri,ni,rss,vsz,comm <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>   --sort<span class="o">=</span>-%mem
</span></span><span class="line"><span class="cl"><span class="c1"># macOS uses -axo and different names (etime/start are fine):</span>
</span></span><span class="line"><span class="cl">ps -axo user,pid,ppid,%cpu,%mem,state,etime,start,tty,pri,nice,rss,vsz,comm
</span></span></code></pre></div><ul>
<li><code>etime</code>: elapsed time</li>
<li><code>rss</code> vs <code>vsz</code>: resident (real RAM) vs virtual size</li>
<li><code>comm</code> = executable name only; <code>args</code> = full command line</li>
</ul>
<h3 id="job-control">Job control</h3>
<ul>
<li>Start in background: <code>cmd &amp;</code></li>
<li>Suspend foreground: <strong>Ctrl-Z</strong></li>
<li>List jobs: <code>jobs -l</code></li>
<li>Resume in background: <code>bg %1</code></li>
<li>Bring to foreground: <code>fg %1</code></li>
<li>Keep alive after logout: <code>disown %1</code> or <code>nohup cmd &amp;</code></li>
<li>Detach fully: <code>setsid cmd &gt;/dev/null 2&gt;&amp;1 &lt; /dev/null &amp;</code></li>
<li>Pause/Resume any PID: <code>kill -STOP &lt;pid&gt;</code> / <code>kill -CONT &lt;pid&gt;</code></li>
</ul>
<h3 id="complimentary-tools-adjacent-to-ps">Complimentary tools (adjacent to ps)</h3>
<ul>
<li>
<p><code>top</code>/<code>htop</code>/<code>atop</code> – interactivity, sort by CPU/MEM, filter fast.</p>
</li>
<li>
<p><code>lsof -p &lt;pid&gt;</code> – what files/sockets a process is using.</p>
</li>
<li>
<p><code>ss -pt</code> or <code>netstat -p</code> – who’s talking on the network (and which PID).</p>
</li>
<li>
<p><code>strace -p &lt;pid&gt;</code> / <code>dtruss -p &lt;pid&gt;</code> (mac) – syscall truth serum (dev environments).</p>
</li>
<li>
<p><code>/proc/&lt;pid&gt;</code> – Linux treasure trove (<code>status</code>, <code>cmdline</code>, <code>environ</code>, <code>fd/</code>).</p>
</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>Feedback? Email me: <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
<blockquote>
<p>May your zombie processes be few in number</p></blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>Hello World</title>
      <link>https://adminjitsu.com/posts/hello-world/</link>
      <pubDate>Tue, 01 Jul 2025 00:00:00 +0000</pubDate>
      <guid>https://adminjitsu.com/posts/hello-world/</guid>
      <description>The very first post on AdminJitsu — a little about why this blog exists, what’s coming, and why it matters.</description>
      <content:encoded><![CDATA[<h2 id="hello-world">Hello, World!</h2>
<p>This seemed like an appropriate first post for a blog about scripting and programming (and Unix in general) since that is the first program anyone learns to write in a computer language.</p>
<p>If I told you the frustrating, tedious story behind how this post&rsquo;s tags caused an enormous issue that broke the site and took hours to fix, you would appreciate why I chose the insane sysadmin as my mascot! Suffice to say, I&rsquo;m glad it&rsquo;s working finally.</p>
<p>I think we&rsquo;ve all found ourselves chasing down weird bugs that make us want to rip our hair out and pound on the keyboard in frustration. That&rsquo;s kind of why I created this page. I want to offer hard won and battle tested advice, tools that are actually friendly and robust and hopefully bring a smile to the frazzled sysadmin who loves this stuff anyway!</p>
<!--
<figure style="text-align:center; margin: 1em auto;">
  <img src="/images/insane-sysadmin.gif" alt="Insane sysadmin pixel art" style="display:block; margin:0 auto; width:246px;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #888; line-height:1.4; margin-top:0.4em;">
    me after a marathon coding sprint
  </figcaption>
</figure>
-->
<h2 id="hello-world-in-different-languages">Hello, World in Different Languages</h2>
<p>Here is Hello World in every language I have dabbled in for one reason or another. These days I tend to stick with Bash, Python and the occasional compiled language.</p>
<h3 id="basic">BASIC</h3>
<p>This was the language of my first computer, the Commodore 64!</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-basic" data-lang="basic"><span class="line"><span class="cl"><span class="nl">10</span><span class="w"> </span><span class="kr">PRINT</span><span class="w"> </span><span class="s2">&#34;HELLO, WORLD&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nl">20</span><span class="w"> </span><span class="kr">END</span>
</span></span></code></pre></div><h3 id="turbo-pascal">Turbo Pascal</h3>
<p>This was the first language I learned in school. Limited but kind of lovely.</p>
<pre tabindex="0"><code class="language-delphi" data-lang="delphi">program HelloWorld;
begin
  Writeln(&#39;Hello, World&#39;);
end.
</code></pre><p>For some reason Chroma refuses to format my pascal block correctly no matter what I do. So I&rsquo;ve decided to accept it as an example of <a href="https://en.wikipedia.org/wiki/Wabi-sabi">Wabi-sabi</a> and move on with my life. I once wrote a Cthulhu themed text adventure in Pascal that hit all the limits for file size, number of files and memory. Fun times!</p>
<figure style="display: block; text-align: center; margin: 1.5em auto;">
  <a href="https://archive.org/details/programming-your-own-adventure-games-in-pascal" target="_blank" rel="noopener noreferrer">
    <img src="pascal.jpg" alt="Programming Your Own Adventure Games in Pascal book cover" style="max-width: 320px; border-radius: 8px; box-shadow: 0 3px 8px rgba(0,0,0,0.3); margin: 0 auto;">
  </a>
  <figcaption style="font-size: 90%; font-style: italic; color: #555; margin-top: 0.5em;">
    <strong><a href="https://archive.org/details/programming-your-own-adventure-games-in-pascal" target="_blank" rel="noopener noreferrer" style="color: inherit; text-decoration: underline;">Programming Your Own Adventure Games in Pascal</a></strong><br> a fantastic vintage guide, now freely readable on the Internet Archive.
  </figcaption>
</figure>
<h3 id="bash">Bash</h3>
<p>The Swiss Army knife of Unix. A one-liner here can topple servers or automate your life!</p>
<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">echo</span> <span class="s2">&#34;Hello, World&#34;</span>
</span></span></code></pre></div><h3 id="perl">Perl</h3>
<p>The duct tape of the early web—messy and often unreadable. Perl was my first real scripting language</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-perl" data-lang="perl"><span class="line"><span class="cl"><span class="ch">#!/usr/bin/perl</span>
</span></span><span class="line"><span class="cl"><span class="k">print</span> <span class="s">&#34;Hello, World\n&#34;</span><span class="p">;</span>
</span></span></code></pre></div><h3 id="python">Python</h3>
<p>Where Perl is inscrutable, Python is elegant — I honestly thought it was pseudocode the first time I saw it. These days, it’s my primary language.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="ch">#!/usr/bin/env python3</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Hello, World&#34;</span><span class="p">)</span>
</span></span></code></pre></div><h3 id="c">C</h3>
<p>I mean Unix is written in C. Garbage collection, recursion and double linked-lists ahoy!</p>
<figure style="display: block; text-align: center; margin: 1.5em auto;">
  <img src="k-and-r.jpg" alt="The C Programming Language by Kernighan & Ritchie" style="max-width: 320px; border-radius: 8px; box-shadow: 0 3px 8px rgba(0,0,0,0.3); margin: 0 auto;">
  <figcaption style="font-size: 90%; font-style: italic; color: #555; margin-top: 0.5em;">
    <strong>Kernighan &amp; Ritchie</strong> — This book is effortlessly brilliant. Like Euclid's Elements for programming.
  </figcaption>
</figure>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;Hello, World</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="c-1">C++</h3>
<p>The language behind just about everything for a long time.</p>
<figure style="display: block; text-align: center; margin: 1.5em auto;">
  <img src="deitels-cpp.jpg" alt="C++ How to Program by H.M. Deitel and P.J. Deitel" style="max-width: 320px; border-radius: 8px; box-shadow: 0 3px 8px rgba(0,0,0,0.3); margin: 0 auto;">
  <figcaption style="font-size: 90%; font-style: italic; color: #555; margin-top: 0.5em;">
    <strong><em>C++ How to Program</em></strong> by H.M. Deitel & P.J. Deitel — a brilliant, and unforgettable classic of the '90s programming bookshelf.
  </figcaption>
</figure>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;iostream&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="k">using</span> <span class="k">namespace</span> <span class="n">std</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">&#34;Hello, World&#34;</span> <span class="o">&lt;&lt;</span> <span class="n">endl</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="java">Java</h3>
<p>Java powers everything from Minecraft and Spotify to Apache Hadoop to the NASDAQ to your home appliances. Not always fun to progam in but incredibly powerful and worth learning.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">public</span><span class="w"> </span><span class="kd">class</span> <span class="nc">HelloWorld</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="kd">static</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">main</span><span class="p">(</span><span class="n">String</span><span class="o">[]</span><span class="w"> </span><span class="n">args</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="s">&#34;Hello, World&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h3 id="ruby">Ruby</h3>
<p>I always joked that Ruby was a language for art majors. Elegantly difficult to follow.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="ch">#!/usr/bin/env ruby</span>
</span></span><span class="line"><span class="cl"><span class="nb">puts</span> <span class="s2">&#34;Hello, World&#34;</span>
</span></span></code></pre></div><h3 id="php">PHP</h3>
<p>The language that built half the internet. This was <strong>the</strong> language for cgi for a very long time. I never loved it but I always respected it.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="o">&lt;?</span><span class="nx">php</span>
</span></span><span class="line"><span class="cl"><span class="k">echo</span> <span class="s2">&#34;Hello, World</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="cp">?&gt;</span><span class="err">
</span></span></span></code></pre></div><h3 id="objective-c">Objective-C</h3>
<p>The language of NeXTSTEP and Apple!</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-objectivec" data-lang="objectivec"><span class="line"><span class="cl"><span class="cp">#import &lt;Foundation/Foundation.h&gt;
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span> <span class="n">argv</span><span class="p">[])</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">@autoreleasepool</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">NSLog</span><span class="p">(</span><span class="s">@&#34;Hello, World&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="lisp">Lisp</h3>
<p>Such a cool language! A ton of seminal AI research was done using Lisp.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lisp" data-lang="lisp"><span class="line"><span class="cl"><span class="p">(</span><span class="nf">format</span> <span class="no">t</span> <span class="s">&#34;Hello, World~%&#34;</span><span class="p">)</span>
</span></span></code></pre></div><figure style="display: block; text-align: center; margin: 1.5em auto;">
  <img src="little-lisper.jpg" alt="The Little LISPer book cover" style="max-width: 280px; border-radius: 8px; box-shadow: 0 3px 8px rgba(0,0,0,0.3); margin: 0 auto;">
  <figcaption style="font-size: 90%; font-style: italic; color: #555; margin-top: 0.5em;">
    <strong>The Little LISPer</strong> — this short book will rewire your brain and change how you think about recursion.
  </figcaption>
</figure>
<hr>
<h3 id="lua">Lua</h3>
<p>If you ever tried modding a game, you might have run into the Lua scripting language. Perfect for powering little video game monster&rsquo;s brains.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="cl"><span class="n">print</span><span class="p">(</span><span class="s2">&#34;Hello, World&#34;</span><span class="p">)</span>
</span></span></code></pre></div><h3 id="javascript-nodejs">JavaScript (Node.js)</h3>
<p>The modern web wouldn&rsquo;t exist without it. I never really liked pure Javascript but Node.js is amazing.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">&#34;Hello, World&#34;</span><span class="p">);</span>
</span></span></code></pre></div><h3 id="x86-assembly-nasm-linux">x86 Assembly (NASM, Linux)</h3>
<p>Programming in assembly is like programming with DNA. It takes a lot of writing to do anything because it is as close to the hardware as you want to get.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-asm" data-lang="asm"><span class="line"><span class="cl"><span class="nf">section</span> <span class="no">.data</span>
</span></span><span class="line"><span class="cl">    <span class="nf">msg</span> <span class="no">db</span> <span class="err">&#34;</span><span class="no">Hello</span><span class="p">,</span> <span class="no">World</span><span class="err">&#34;</span><span class="p">,</span> <span class="mi">0xA</span>   <span class="c1">; The string, with newline
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nf">len</span> <span class="no">equ</span> <span class="no">$</span> <span class="p">-</span> <span class="no">msg</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">section</span> <span class="no">.text</span>
</span></span><span class="line"><span class="cl">    <span class="nf">global</span> <span class="no">_start</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nl">_start:</span>
</span></span><span class="line"><span class="cl">    <span class="nf">mov</span> <span class="no">eax</span><span class="p">,</span> <span class="mi">4</span>        <span class="c1">; sys_write
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nf">mov</span> <span class="no">ebx</span><span class="p">,</span> <span class="mi">1</span>        <span class="c1">; file descriptor (stdout)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nf">mov</span> <span class="no">ecx</span><span class="p">,</span> <span class="no">msg</span>      <span class="c1">; message address
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nf">mov</span> <span class="no">edx</span><span class="p">,</span> <span class="no">len</span>      <span class="c1">; message length
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nf">int</span> <span class="mi">0x80</span>          <span class="c1">; call kernel
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl">    <span class="nf">mov</span> <span class="no">eax</span><span class="p">,</span> <span class="mi">1</span>        <span class="c1">; sys_exit
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nf">xor</span> <span class="no">ebx</span><span class="p">,</span> <span class="no">ebx</span>      <span class="c1">; exit code 0
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nf">int</span> <span class="mi">0x80</span>
</span></span></code></pre></div><h3 id="ada">Ada</h3>
<p>Named after Lady Lovelace, this general-purpose, strongly typed, systems language has been used in avionics, missile guidance systems, satellites, rail control systems, even traffic lights.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ada" data-lang="ada"><span class="line"><span class="cl"><span class="kn">with</span> <span class="nn">Ada.Text_IO</span><span class="p">;</span> <span class="kn">use</span> <span class="nn">Ada.Text_IO</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">procedure</span> <span class="nf">Hello_World</span> <span class="kr">is</span>
</span></span><span class="line"><span class="cl"><span class="kr">begin</span>
</span></span><span class="line"><span class="cl">   <span class="n">Put_Line</span> <span class="p">(</span><span class="s">&#34;Hello, World&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">end</span> <span class="nf">Hello_World</span><span class="p">;</span>
</span></span></code></pre></div><h3 id="smalltalk">Smalltalk</h3>
<p>This was a really neat language!</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-smalltalk" data-lang="smalltalk"><span class="line"><span class="cl"><span class="nc">Transcript</span> <span class="nf">show:</span> <span class="s">&#39;Hello, World&#39;</span><span class="p">;</span> <span class="nf">cr</span><span class="p">.</span>
</span></span></code></pre></div><h3 id="cobol">COBOL</h3>
<p>THE HORROR&hellip;</p>
<figure style="display: block; text-align: center; margin: 2em auto;">
  <img src="grace-hopper.jpg" alt="Rear Admiral Grace Hopper" style="max-width: 500px; border-radius: 8px; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); margin: 0 auto;">
  <figcaption style="font-size: 90%; font-style: italic; color: #555; margin-top: 0.6em; line-height: 1.4;">
    Rear Admiral Grace Hopper—pioneer of programming languages, inventor of the first compiler, and one of the driving forces behind COBOL.<br>
    She coined the term “debugging” and could out-logic any mainframe (in heels no less).
  </figcaption>
</figure>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cobol" data-lang="cobol"><span class="line"><span class="cl"><span class="c">IDENTI</span><span class="nv">FICATION</span> <span class="kr">DIVISION</span><span class="p">.</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="c">PROGRA</span><span class="nv">M-ID</span><span class="p">.</span> <span class="nv">HELLO-WORLD</span><span class="p">.</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="c">PROCED</span><span class="nv">URE</span> <span class="kr">DIVISION</span><span class="p">.</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="c">    DI</span><span class="nv">SPLAY</span> <span class="s2">&#34;HELLO, WORLD&#34;</span><span class="p">.</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="c">    ST</span><span class="nv">OP</span> <span class="kp">RUN</span><span class="p">.</span><span class="err">
</span></span></span></code></pre></div><h3 id="fortran">Fortran</h3>
<p>It&rsquo;s shocking how much Fortran code is still out there.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fortran" data-lang="fortran"><span class="line"><span class="cl"><span class="k">PROGRAM</span><span class="w"> </span><span class="n">HELLO</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">PRINT</span><span class="w"> </span><span class="o">*</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;Hello, World&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">END</span><span class="w">
</span></span></span></code></pre></div><h2 id="intentionally-obtuse-code">Intentionally Obtuse Code</h2>
<p>Here&rsquo;s a fun recursive example!</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">painfully_hello</span><span class="p">(</span><span class="n">msg</span><span class="p">,</span> <span class="n">i</span><span class="o">=</span><span class="mi">0</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="nb">len</span><span class="p">(</span><span class="n">msg</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">msg</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">end</span><span class="o">=</span><span class="s1">&#39;&#39;</span><span class="p">,</span> <span class="n">flush</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">painfully_hello</span><span class="p">(</span><span class="n">msg</span><span class="p">,</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">painfully_hello</span><span class="p">(</span><span class="s2">&#34;Hello, World&#34;</span><span class="p">)</span>
</span></span></code></pre></div><p>And another in Lisp</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lisp" data-lang="lisp"><span class="line"><span class="cl"><span class="p">(</span><span class="nb">defun</span> <span class="nv">painfully-hello</span> <span class="p">(</span><span class="nv">letters</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">(</span><span class="nb">when</span> <span class="nv">letters</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="nf">princ</span> <span class="p">(</span><span class="nf">car</span> <span class="nv">letters</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="nv">painfully-hello</span> <span class="p">(</span><span class="nf">cdr</span> <span class="nv">letters</span><span class="p">))))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span><span class="nv">painfully-hello</span> <span class="p">(</span><span class="nc">list</span> <span class="sc">#\H</span> <span class="sc">#\e</span> <span class="sc">#\l</span> <span class="sc">#\l</span> <span class="sc">#\o</span> <span class="sc">#\,</span> <span class="sc">#\Space</span> <span class="sc">#\W</span> <span class="sc">#\o</span> <span class="sc">#\r</span> <span class="sc">#\l</span> <span class="sc">#\d</span><span class="p">))</span>
</span></span></code></pre></div><h2 id="welcome">Welcome</h2>
<p>Just to make sure you feel extra welcome, and please forgive me if I left off your favorite language:</p>
<ul>
<li>English:      <strong>Welcome!</strong></li>
<li>French:       <strong>Bienvenue !</strong></li>
<li>German:       <strong>Willkommen!</strong></li>
<li>Italian:      <strong>Benvenuto!</strong></li>
<li>Spanish:      <strong>¡Bienvenido!</strong></li>
<li>Portuguese:   <strong>Bem-vindo!</strong></li>
<li>Polish:       <strong>Witaj!</strong></li>
<li>Ukrainian:    <strong>Ласкаво просимо! (Laskavo prosymo!)</strong></li>
<li>Turkish:      <strong>Hoş geldiniz!</strong></li>
<li>Yoruba:       <strong>Ẹ ku abọ!</strong></li>
<li>Hindi:        <strong>स्वागत है! (Swāgat hai!)</strong></li>
<li>Sanskrit:     <strong>स्वागतं! (Svāgataṁ!)</strong></li>
<li>Latin:        <strong>Salve!</strong></li>
<li>Greek:        <strong>Καλώς ήρθατε! (Kalós írthate!)</strong></li>
<li>Japanese:     <strong>ようこそ！ (Yōkoso!)</strong></li>
<li>Mandarin:     <strong>欢迎！ (Huānyíng!)</strong></li>
<li>Arabic:       <strong>أهلاً وسهلاً! (Ahlan wa sahlan!)</strong></li>
<li>Klingon:      <strong>yI&rsquo;el! (literally “Enter!”)</strong></li>
<li>Quenya:       <strong>Hantale! (formal welcome/gratitude)</strong></li>
<li>Esperanto:    <strong>Bonvenon!</strong></li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>There you have it! That&rsquo;s hello world and welcome in 20 languages. I hope you enjoy the site!</p>
<hr>
<h3 id="want-to-connect">Want to connect?</h3>
<p><a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a><br>
<a href="https://github.com/forfaxx">GitHub: forfaxx</a></p>
<hr>
<p><em>See you in the trenches.</em></p>
]]></content:encoded>
    </item>
  </channel>
</rss>
