<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
    xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>sed — LowEndSpirit</title>
        <link>https://lowendspirit.com/</link>
        <pubDate>Sun, 23 Aug 2026 16:07:32 +0000</pubDate>
        <language>en</language>
            <description>sed — LowEndSpirit</description>
    <atom:link href="https://lowendspirit.com/discussions/tagged/sed/feed.rss" rel="self" type="application/rss+xml"/>
    <item>
        <title>sed -i and the backup you forgot</title>
        <link>https://lowendspirit.com/discussion/11252/sed-i-and-the-backup-you-forgot</link>
        <pubDate>Sat, 22 Aug 2026 17:00:00 +0000</pubDate>
        <category>Blog</category>
        <dc:creator>mikho</dc:creator>
        <guid isPermaLink="false">11252@/discussions</guid>
        <description><![CDATA[<p>Two sites, one VPS, one nginx.conf. You're consolidating: <code>blog.oldsite.com</code> is moving off its own droplet and onto the box that already runs <code>oldsite.com</code>, and you want the config renamed to match before you rebrand the domain next week. One command, right?</p>

<pre><code>sed -i 's/oldsite.com/newsite.com/g' nginx.conf
</code></pre>

<p>It runs instantly and prints nothing, which feels like success! <br />
You reload nginx. <br />
Nothing feels like success right up until it doesn't.</p>

<pre><code>$ systemctl reload nginx
Job for nginx.service failed because the control process exited with error code.
</code></pre>

<p><code>nginx -t</code> tells you why:</p>

<pre><code>nginx: [emerg] cannot load certificate "/etc/letsencrypt/live/newsite.com/fullchain.pem": BIO_new_file() failed (SSL: error:02001002:system library:fopen:No such file or directory)
</code></pre>

<p>Here's what actually happened. Your regex didn't just touch the two <code>server_name</code> lines you meant to change, the ones with <code>oldsite.com</code> and <code>blog.oldsite.com</code> in them. It touched every occurrence of the string <code>oldsite.com</code>, anywhere in the file, including the <code>ssl_certificate</code> and <code>ssl_certificate_key</code> paths:</p>

<pre><code>ssl_certificate /etc/letsencrypt/live/oldsite.com/fullchain.pem;
</code></pre>

<p>became</p>

<pre><code>ssl_certificate /etc/letsencrypt/live/newsite.com/fullchain.pem;
</code></pre>

<p><code>sed</code> doesn't know the difference between a hostname in a <code>server_name</code> directive and a hostname baked into a filesystem path. It has no concept of nginx.conf as a config file with meaning, only as a stream of characters to match against. You renamed the domain in your config, but Certbot never renamed the actual directory on disk, so now nginx is pointed at a certificate that doesn't exist. Two working sites are down over a rename that should have taken thirty seconds.</p>

<p>And here's the part that turns "annoying" into "bad night": you ran <code>sed -i</code> with no backup suffix. <br />
Not <code>sed -i.bak</code>, not <code>sed -i.orig</code>, just <code>-i</code>.</p>

<p>On Linux, that's GNU sed's in-place mode with no backup at all. The original file is gone the moment the command returns. You don't have a copy to diff against, you don't remember every line the global match touched, and you're now reconstructing a working nginx.conf from memory at whatever hour this is happening.</p>

<p>This is the exact shape of the thread that resurfaces on LowEndSpirit every few months: someone ran a one-line sed against a live config, it matched more than they expected, and the backup they meant to take never happened because the command felt too small to need one. It's never the complicated multi-line sed script that does this. It's always the quick one-liner you were sure was safe.</p>

<p><strong>The fix costs one extra flag.</strong> <code>sed -i.bak</code> writes the modified file in place and leaves the untouched original sitting right next to it:</p>

<pre><code>$ sed -i.bak 's/oldsite.com/newsite.com/g' nginx.conf
$ ls nginx.conf*
nginx.conf  nginx.conf.bak
</code></pre>

<p>If the replace goes wrong, you're one <code>cp</code> away from a working config again:</p>

<pre><code>cp nginx.conf.bak nginx.conf
</code></pre>

<p>That's it. That's the whole insurance policy, and it costs you five characters.</p>

<p><strong>Better still, don't run <code>-i</code> blind in the first place.</strong> Drop the flag and pipe straight into <code>diff</code> to see exactly what would change before you commit to it:</p>

<pre><code>$ sed 's/oldsite.com/newsite.com/g' nginx.conf | diff nginx.conf -
3c3
&lt;     server_name oldsite.com www.oldsite.com;
---
&gt;     server_name newsite.com www.newsite.com;
5,6c5,6
&lt;     ssl_certificate /etc/letsencrypt/live/oldsite.com/fullchain.pem;
&lt;     ssl_certificate_key /etc/letsencrypt/live/oldsite.com/privkey.pem;
---
&gt;     ssl_certificate /etc/letsencrypt/live/newsite.com/fullchain.pem;
&gt;     ssl_certificate_key /etc/letsencrypt/live/newsite.com/privkey.pem;
</code></pre>

<p>Right there in the diff output, before you've touched anything, you can see the certificate lines are about to get rewritten to paths that don't exist. That's the moment to stop and narrow the regex, maybe anchor it to <code>server_name</code> specifically instead of matching the domain everywhere it appears, rather than the moment you find out from a failed <code>nginx -t</code> at 1 am.</p>

<p>Neither habit takes longer than the reckless version. <code>-i.bak</code> is five extra characters. <br />
The dry-run pipe is one extra pipe. The only thing either one costs you is the two seconds it takes to type them, and that's cheaper than every version of this story that ends with, "<em>and then I remembered I hadn't backed up nginx.conf since I set the server up</em>."</p>
]]>
        </description>
    </item>
    <item>
        <title>sed vs awk vs grep: when to reach for which</title>
        <link>https://lowendspirit.com/discussion/11251/sed-vs-awk-vs-grep-when-to-reach-for-which</link>
        <pubDate>Sat, 22 Aug 2026 07:00:00 +0000</pubDate>
        <category>Blog</category>
        <dc:creator>mikho</dc:creator>
        <guid isPermaLink="false">11251@/discussions</guid>
        <description><![CDATA[<p>Every terminal tutorial teaches these three with the same tired example: <code>cat file | grep foo</code>. <br />
That's not a real task, it's a syntax demo.</p>

<p>On an actual VPS, the question isn't "how does grep work," it's "which of these three do I even use?" Here's how to answer that in about three seconds, using a real <code>access.log</code>.</p>

<p>Say your box just had a slow morning and you're digging through last night's nginx access log:</p>

<pre><code>203.0.113.14 - - [18/Aug/2026:03:12:01 +0000] "GET /wp-login.php HTTP/1.1" 404 162 0.002
198.51.100.7 - - [18/Aug/2026:03:14:22 +0000] "GET /api/v1/status HTTP/1.1" 200 48 0.014
198.51.100.7 - - [18/Aug/2026:03:14:23 +0000] "GET /api/v1/status HTTP/1.1" 200 48 0.011
203.0.113.14 - - [18/Aug/2026:03:15:40 +0000] "POST /xmlrpc.php HTTP/1.1" 404 162 0.002
192.0.2.55 - - [18/Aug/2026:03:20:10 +0000] "GET /api/v1/status HTTP/1.1" 200 48 3.821
192.0.2.55 - - [18/Aug/2026:03:20:15 +0000] "GET /api/v1/report?range=30d HTTP/1.1" 200 118402 1.204
198.51.100.7 - - [18/Aug/2026:03:22:01 +0000] "GET /api/v1/status HTTP/1.1" 200 48 0.009
203.0.113.14 - - [18/Aug/2026:03:23:55 +0000] "GET /.env HTTP/1.1" 404 162 0.001
192.0.2.55 - - [18/Aug/2026:03:25:30 +0000] "GET /api/v1/report?range=7d HTTP/1.1" 200 41200 0.412
</code></pre>

<p>The last column is response time in seconds, added by a custom log format. That's the line that pays off later.</p>

<h3>Question one: am I looking for lines, or looking at data inside them?</h3>

<p>If the answer is "I just need to find the lines," that's grep. Nothing else. You want every request that hit your status endpoint:</p>

<pre><code>$ grep '/api/v1/status' access.log

198.51.100.7 - - [18/Aug/2026:03:14:22 +0000] "GET /api/v1/status HTTP/1.1" 200 48 0.014
198.51.100.7 - - [18/Aug/2026:03:14:23 +0000] "GET /api/v1/status HTTP/1.1" 200 48 0.011
192.0.2.55 - - [18/Aug/2026:03:20:10 +0000] "GET /api/v1/status HTTP/1.1" 200 48 3.821
198.51.100.7 - - [18/Aug/2026:03:22:01 +0000] "GET /api/v1/status HTTP/1.1" 200 48 0.009
</code></pre>

<p>Four lines out of ten, full text, unmodified. That's the whole job of grep: a filter, not a processor. The moment you catch yourself piping grep's output into <code>cut</code> or <code>awk '{print $1}'</code> to pull a field back out, stop, you skipped a step. You didn't need lines, you needed data.</p>

<p><img src="https://lowendspirit.com/uploads/editor/xf/arvs39622270.webp" alt="LowEndSpirit - VPS Hosting and tech forum" title="" /></p>

<h3>Question two: do I need to compute something across fields?</h3>

<p>That 3.821-second response on line five is the actual problem, one request from 192.0.2.55 that took four thousand times longer than the others. Finding that by eye in a real log with fifty thousand lines isn't happening. This is awk's job, because awk thinks in fields and running totals, not just matched lines:</p>

<pre><code>$ awk '{ bytes[$1]+=$10; if ($NF+0 &gt; slow[$1]) slow[$1]=$NF }
       END { for (ip in bytes) printf "%-15s bytes=%-8d slowest=%.3fs\n", ip, bytes[ip], slow[ip] }' access.log

198.51.100.7    bytes=144      slowest=0.014s
192.0.2.55      bytes=159650   slowest=3.821s
203.0.113.14    bytes=648      slowest=0.002s
</code></pre>

<p>One pass over the file, running two accumulators keyed by IP, and the answer falls out: 192.0.2.55 is both your heaviest bandwidth consumer and the source of that slow request. grep could never have told you that. <br />
It doesn't do arithmetic, and it doesn't remember anything between lines. awk does both by default. <br />
That's the actual dividing line between them: grep answers "which," awk answers "how much," "how many," or "what's the total."<br />
<img src="https://lowendspirit.com/uploads/editor/5k/vtizrd5cfq64.webp" alt="LowEndSpirit - VPS Hosting and tech forum" title="" /></p>

<h3>Question three: do I need to change the file itself?</h3>

<p>Neither grep nor awk touches your file; they both just read it and print to stdout. If the task is "the box just got renamed, and I need to update the nginx config to match," that's sed, because sed's whole reason to exist is in-place transformation:</p>

<pre><code>$ cat nginx-site.conf
server {
    listen 80;
    server_name oldbox.example.net www.oldbox.example.net;
    ...
    proxy_set_header Host oldbox.example.net;
}

$ sed -i.bak 's/oldbox\.example\.net/newbox.example.net/g' nginx-site.conf

$ cat nginx-site.conf
server {
    listen 80;
    server_name newbox.example.net www.newbox.example.net;
    ...
    proxy_set_header Host newbox.example.net;
}
</code></pre>

<p>Three occurrences, one command, and the <code>.bak</code> suffix means the original is still sitting right next to it if the regex was wrong. (It wasn't, but check anyway, every time, no exceptions.)<br />
<img src="https://lowendspirit.com/uploads/editor/te/ex4mxd37qpc0.webp" alt="LowEndSpirit - VPS Hosting and tech forum" title="" /></p>

<h3>The actual decision tree</h3>

<p>Not three bullet points with bold headers, just the real question in your head when you open a log file: <strong>do you need to find lines, compute across fields, or edit the file in place?</strong></p>

<ul>
<li>Find lines, grep.</li>
<li>Compute or reshape data, awk.</li>
<li>Change the file, sed.</li>
</ul>

<p>Most real admin tasks are one of these, cleanly, and the rare ones that feel like two at once (find matching lines <em>and</em> count them) are just awk wearing grep's job for a minute, since awk can filter with a pattern before its action block just fine.</p>

<p>You'll still combine grep and awk plenty, and that's fine. grep filters the lines first, so awk only has to work through what's left, which matters on a big file. The real skill isn't picking a favorite tool; it's noticing what you're actually asking for: a field, a total, a changed line. Once you can name that, the right tool is obvious. Most tutorials never make you ask a real question, so you never get the practice.</p>
]]>
        </description>
    </item>
   </channel>
</rss>
