<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"><channel><title>ongardie.net</title><link>https://ongardie.net/blog/</link><description>Diego Ongaro's Blog</description><lastBuildDate>Mon, 19 Jan 2026 03:04:30 GMT</lastBuildDate><generator>PyRSS2Gen-1.1.0</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>Running a Desktop Virtual Machine</title><link>https://ongardie.net/blog/desktop-vm/</link><description>&lt;p&gt;This post is about my experience running a Linux desktop virtual machine on a
Linux host. I used the VM interactively, often in fullscreen mode, to develop
software and run web apps (mostly Slack and Google Docs). My experience wasn't
great, as I ran into many challenges.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://ongardie.net/blog/vm/"&gt;previous post on running VMs&lt;/a&gt; discussed running
bare-bones Linux VMs and getting SSH access to them. I usually do that for
testing, where the VMs are light-weight, shortlived, and disposable. It turns
out that long-lived desktop VMs have more challenging requirements and come
with a whole new set of issues.&lt;/p&gt;

&lt;section id="vm-setup"&gt;
&lt;h2&gt;VM setup&lt;a class="section-link" href="#vm-setup"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Following a &lt;code&gt;virt-install&lt;/code&gt;, I did many of the tasks I normally do for setting
up a Linux host (see my &lt;a href="https://github.com/ongardie/configs"&gt;configs&lt;/a&gt; repo),
including setting the hostname, setting the timezone, configuring APT,
installing packages, and configuring other software.&lt;/p&gt;
&lt;p&gt;I renamed the username and group from &lt;code&gt;debian&lt;/code&gt; to &lt;code&gt;diego&lt;/code&gt;, while logged in as
&lt;code&gt;root&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;usermod&lt;span class="hl-w"&gt; &lt;/span&gt;-d&lt;span class="hl-w"&gt; &lt;/span&gt;/home/diego&lt;span class="hl-w"&gt; &lt;/span&gt;-m&lt;span class="hl-w"&gt; &lt;/span&gt;debian
usermod&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s1"&gt;&amp;#39;Diego&amp;#39;&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-l&lt;span class="hl-w"&gt; &lt;/span&gt;diego&lt;span class="hl-w"&gt; &lt;/span&gt;debian
groupmod&lt;span class="hl-w"&gt; &lt;/span&gt;-n&lt;span class="hl-w"&gt; &lt;/span&gt;diego&lt;span class="hl-w"&gt; &lt;/span&gt;debian
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I could have created a new user instead, but I guess I like having &lt;code&gt;uid&lt;/code&gt; 1000.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="cpu-ram"&gt;
&lt;h2&gt;CPU and RAM&lt;a class="section-link" href="#cpu-ram"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;For CPU, I typically assigned 4 vCPUs to the VM. Changing this value requires
restarting the VM. Just because the VM has a vCPU doesn't mean it's using it,
so it might be reasonable to set this to half or more of the host CPUs. I think
4 vCPUs is the bare minimum needed for software development with VS Code and
Rust, and I probably should have allocated more.&lt;/p&gt;
&lt;p&gt;For RAM, &lt;code&gt;virt-manager&lt;/code&gt; has a field for the maximum allocation and another for
the current allocation. Changing the maximum allocation requires restarting the
VM, but changing the current allocation up to the maximum can happen at
runtime.&lt;/p&gt;
&lt;p&gt;I created an additional swap disk to relieve memory pressure. Using a separate
disk image was convenient because I didn't have to copy it every time when
moving the VM to a different host. I still copied the swap image the first time
because the VM's &lt;code&gt;/etc/fstab&lt;/code&gt; referred to the swap partition's &lt;code&gt;PARTUUID&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I also set
&lt;a href="https://support.mozilla.org/en-US/kb/unload-inactive-tabs-save-system-memory-firefox"&gt;&lt;code&gt;browser.tabs.unloadOnLowMemory&lt;/code&gt;&lt;/a&gt;
to &lt;code&gt;true&lt;/code&gt; in Firefox, which may help with memory pressure.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="display"&gt;
&lt;h2&gt;Display&lt;a class="section-link" href="#display"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;For video settings, I tried a few different options. My main problem was that I
encountered serious rendering glitches with Google Docs in Firefox. I
ultimately used &lt;code&gt;Virtio&lt;/code&gt; with &lt;code&gt;3D Acceleration&lt;/code&gt; on and a display type of &lt;code&gt;Spice Server&lt;/code&gt; with &lt;code&gt;OpenGL&lt;/code&gt; on. However, I set &lt;code&gt;gfx.canvas.accelerated&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; in
Firefox to work around the Google Docs rendering glitches.&lt;/p&gt;
&lt;p&gt;I wanted the VM to be able to run in both windowed and fullscreen modes,
which for my widescreen monitor meant &lt;code&gt;1792x1344&lt;/code&gt; and &lt;code&gt;5120x1440&lt;/code&gt; resolutions.&lt;/p&gt;
&lt;p&gt;The default display resolutions were missing &lt;code&gt;5120x1440&lt;/code&gt;. I created
&lt;code&gt;/etc/X11/xorg.conf.d/10-monitor.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-xorg.conf"&gt;&lt;span class="hl-se"&gt;Section&lt;/span&gt; &lt;span class="hl-se"&gt;&amp;quot;Monitor&amp;quot;&lt;/span&gt;
	&lt;span class="hl-nb"&gt;Identifier&lt;/span&gt; &lt;span class="hl-no"&gt;&amp;quot;Virtual-1&amp;quot;&lt;/span&gt;
	&lt;span class="hl-nb"&gt;Modeline&lt;/span&gt; &lt;span class="hl-no"&gt;&amp;quot;5120x1440_60.00&amp;quot;  624.50  5120 5496 6048 6976  1440 1443 1453 1493 -hsync +vsync&lt;/span&gt;
	&lt;span class="hl-nb"&gt;Option&lt;/span&gt; &lt;span class="hl-no"&gt;&amp;quot;PreferredMode&amp;quot; &amp;quot;1792x1344&amp;quot;&lt;/span&gt;
&lt;span class="hl-se"&gt;EndSection&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I got that &lt;code&gt;Modeline&lt;/code&gt; by running:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-console"&gt;&lt;span class="hl-gp"&gt;$ &lt;/span&gt;sudo&lt;span class="hl-w"&gt; &lt;/span&gt;apt&lt;span class="hl-w"&gt; &lt;/span&gt;install&lt;span class="hl-w"&gt; &lt;/span&gt;xcvt
&lt;span class="hl-go"&gt;[...]&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;cvt&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-m"&gt;5120&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-m"&gt;1440&lt;/span&gt;
&lt;span class="hl-gp"&gt;# &lt;/span&gt;5120x1440&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-m"&gt;59&lt;/span&gt;.96&lt;span class="hl-w"&gt; &lt;/span&gt;Hz&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;(&lt;/span&gt;CVT&lt;span class="hl-o"&gt;)&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;hsync:&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-m"&gt;89&lt;/span&gt;.52&lt;span class="hl-w"&gt; &lt;/span&gt;kHz&lt;span class="hl-p"&gt;;&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;pclk:&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-m"&gt;624&lt;/span&gt;.50&lt;span class="hl-w"&gt; &lt;/span&gt;MHz
&lt;span class="hl-go"&gt;Modeline &amp;quot;5120x1440_60.00&amp;quot;  624.50  5120 5496 6048 6976  1440 1443 1453 1493 -hsync +vsync&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I found that the Xfce Display Settings app was causing the VM to enter
&lt;code&gt;5120x1440&lt;/code&gt; mode on login, even though I wanted it to default to &lt;code&gt;1792x1344&lt;/code&gt;. I
think I removed &lt;code&gt;.config/xfce4/xfconf/xfce-perchannel-xml/displays.xml&lt;/code&gt; and
then never opened the Xfce Display Settings again to work around that.&lt;/p&gt;
&lt;p&gt;I created two scripts and two launcher buttons on the Xfce panel to switch
between windowed and fullscreen modes. The &lt;code&gt;video-windowed&lt;/code&gt; script:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;&lt;span class="hl-ch"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="hl-nb"&gt;exec&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;xrandr&lt;span class="hl-w"&gt; &lt;/span&gt;--output&lt;span class="hl-w"&gt; &lt;/span&gt;Virtual-1&lt;span class="hl-w"&gt; &lt;/span&gt;--mode&lt;span class="hl-w"&gt; &lt;/span&gt;1792x1344
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the &lt;code&gt;video-fullscreen&lt;/code&gt; script:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;&lt;span class="hl-ch"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="hl-nb"&gt;exec&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;xrandr&lt;span class="hl-w"&gt; &lt;/span&gt;--output&lt;span class="hl-w"&gt; &lt;/span&gt;Virtual-1&lt;span class="hl-w"&gt; &lt;/span&gt;--mode&lt;span class="hl-w"&gt; &lt;/span&gt;5120x1440_60.00
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, I ran into an issue with the
&lt;a href="https://notionwm.net/"&gt;Notion window manager&lt;/a&gt; when running the VM in windowed
mode. I normally hold the Meta key and drag the right mouse button to resize
windows. This doesn't work. I could resize a window by dragging the left mouse
button on the window border, but that border so thin that it's hard to hit. As
a workaround, I enabled &lt;code&gt;Shift+Meta&lt;/code&gt; while dragging windows in the VM, in
&lt;code&gt;.notion/cfg_bindings.lua&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-diff"&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;    bdoc(&amp;quot;Resize the frame.&amp;quot;),
&lt;span class="hl-w"&gt; &lt;/span&gt;    mdrag(&amp;quot;Button1@border&amp;quot;, &amp;quot;WFrame.p_resize(_)&amp;quot;),
&lt;span class="hl-w"&gt; &lt;/span&gt;    mdrag(META..&amp;quot;Button3&amp;quot;, &amp;quot;WFrame.p_resize(_)&amp;quot;),
&lt;span class="hl-gi"&gt;+    mdrag(&amp;quot;Shift+&amp;quot;..META..&amp;quot;Button3&amp;quot;, &amp;quot;WFrame.p_resize(_)&amp;quot;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;section id="suspend"&gt;
&lt;h2&gt;Suspend&lt;a class="section-link" href="#suspend"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I wanted to be able to keep this VM &amp;quot;turned on&amp;quot; even while the host computer
was suspended or hibernated.&lt;/p&gt;
&lt;p&gt;Early on, I found that running suspend or hibernate within the VM did not work,
and I disabled it:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;sudo&lt;span class="hl-w"&gt; &lt;/span&gt;systemctl&lt;span class="hl-w"&gt; &lt;/span&gt;mask&lt;span class="hl-w"&gt; &lt;/span&gt;sleep.target&lt;span class="hl-w"&gt; &lt;/span&gt;suspend.target&lt;span class="hl-w"&gt; &lt;/span&gt;hibernate.target&lt;span class="hl-w"&gt; &lt;/span&gt;hybrid-sleep.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Xfce logout dialog stops showing the buttons to take these disabled
actions, which is nice.&lt;/p&gt;
&lt;p&gt;I also found that I couldn't pause and resume the VM in &lt;code&gt;virt-manager&lt;/code&gt;
successfully.&lt;/p&gt;
&lt;p&gt;I could generally suspend the host, and the VM would tolerate that.&lt;/p&gt;
&lt;p&gt;However, in March 2025, the VM kept crashing when the host suspended. I
upgraded the &lt;code&gt;qemu-*&lt;/code&gt; and &lt;code&gt;seabios&lt;/code&gt; packages to the Debian 12 backports
versions (and had to install &lt;code&gt;qemu-system-modules-spice&lt;/code&gt;), which resolved the
suspend issue.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="file-share"&gt;
&lt;h2&gt;Shared filesystem&lt;a class="section-link" href="#file-share"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I set up a Virtio filesystem to share files between the host and the VM. In
&lt;code&gt;virt-manager&lt;/code&gt;, I used &lt;code&gt;virtio-9p&lt;/code&gt; because &lt;code&gt;virtiofs&lt;/code&gt; wasn't supported when
running libvirt under in user (session) mode:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Unable to add device: unsupported configuration: virtiofs is not yet
supported in session mode&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The target path in &lt;code&gt;virt-manager&lt;/code&gt; ends up being the name of the 9p share in the
VM. I added this to &lt;code&gt;/etc/fstab&lt;/code&gt; in the VM:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/home/diego/host-share /home/diego/host-share 9p trans=virtio,version=9p2000.L,posixacl,msize=104857600 0 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I don't remember where I found these options; it may have been the
&lt;a href="https://wiki.qemu.org/Documentation/9psetup"&gt;QEMU wiki&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="migration"&gt;
&lt;h2&gt;Disk image size and VM migration&lt;a class="section-link" href="#migration"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I occasionally moved this VM from a desktop to a laptop and vice versa. I
didn't set up any fancy online migration; I just shut it down and copied the
disk image.&lt;/p&gt;
&lt;p&gt;Ideally, I'd free up some space and shrink the disk image prior to copying it.
I have &lt;code&gt;discard&lt;/code&gt; enabled in the VM's &lt;code&gt;/etc/fstab&lt;/code&gt;. I found I had to change the
&lt;code&gt;virt-manager&lt;/code&gt; disk's &lt;code&gt;Discard mode&lt;/code&gt; setting from &lt;code&gt;Hypervisor default&lt;/code&gt; to
&lt;code&gt;unmap&lt;/code&gt;. Then I ran:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;sudo&lt;span class="hl-w"&gt; &lt;/span&gt;fstrim&lt;span class="hl-w"&gt; &lt;/span&gt;-v&lt;span class="hl-w"&gt; &lt;/span&gt;/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;within the VM, which reported that it trimmed many gigabytes. The &lt;code&gt;qcow2&lt;/code&gt; image
shrank in size accordingly on the host. The manual &lt;code&gt;fstrim&lt;/code&gt; may not be
necessary if you configure &lt;code&gt;virt-manager&lt;/code&gt; from the start.&lt;/p&gt;
&lt;p&gt;I used &lt;code&gt;rsync&lt;/code&gt; to copy the disk image, with the &lt;code&gt;--compress&lt;/code&gt; and &lt;code&gt;--sparse&lt;/code&gt;
flags. I probably should have used the &lt;code&gt;--inplace&lt;/code&gt; flag also (which used to
conflict with &lt;code&gt;--sparse&lt;/code&gt; but doesn't anymore).&lt;/p&gt;
&lt;p&gt;To copy the VM's configuration, I ran this on the original host:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;virsh&lt;span class="hl-w"&gt; &lt;/span&gt;dumpxml&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$NAME&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$NAME&lt;/span&gt;.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then imported it on the new host:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;virsh&lt;span class="hl-w"&gt; &lt;/span&gt;define&lt;span class="hl-w"&gt; &lt;/span&gt;--file&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$NAME&lt;/span&gt;.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also needed to change the display Splice device to &lt;code&gt;auto&lt;/code&gt;, but that might be
because I had messed with it on the original host.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="keyboard"&gt;
&lt;h2&gt;Keyboard&lt;a class="section-link" href="#keyboard"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I set Caps Lock to be Control on some keyboards. I found I had to do this again
within the VM by setting:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;&lt;span class="hl-nv"&gt;XKBOPTIONS&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s2"&gt;&amp;quot;ctrl:nocaps&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;in &lt;code&gt;/etc/default/keyboard&lt;/code&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="webcam"&gt;
&lt;h2&gt;Webcam&lt;a class="section-link" href="#webcam"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I tried passing through a USB webcam for video conferencing, but this was too
slow to work reliably. I don't have a good solution to this. My workaround was
to run the video conferencing on the host.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="conclusion"&gt;
&lt;h2&gt;Conclusion&lt;a class="section-link" href="#conclusion"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;That's it. I used this VM for months, so I don't think I'd find new issues
beyond these. The good news is that most of these are either easy to work
around or acceptable limitations, with some patience. My top remaining issues
are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No webcam support,&lt;/li&gt;
&lt;li&gt;No ability to suspend/hibernate/pause the VM, and&lt;/li&gt;
&lt;li&gt;No dynamic CPU allocation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Webcam support is important, especially because video conferencing often needs
to be authenticated, and that account may &amp;quot;belong&amp;quot; inside the VM. I don't know
how to solve it today, other than perhaps passing a PCIe USB card through to
the VM. The last two are &amp;quot;software issues&amp;quot; and might well be solvable with some
more configuration effort.&lt;/p&gt;
&lt;/section&gt;
</description><guid isPermaLink="true">https://ongardie.net/blog/desktop-vm/</guid><pubDate>Fri, 09 May 2025 03:50:00 GMT</pubDate></item><item><title>Trying Nushell</title><link>https://ongardie.net/blog/nushell/</link><description>&lt;p&gt;I've been trying out &lt;a href="https://www.nushell.sh/"&gt;Nushell&lt;/a&gt; again lately. I
&lt;a href="https://ongardie.net/blog/command-not-found/"&gt;blogged in 2021&lt;/a&gt; about how
&lt;a href="https://code.launchpad.net/~ubuntu-core-dev/command-not-found/ubuntu"&gt;&lt;code&gt;command-not-found&lt;/code&gt;&lt;/a&gt;
is slow to build an index of which commands are provided by which Debian
packages. I created an alternative Posix shell script that performs well
without an index. Now, I ported this script to Nushell. I found it interesting
to compare the differences.&lt;/p&gt;

&lt;section id="comparison"&gt;
&lt;h2&gt;Comparison&lt;a class="section-link" href="#comparison"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The two versions are
&lt;a href="https://github.com/ongardie/cubicle/tree/main/packages/apt-binary/bin"&gt;here&lt;/a&gt;.
Both rely heavily on regular expressions. The Posix shell version is about 10
lines and 250 characters longer. Part of this is additional logic to use
&lt;a href="https://github.com/BurntSushi/ripgrep"&gt;ripgrep&lt;/a&gt; with
&lt;a href="https://github.com/lz4/lz4"&gt;LZ4&lt;/a&gt; when available, yet fall back to some default
that works otherwise:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;&lt;span class="hl-nv"&gt;PATTERN&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s2"&gt;&amp;quot;^(usr/)?s?bin/&lt;/span&gt;&lt;span class="hl-nv"&gt;$1&lt;/span&gt;&lt;span class="hl-s2"&gt;\s&amp;quot;&lt;/span&gt;
&lt;span class="hl-k"&gt;if&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nb"&gt;command&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-v&lt;span class="hl-w"&gt; &lt;/span&gt;rg&lt;span class="hl-w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="hl-w"&gt; &lt;/span&gt;/dev/null&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nb"&gt;command&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-v&lt;span class="hl-w"&gt; &lt;/span&gt;lz4&lt;span class="hl-w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="hl-w"&gt; &lt;/span&gt;/dev/null&lt;span class="hl-p"&gt;;&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-k"&gt;then&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;&lt;span class="hl-nv"&gt;LINES&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-k"&gt;$(&lt;/span&gt;files&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;xargs&lt;span class="hl-w"&gt; &lt;/span&gt;-0&lt;span class="hl-w"&gt; &lt;/span&gt;rg&lt;span class="hl-w"&gt; &lt;/span&gt;--no-filename&lt;span class="hl-w"&gt; &lt;/span&gt;--search-zip&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="hl-nv"&gt;$PATTERN&lt;/span&gt;&lt;span class="hl-s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="hl-k"&gt;)&lt;/span&gt;
&lt;span class="hl-k"&gt;else&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;&lt;span class="hl-nb"&gt;echo&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;&amp;quot;Run &amp;#39;sudo apt install ripgrep lz4&amp;#39; to speed this up&amp;quot;&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;&lt;span class="hl-nv"&gt;LINES&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-k"&gt;$(&lt;/span&gt;files&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;xargs&lt;span class="hl-w"&gt; &lt;/span&gt;-0&lt;span class="hl-w"&gt; &lt;/span&gt;/usr/lib/apt/apt-helper&lt;span class="hl-w"&gt; &lt;/span&gt;cat-file&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;grep&lt;span class="hl-w"&gt; &lt;/span&gt;-P&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="hl-nv"&gt;$PATTERN&lt;/span&gt;&lt;span class="hl-s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="hl-k"&gt;)&lt;/span&gt;
&lt;span class="hl-k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The equivalent Nushell code uses
&lt;a href="https://www.nushell.sh/commands/docs/par-each.html"&gt;&lt;code&gt;par-each&lt;/code&gt;&lt;/a&gt; for
parallelism, which is built into Nushell and seems quite handy:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nushell"&gt;&lt;span class="hl-n"&gt;let&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-n"&gt;packages&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$files&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;&lt;span class="hl-o"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-n"&gt;par&lt;/span&gt;&lt;span class="hl-o"&gt;-&lt;/span&gt;&lt;span class="hl-nb"&gt;each&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;{&lt;/span&gt;
&lt;span class="hl-w"&gt;        &lt;/span&gt;&lt;span class="hl-sr"&gt;/usr/&lt;/span&gt;&lt;span class="hl-n"&gt;lib&lt;/span&gt;&lt;span class="hl-sr"&gt;/apt/&lt;/span&gt;&lt;span class="hl-n"&gt;apt&lt;/span&gt;&lt;span class="hl-o"&gt;-&lt;/span&gt;&lt;span class="hl-n"&gt;helper&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-n"&gt;cat&lt;/span&gt;&lt;span class="hl-o"&gt;-&lt;/span&gt;&lt;span class="hl-n"&gt;file&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$in&lt;/span&gt;
&lt;span class="hl-w"&gt;            &lt;/span&gt;&lt;span class="hl-o"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-n"&gt;parse&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;-&lt;/span&gt;&lt;span class="hl-n"&gt;r&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;(&lt;/span&gt;&lt;span class="hl-s"&gt;&amp;#39;^(?:usr/)?s?bin/&amp;#39;&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;+&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$command&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;+&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s"&gt;&amp;#39;[ \t]+(?&amp;lt;packages&amp;gt;.*)$&amp;#39;&lt;/span&gt;&lt;span class="hl-p"&gt;)&lt;/span&gt;
&lt;span class="hl-w"&gt;            &lt;/span&gt;&lt;span class="hl-o"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-n"&gt;get&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-n"&gt;packages&lt;/span&gt;
&lt;span class="hl-w"&gt;            &lt;/span&gt;&lt;span class="hl-c1"&gt;# ...&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;&lt;span class="hl-p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(This website uses &lt;a href="https://pygments.org/"&gt;Pygments&lt;/a&gt; to do syntax highlighting.
I found that it doesn't support Nushell syntax yet, so the above is rendered as
Perl for now. Perl is my go-to when I need syntax highlighting on an
unsupported language, since it accepts almost any syntax.)&lt;/p&gt;
&lt;p&gt;I also appreciate how I was able to replace this sequence of sed commands. It
was opaque enough to require a comment, and the sed command had two bugs I
found in writing this post (missing &lt;code&gt;-E&lt;/code&gt;, which is needed for the &lt;code&gt;+&lt;/code&gt;, and
missing &lt;code&gt;m&lt;/code&gt; flag, which is needed for multiline processing):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;&lt;span class="hl-c1"&gt;# This sed expression drops the filename, splits the package list by the comma&lt;/span&gt;
&lt;span class="hl-c1"&gt;# delimiter, and drops the section names.&lt;/span&gt;
&lt;span class="hl-nv"&gt;PACKAGES&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-k"&gt;$(&lt;/span&gt;&lt;span class="hl-nb"&gt;echo&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="hl-nv"&gt;$LINES&lt;/span&gt;&lt;span class="hl-s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;sed&lt;span class="hl-w"&gt; &lt;/span&gt;-E&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s1"&gt;&amp;#39;s/^.* +//; s/,/\n/g; s/^.*\///m&amp;#39;&lt;/span&gt;&lt;span class="hl-k"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this Nushell code (following the longer pattern above), which makes its
intent more clear:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nushell"&gt;&lt;span class="hl-c1"&gt;# ...&lt;/span&gt;
&lt;span class="hl-o"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nb"&gt;split&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-n"&gt;row&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s"&gt;&amp;#39;,&amp;#39;&lt;/span&gt;
&lt;span class="hl-o"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-n"&gt;str&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-n"&gt;replace&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;-&lt;/span&gt;&lt;span class="hl-n"&gt;r&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s"&gt;&amp;#39;^.*/&amp;#39;&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s"&gt;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similarly, another buggy call to sed in Posix shell (this won't work with
newlines):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;&lt;span class="hl-k"&gt;$(&lt;/span&gt;&lt;span class="hl-nb"&gt;echo&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="hl-nv"&gt;$PACKAGES&lt;/span&gt;&lt;span class="hl-s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;sed&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s1"&gt;&amp;#39;s/ /|/g&amp;#39;&lt;/span&gt;&lt;span class="hl-k"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;becomes a more obvious operation in Nushell:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nushell"&gt;&lt;span class="hl-p"&gt;(&lt;/span&gt;&lt;span class="hl-nv"&gt;$packages&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-n"&gt;str&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nb"&gt;join&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s"&gt;&amp;#39;|&amp;#39;&lt;/span&gt;&lt;span class="hl-p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;section id="performance"&gt;
&lt;h2&gt;Performance&lt;a class="section-link" href="#performance"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I ran a quick benchmark to compare the versions of &lt;code&gt;apt-binary.sh&lt;/code&gt; (with and
without ripgrep) and &lt;code&gt;apt-binary.nu&lt;/code&gt;. I benchmarked with the files in cache, by
repeating each command at least once and discarding the first result, although
it didn't seem to matter much. I used
&lt;a href="https://github.com/nushell/nushell/releases/tag/0.103.0"&gt;Nushell v0.103.0&lt;/a&gt;
from the Github release binary for &lt;code&gt;x86_64-unknown-linux-gnu&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I ran this in a container where there wasn't much parallelism available, as it
only had Debian Bookworm package lists for one architecture:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-console"&gt;&lt;span class="hl-gp"&gt;$ &lt;/span&gt;ls&lt;span class="hl-w"&gt; &lt;/span&gt;/var/lib/apt/lists/*Contents*&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;get&lt;span class="hl-w"&gt; &lt;/span&gt;size&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;sort
&lt;span class="hl-go"&gt;╭───┬──────────╮&lt;/span&gt;
&lt;span class="hl-go"&gt;│ 0 │  13.6 kB │&lt;/span&gt;
&lt;span class="hl-go"&gt;│ 1 │ 105.9 kB │&lt;/span&gt;
&lt;span class="hl-go"&gt;│ 2 │ 132.2 kB │&lt;/span&gt;
&lt;span class="hl-go"&gt;│ 3 │ 166.8 kB │&lt;/span&gt;
&lt;span class="hl-go"&gt;│ 4 │   1.5 MB │&lt;/span&gt;
&lt;span class="hl-go"&gt;│ 5 │  19.8 MB │&lt;/span&gt;
&lt;span class="hl-go"&gt;│ 6 │  57.0 MB │&lt;/span&gt;
&lt;span class="hl-go"&gt;╰───┴──────────╯&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here are the results for the two versions:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-console"&gt;&lt;span class="hl-gp"&gt;$ &lt;/span&gt;sudo&lt;span class="hl-w"&gt; &lt;/span&gt;apt-get&lt;span class="hl-w"&gt; &lt;/span&gt;remove&lt;span class="hl-w"&gt; &lt;/span&gt;-y&lt;span class="hl-w"&gt; &lt;/span&gt;ripgrep&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;ignore
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;apt-binary.sh&lt;span class="hl-w"&gt; &lt;/span&gt;cvs&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;Run &amp;#39;sudo apt install ripgrep lz4&amp;#39; to speed this up&lt;/span&gt;
&lt;span class="hl-go"&gt;Sorting... Done&lt;/span&gt;
&lt;span class="hl-go"&gt;Full Text Search... Done&lt;/span&gt;
&lt;span class="hl-go"&gt;cvs/stable 2:1.12.13+real-28+deb12u1 amd64&lt;/span&gt;
&lt;span class="hl-go"&gt;  Concurrent Versions System&lt;/span&gt;

&lt;span class="hl-go"&gt;908ms 116µs 365ns&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;sudo&lt;span class="hl-w"&gt; &lt;/span&gt;apt-get&lt;span class="hl-w"&gt; &lt;/span&gt;install&lt;span class="hl-w"&gt; &lt;/span&gt;-y&lt;span class="hl-w"&gt; &lt;/span&gt;ripgrep&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;ignore
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;apt-binary.sh&lt;span class="hl-w"&gt; &lt;/span&gt;cvs&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;...&lt;/span&gt;
&lt;span class="hl-go"&gt;804ms 462µs 550ns&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;apt-binary.nu&lt;span class="hl-w"&gt; &lt;/span&gt;cvs&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;...&lt;/span&gt;
&lt;span class="hl-go"&gt;1sec 510ms 564µs 515ns&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Nushell version takes almost twice as long as the ripgrep version. It seems
to be much slower at doing pipelines with built-in commands, while performing
similarly to &lt;a href="https://en.wikipedia.org/wiki/Almquist_shell#Dash"&gt;dash&lt;/a&gt; for
external pipelines.&lt;/p&gt;
&lt;p&gt;Here's an example with counting bytes, where piping into Nushell's
&lt;a href="https://www.nushell.sh/commands/docs/length.html"&gt;&lt;code&gt;length&lt;/code&gt;&lt;/a&gt; adds a lot of
time:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-console"&gt;&lt;span class="hl-gp"&gt;$ &lt;/span&gt;&lt;span class="hl-nb"&gt;let&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;f&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;&amp;quot;/var/lib/apt/lists/deb.debian.org_debian_dists_bookworm_main_Contents-all.lz4&amp;quot;&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;open&lt;span class="hl-w"&gt; &lt;/span&gt;--raw&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$f&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;wc&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;57084589&lt;/span&gt;
&lt;span class="hl-go"&gt;4ms 101µs 100ns&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;open&lt;span class="hl-w"&gt; &lt;/span&gt;--raw&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$f&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;length&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;print&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;57084589&lt;/span&gt;
&lt;span class="hl-go"&gt;756ms 112µs 752ns&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;sh&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s1"&gt;$&amp;#39;lz4 -d ($f) -c | wc -c&amp;#39;&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;516439434&lt;/span&gt;
&lt;span class="hl-go"&gt;252ms 204µs 142ns&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;lz4&lt;span class="hl-w"&gt; &lt;/span&gt;-d&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$f&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;wc&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;516439434&lt;/span&gt;
&lt;span class="hl-go"&gt;259ms 633µs 621ns&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;lz4&lt;span class="hl-w"&gt; &lt;/span&gt;-d&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$f&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;length&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;print&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;516439434&lt;/span&gt;
&lt;span class="hl-go"&gt;6sec 361ms 656µs 908ns&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's another example for a basic grep or equivalent, where Nushell's
&lt;a href="https://www.nushell.sh/commands/docs/find.html"&gt;&lt;code&gt;find&lt;/code&gt;&lt;/a&gt; takes much longer:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-console"&gt;&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;sh&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s1"&gt;$&amp;#39;lz4 -d ($f) -c | grep ripgrep&amp;#39;&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;usr/src/rustc-1.78.0/src/tools/rust-analyzer/crates/project-model/test_data/ripgrep-metadata.json devel/rust-web-src&lt;/span&gt;
&lt;span class="hl-go"&gt;283ms 70µs 233ns&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;lz4&lt;span class="hl-w"&gt; &lt;/span&gt;-d&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$f&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;grep&lt;span class="hl-w"&gt; &lt;/span&gt;ripgrep&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;usr/src/rustc-1.78.0/src/tools/rust-analyzer/crates/project-model/test_data/ripgrep-metadata.json devel/rust-web-src&lt;/span&gt;
&lt;span class="hl-go"&gt;297ms 517µs 864ns&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;lz4&lt;span class="hl-w"&gt; &lt;/span&gt;-d&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$f&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;find&lt;span class="hl-w"&gt; &lt;/span&gt;ripgrep&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;print&lt;span class="hl-w"&gt; &lt;/span&gt;-r&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;usr/src/rustc-1.78.0/src/tools/rust-analyzer/crates/project-model/test_data/ripgrep-metadata.json devel/rust-web-src&lt;/span&gt;
&lt;span class="hl-go"&gt;805ms 466µs 406ns&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These may not be quite apples-to-apples comparisons for reasons like Unicode
handling, but it seems like Nushell's internal pipelines could use more
optimization.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="refactoring-issues"&gt;
&lt;h2&gt;Refactoring issues&lt;a class="section-link" href="#refactoring-issues"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Nushell has some support for
&lt;a href="https://www.nushell.sh/book/testing.html"&gt;testing and assertions&lt;/a&gt;, which I
hoped to use for testing the parsing code. I ran into some problems, however.
It seems like &lt;a href="https://www.nushell.sh/commands/docs/parse.html"&gt;&lt;code&gt;parse&lt;/code&gt;&lt;/a&gt; is
special in being able to take a byte stream and parse lines of it,
but this somehow doesn't work with a function call (a &amp;quot;command&amp;quot; in Nushell
terminology):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-console"&gt;&lt;span class="hl-gp"&gt;$ &lt;/span&gt;&lt;span class="hl-nb"&gt;let&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;f&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;&amp;quot;/var/lib/apt/lists/deb.debian.org_debian_dists_bookworm_main_Contents-all.lz4&amp;quot;&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;def&lt;span class="hl-w"&gt; &lt;/span&gt;parsefn&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;[&lt;/span&gt;pattern&lt;span class="hl-o"&gt;]&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$in&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;parse&lt;span class="hl-w"&gt; &lt;/span&gt;-r&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$pattern&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;lz4&lt;span class="hl-w"&gt; &lt;/span&gt;-d&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$f&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;parse&lt;span class="hl-w"&gt; &lt;/span&gt;-r&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;&amp;quot;^usr/bin/(.*) &amp;quot;&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;length&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;print&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;8791&lt;/span&gt;
&lt;span class="hl-go"&gt;1sec 142ms 15µs 922ns&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;lz4&lt;span class="hl-w"&gt; &lt;/span&gt;-d&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$f&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;parsefn&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;&amp;quot;^usr/bin/(.*) &amp;quot;&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;length&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;print&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;0&lt;/span&gt;
&lt;span class="hl-go"&gt;1sec 344ms 869µs 81ns&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;lz4&lt;span class="hl-w"&gt; &lt;/span&gt;-d&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$f&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;lines&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;parse&lt;span class="hl-w"&gt; &lt;/span&gt;-r&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;&amp;quot;^usr/bin/(.*) &amp;quot;&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;length&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;print&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;8791&lt;/span&gt;
&lt;span class="hl-go"&gt;1sec 269ms 821µs 783ns&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;lz4&lt;span class="hl-w"&gt; &lt;/span&gt;-d&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$f&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;lines&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;parsefn&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;&amp;quot;^usr/bin/(.*) &amp;quot;&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;length&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;print&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;8791&lt;/span&gt;
&lt;span class="hl-go"&gt;3sec 169ms 105µs 892ns&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It seems like byte streams can't cross into function calls:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-console"&gt;&lt;span class="hl-gp"&gt;$ &lt;/span&gt;/bin/echo&lt;span class="hl-w"&gt; &lt;/span&gt;hi&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;describe
&lt;span class="hl-go"&gt;byte stream&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;def&lt;span class="hl-w"&gt; &lt;/span&gt;describefn&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;[]&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$in&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;describe&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;/bin/echo&lt;span class="hl-w"&gt; &lt;/span&gt;hi&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;describefn
&lt;span class="hl-go"&gt;string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So by trying to refactor my code to test it, I unintentionally changed its
behavior. Getting the old behavior back would require harming performance
drastically or finding a different abstraction boundary.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="closing"&gt;
&lt;h2&gt;Closing thoughts&lt;a class="section-link" href="#closing"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I'm torn about Nushell. Even with over two decades of using Bash/Dash/Zsh, I
still struggle with it. In writing this post, I found 3 bugs in my Posix shell
script. Nushell feels like a massive improvement. It has nice syntax, type
checking, data structures, convenient argument parsing, and a cohesive library
of built-in commands. Nushell remains succinct enough to feel like a shell
rather than a programming language, with easy escapes into &amp;quot;raw&amp;quot; Unix programs.&lt;/p&gt;
&lt;p&gt;Beyond retraining my brain, I struggle with two things. First, it didn't take
me long to run into the issues above, so Nushell may still need more time/work
to mature. Second, whenever I need interoperability with others, I can count on
them having a Posix shell available, but I can't count on them using Nushell. I
shouldn't let that hold me back from using a better tool on my own computers,
but I can't escape having to write Posix shell scripts going forward, which
means having to remember all the gotchas and tricks. Maybe it'd help if there
was a subset of Nushell that could be compiled to Posix shell for
interoperability.&lt;/p&gt;
&lt;/section&gt;
</description><guid isPermaLink="true">https://ongardie.net/blog/nushell/</guid><pubDate>Mon, 28 Apr 2025 23:03:00 GMT</pubDate></item><item><title>Running Virtual Machines on Linux</title><link>https://ongardie.net/blog/vm/</link><description>&lt;p&gt;Virtual machines are useful for running other operating systems within your
computer and for testing and sandboxing system-level software. This post is
about running VMs locally on Linux: how to get a usable disk image and how to
connect to it over SSH. It's not as trivial as it sounds.&lt;/p&gt;
&lt;p&gt;The VM ecosystem has evolved over the last decades.
&lt;a href="https://en.wikipedia.org/wiki/QEMU"&gt;QEMU&lt;/a&gt;/&lt;a href="https://en.wikipedia.org/wiki/Kernel-based_Virtual_Machine"&gt;KVM&lt;/a&gt;
is still the easiest way to get started on Linux, but many of the ecosystem's
projects are designed for running cloud services rather than for desktop or
casual server use. This post aims to provide simple instructions for running
VMs without installing large software stacks. It covers running VMs with and
without &lt;a href="https://cloudinit.readthedocs.io/"&gt;&lt;code&gt;cloud-init&lt;/code&gt;&lt;/a&gt; and
&lt;a href="https://libvirt.org/"&gt;libvirt&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This post deals with a few challenges:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Operating systems and distributions traditionally offered installers, which
you'd insert into a computer on removable media, like a CD or USB stick.
Installers can also be used for VMs, but they take a lot of steps to run.
Now, distros typically offer multiple types of disk images, letting users
skip the install process. (These images handle issues that normally come with
cloning disks, like generating new machine IDs and SSH keys.) Where to find
these images or which one to choose can be non-obvious. For VMs, I usually
want barebone images that offer a quick path to SSH access.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The virtual disk images are often too small to be practical. For example,
Debian's are only 2 GB in size, so you can't download or install much
software in them before running out of space. Worse yet, lots of software
breaks with confusing errors when the filesystem is out of space. This post
includes instructions for growing the disk images, their root partition, and
their root filesystem. Since QEMU's
&lt;a href="https://en.wikipedia.org/wiki/Qcow"&gt;&lt;code&gt;qcow&lt;/code&gt;&lt;/a&gt; disk image format is sparse,
this won't require much more space on the host's disk.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;This post uses
&lt;a href="https://wiki.qemu.org/Documentation/Networking"&gt;QEMU's default user-mode networking stack&lt;/a&gt;,
which creates a local network with NAT for the VM. This allows the VM to make
connections to the Internet and to the host (at &lt;code&gt;10.0.2.2&lt;/code&gt;). Note that the VM
might not be able to ping the host, but TCP and UDP traffic should still
work. However, the host won't be allowed to make connections to the VM. This
post uses QEMU's host port forwarding to allow the host to connect over SSH
to the guest VM.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update 2025-05-08:&lt;/strong&gt; The VM will be able to access services listening
on localhost on the host, as well as services on the local network, which
could be security concerns.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Logging into the distribution-provided disk images can also be a challenge.
This post gets to logging in over SSH and includes both manually installing
an SSH key and using &lt;a href="https://cloudinit.readthedocs.io/"&gt;&lt;code&gt;cloud-init&lt;/code&gt;&lt;/a&gt; to
automate this process.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These instructions are Linux, Debian, and &lt;code&gt;amd64&lt;/code&gt;-centric because that's what
I'm most familiar with. There are many alternatives and options at every level
of the stack. This post aims to provide a reasonable starting point, not the
most optimal configuration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update 2025-05-08:&lt;/strong&gt; There's a follow-up post on
&lt;a href="https://ongardie.net/blog/desktop-vm/"&gt;running desktop VMs&lt;/a&gt;.&lt;/p&gt;
&lt;section id="nocloud"&gt;
&lt;h2&gt;&lt;code&gt;nocloud&lt;/code&gt; images&lt;a class="section-link" href="#nocloud"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This section describes how to start a basic interactive VM. To achieve this,
we'll use QEMU/KVM directly and use Debian's &lt;code&gt;nocloud&lt;/code&gt; image. This image allows
&lt;code&gt;root&lt;/code&gt; to log in with no password and does not have &lt;code&gt;cloud-init&lt;/code&gt; installed.
(The next section deals with &lt;code&gt;cloud-init&lt;/code&gt; images.)&lt;/p&gt;
&lt;p&gt;First, ensure the host CPU has
&lt;a href="https://en.wikipedia.org/wiki/X86_virtualization#Central_processing_unit"&gt;virtualization extensions&lt;/a&gt;
enabled. Almost all modern &lt;code&gt;amd64&lt;/code&gt;/&lt;code&gt;x86-64&lt;/code&gt; CPUs support virtualization, but
some systems have this disabled in the UEFI settings. For AMD, the extensions
are called SVM or AMD-V, and should cause an &lt;code&gt;svm&lt;/code&gt; flag to show up in
&lt;code&gt;/proc/cpuinfo&lt;/code&gt; when enabled. For Intel, the extensions are called VT-x and
should cause a &lt;code&gt;vmx&lt;/code&gt; flag to show up in &lt;code&gt;/proc/cpuinfo&lt;/code&gt; when enabled.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;grep&lt;span class="hl-w"&gt; &lt;/span&gt;-m&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-m"&gt;1&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-P&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s1"&gt;&amp;#39;^flags\b.*\b(svm|vmx)\b&amp;#39;&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;/proc/cpuinfo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you get no output, reboot into your UEFI settings, enable the setting, and
check again.&lt;/p&gt;
&lt;p&gt;Then, install the packages on the host for KVM/QEMU:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;sudo&lt;span class="hl-w"&gt; &lt;/span&gt;apt&lt;span class="hl-w"&gt; &lt;/span&gt;install&lt;span class="hl-w"&gt; &lt;/span&gt;ovmf&lt;span class="hl-w"&gt; &lt;/span&gt;qemu-system-gui&lt;span class="hl-w"&gt; &lt;/span&gt;qemu-system-x86&lt;span class="hl-w"&gt; &lt;/span&gt;qemu-utils
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Download the Debian 12 &lt;code&gt;nocloud&lt;/code&gt; VM image, resize the virtual disk, and run the
VM:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;curl&lt;span class="hl-w"&gt; &lt;/span&gt;-LO&lt;span class="hl-w"&gt; &lt;/span&gt;https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-nocloud-amd64.qcow2
cp&lt;span class="hl-w"&gt; &lt;/span&gt;-i&lt;span class="hl-w"&gt; &lt;/span&gt;debian-12-nocloud-amd64.qcow2&lt;span class="hl-w"&gt; &lt;/span&gt;test.qcow2
qemu-img&lt;span class="hl-w"&gt; &lt;/span&gt;resize&lt;span class="hl-w"&gt; &lt;/span&gt;test.qcow2&lt;span class="hl-w"&gt; &lt;/span&gt;32G
kvm&lt;span class="hl-w"&gt; &lt;/span&gt;-m&lt;span class="hl-w"&gt; &lt;/span&gt;4G&lt;span class="hl-w"&gt; &lt;/span&gt;-nic&lt;span class="hl-w"&gt; &lt;/span&gt;user,hostfwd&lt;span class="hl-o"&gt;=&lt;/span&gt;tcp::2200-:22&lt;span class="hl-w"&gt; &lt;/span&gt;test.qcow2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The options for running KVM/QEMU are vast. Refer to the
&lt;a href="https://dyn.manpages.debian.org/bookworm/qemu-system-x86/qemu-system-x86_64.1.en.html"&gt;man page&lt;/a&gt;
and &lt;a href="https://www.qemu.org/docs/master/system/index.html"&gt;docs&lt;/a&gt; as needed.&lt;/p&gt;
&lt;p&gt;In the graphical window that pops up, log into the VM as &lt;code&gt;root&lt;/code&gt; with no
password.&lt;/p&gt;
&lt;p&gt;Note: If the VM grabs (steals) your mouse and keyboard, there's a magical key
combination to escape, which is
&lt;kbd&gt;&lt;kbd&gt;Ctrl&lt;/kbd&gt;-&lt;kbd&gt;Alt&lt;/kbd&gt;-&lt;kbd&gt;G&lt;/kbd&gt;&lt;/kbd&gt; for me.
Check the window titlebar for a hint if that doesn't work.&lt;/p&gt;
&lt;p&gt;Install an SSH server in the VM:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;apt&lt;span class="hl-w"&gt; &lt;/span&gt;update
apt&lt;span class="hl-w"&gt; &lt;/span&gt;instal&lt;span class="hl-w"&gt; &lt;/span&gt;--yes&lt;span class="hl-w"&gt; &lt;/span&gt;openssh-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, you have some authentication options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Ideally, authorize an SSH key, for example by downloading it from GitHub:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;mkdir&lt;span class="hl-w"&gt; &lt;/span&gt;-m&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-m"&gt;700&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-p&lt;span class="hl-w"&gt; &lt;/span&gt;.ssh
curl&lt;span class="hl-w"&gt; &lt;/span&gt;https://github.com/⟪USER⟫.keys&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;tee&lt;span class="hl-w"&gt; &lt;/span&gt;-a&lt;span class="hl-w"&gt; &lt;/span&gt;.ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Or if that's inconvenient, set a root password:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;passwd
&lt;span class="hl-nb"&gt;echo&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s1"&gt;&amp;#39;PermitRootLogin yes&amp;#39;&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="hl-w"&gt; &lt;/span&gt;/etc/ssh/sshd_config.d/10rootpassword.conf
systemctl&lt;span class="hl-w"&gt; &lt;/span&gt;restart&lt;span class="hl-w"&gt; &lt;/span&gt;ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Or if you don't care for this VM at all and are confident in the security of
your host's network, allow SSH as root with no password:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;cat&lt;span class="hl-w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="hl-w"&gt; &lt;/span&gt;/etc/ssh/sshd_config.d/10insecure.conf&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s"&gt;&amp;lt;&amp;lt;END&lt;/span&gt;
&lt;span class="hl-s"&gt;PermitRootLogin yes&lt;/span&gt;
&lt;span class="hl-s"&gt;PermitEmptyPasswords yes&lt;/span&gt;
&lt;span class="hl-s"&gt;END&lt;/span&gt;
systemctl&lt;span class="hl-w"&gt; &lt;/span&gt;restart&lt;span class="hl-w"&gt; &lt;/span&gt;ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now, you can SSH from the host:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;ssh&lt;span class="hl-w"&gt; &lt;/span&gt;root@localhost&lt;span class="hl-w"&gt; &lt;/span&gt;-p&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-m"&gt;2200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From inside the VM, resize the root partition and filesystem:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;apt&lt;span class="hl-w"&gt; &lt;/span&gt;install&lt;span class="hl-w"&gt; &lt;/span&gt;--yes&lt;span class="hl-w"&gt; &lt;/span&gt;cloud-guest-utils
growpart&lt;span class="hl-w"&gt; &lt;/span&gt;/dev/sda&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-m"&gt;1&lt;/span&gt;
resize2fs&lt;span class="hl-w"&gt; &lt;/span&gt;/dev/sda1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And upgrade stale packages:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;apt&lt;span class="hl-w"&gt; &lt;/span&gt;upgrade&lt;span class="hl-w"&gt; &lt;/span&gt;--yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now your VM is ready to use.&lt;/p&gt;
&lt;p&gt;You can shut it down gracefully or kill the &lt;code&gt;kvm&lt;/code&gt; process to stop the VM. Then
you can remove its disk image.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="cloud-init"&gt;
&lt;h2&gt;&lt;code&gt;cloud-init&lt;/code&gt; images&lt;a class="section-link" href="#cloud-init"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This section describes how to run
&lt;a href="https://cloudinit.readthedocs.io/"&gt;&lt;code&gt;cloud-init&lt;/code&gt;&lt;/a&gt; images, which are provided by
many distributions and operating systems. &lt;code&gt;cloud-init&lt;/code&gt; is software inside the
VM that runs at boot to discover configuration provided to it from the outside
world. This requires a little more setup before starting the VM, but then the
VM will be better configured on startup.&lt;/p&gt;
&lt;p&gt;There are various ways to inject the &lt;code&gt;cloud-init&lt;/code&gt; configuration. This post
uses the simplest
&lt;a href="https://cloudinit.readthedocs.io/en/latest/reference/datasources/nocloud.html#source-2-drive-with-labeled-filesystem"&gt;NoCloud data source with an extra attached drive&lt;/a&gt;.
The extra drive (like a virtual CD-ROM or thumb drive) is given a volume label
of &lt;code&gt;CIDATA&lt;/code&gt; so &lt;code&gt;cloud-init&lt;/code&gt; inside the VM can discover it during boot and look
for configuration files inside.&lt;/p&gt;
&lt;p&gt;One added benefit of using &lt;code&gt;cloud-init&lt;/code&gt; is that these images typically have
&lt;a href="https://packages.debian.org/bookworm/cloud-initramfs-growroot"&gt;&lt;code&gt;cloud-initramfs-growroot&lt;/code&gt;&lt;/a&gt;,
which means they will automatically grow their root partition and filesystem if
the virtual disk has extra space (at least on the first boot). This saves us
some steps.&lt;/p&gt;
&lt;p&gt;In this section, we'll use Debian's &lt;code&gt;generic&lt;/code&gt; image, which contains
&lt;code&gt;cloud-init&lt;/code&gt;. There is no way to log into this image without using
&lt;code&gt;cloud-init&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;nocloud&lt;/code&gt; terminology is confusing: Debian &lt;code&gt;nocloud&lt;/code&gt; images do not contain
&lt;code&gt;cloud-init&lt;/code&gt;. Debian &lt;code&gt;generic&lt;/code&gt; images contain &lt;code&gt;cloud-init&lt;/code&gt; and can use its
NoCloud data source. No clouds were harmed in the writing of this blog post.&lt;/p&gt;
&lt;p&gt;Debian also offers a &lt;code&gt;genericcloud&lt;/code&gt; image, but I don't recommend it. It's
smaller than the &lt;code&gt;generic&lt;/code&gt; image by omitting a bunch of drivers (about
330 MB vs 420 MB). However, these drivers can be useful, even with
KVM/QEMU:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;QEMU emulates an &lt;code&gt;e1000&lt;/code&gt; driver for the NIC by default, and the
&lt;code&gt;genericcloud&lt;/code&gt; image doesn't have that driver. (You can use a &lt;code&gt;virtio&lt;/code&gt; NIC to
work around this by passing &lt;code&gt;model=virtio-net-pci&lt;/code&gt; as an option to &lt;code&gt;-nic&lt;/code&gt;.)&lt;/li&gt;
&lt;li&gt;The way that &lt;code&gt;cloud-install&lt;/code&gt; presents its &lt;code&gt;cloud-init&lt;/code&gt; configuration drive
(as used in the next section) also requires drivers, so the &lt;code&gt;genericcloud&lt;/code&gt;
images will fail to find the drive.&lt;/li&gt;
&lt;li&gt;I don't know what other QEMU devices might have missing drivers and cause
headaches in the future.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Download the Debian 12 &lt;code&gt;generic&lt;/code&gt; VM image and resize the virtual disk:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;curl&lt;span class="hl-w"&gt; &lt;/span&gt;-LO&lt;span class="hl-w"&gt; &lt;/span&gt;https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2
cp&lt;span class="hl-w"&gt; &lt;/span&gt;-i&lt;span class="hl-w"&gt; &lt;/span&gt;debian-12-generic-amd64.qcow2&lt;span class="hl-w"&gt; &lt;/span&gt;test.qcow2
qemu-img&lt;span class="hl-w"&gt; &lt;/span&gt;resize&lt;span class="hl-w"&gt; &lt;/span&gt;test.qcow2&lt;span class="hl-w"&gt; &lt;/span&gt;32G
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point, if you run &lt;code&gt;kvm&lt;/code&gt; like before, you'll observe that you can't log
in. This can be frustrating if &lt;code&gt;cloud-init&lt;/code&gt; somehow isn't working.&lt;/p&gt;
&lt;p&gt;Next, set up a &lt;code&gt;cloud-init&lt;/code&gt; configuration to set a password and authorize an
SSH key. This happens in a file called &lt;code&gt;user-data&lt;/code&gt;. The user-data file format
is YAML with an extra &lt;code&gt;#cloud-config&lt;/code&gt; header. This post uses a more JSON-like
format to prevent whitespace errors; as YAML is a superset of JSON, this is
allowed. These options apply to the default user, which varies per distro. The
default username is &lt;code&gt;debian&lt;/code&gt; for Debian images.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;mkdir&lt;span class="hl-w"&gt; &lt;/span&gt;-p&lt;span class="hl-w"&gt; &lt;/span&gt;cidata
tee&lt;span class="hl-w"&gt; &lt;/span&gt;cidata/user-data&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s"&gt;&amp;lt;&amp;lt;END&lt;/span&gt;
&lt;span class="hl-s"&gt;#cloud-config&lt;/span&gt;
&lt;span class="hl-s"&gt;{&lt;/span&gt;
&lt;span class="hl-s"&gt;    &amp;quot;password&amp;quot;: &amp;quot;p4ssw0rd&amp;quot;,&lt;/span&gt;
&lt;span class="hl-s"&gt;    &amp;quot;chpasswd&amp;quot;: {&lt;/span&gt;
&lt;span class="hl-s"&gt;        &amp;quot;expire&amp;quot;: false,&lt;/span&gt;
&lt;span class="hl-s"&gt;    },&lt;/span&gt;
&lt;span class="hl-s"&gt;    &amp;quot;ssh_authorized_keys&amp;quot;: [&lt;/span&gt;
&lt;span class="hl-s"&gt;        &amp;quot;$(cat ~/.ssh/id_ed25519.pub)&amp;quot;,&lt;/span&gt;
&lt;span class="hl-s"&gt;    ],&lt;/span&gt;
&lt;span class="hl-s"&gt;}&lt;/span&gt;
&lt;span class="hl-s"&gt;END&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also, create a blank &lt;code&gt;meta-data&lt;/code&gt; file, since that's required:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;&lt;span class="hl-nb"&gt;echo&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="hl-w"&gt; &lt;/span&gt;cidata/meta-data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href="https://docs.cloud-init.io/en/latest/reference/modules.html#users-and-groups"&gt;&lt;code&gt;cloud-init&lt;/code&gt; documentation&lt;/a&gt; describes the possible fields.&lt;/p&gt;
&lt;p&gt;To get these files to the VM, they'll need to be packaged into an ISO or VFAT
filesystem with the volume label of &lt;code&gt;CIDATA&lt;/code&gt;. Rather than do that manually,
&lt;a href="https://www.qemu.org/docs/master/system/qemu-block-drivers.html#virtual-fat-disk-images"&gt;QEMU can create a VFAT drive from a directory&lt;/a&gt;.
The QEMU invocation is a mouthful, but it's still fairly convenient.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;kvm&lt;span class="hl-w"&gt; &lt;/span&gt;-m&lt;span class="hl-w"&gt; &lt;/span&gt;4G&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;-nic&lt;span class="hl-w"&gt; &lt;/span&gt;user,hostfwd&lt;span class="hl-o"&gt;=&lt;/span&gt;tcp::2200-:22&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;test.qcow2&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;-drive&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;file&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;fat:./cidata,format&lt;span class="hl-o"&gt;=&lt;/span&gt;vvfat,if&lt;span class="hl-o"&gt;=&lt;/span&gt;virtio,label&lt;span class="hl-o"&gt;=&lt;/span&gt;CIDATA
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If all goes well, you can log in interactively as user &lt;code&gt;debian&lt;/code&gt; with password
&lt;code&gt;p4ssw0rd&lt;/code&gt; (as set in &lt;code&gt;user-data&lt;/code&gt;), and that user can &lt;code&gt;sudo&lt;/code&gt; without a
password. You can also use SSH with your SSH key:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;ssh&lt;span class="hl-w"&gt; &lt;/span&gt;debian@localhost&lt;span class="hl-w"&gt; &lt;/span&gt;-p&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-m"&gt;2200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thanks to &lt;code&gt;cloud-init&lt;/code&gt;, the root partition and filesystem have already been
expanded to the available disk image size.&lt;/p&gt;
&lt;p&gt;Upgrade stale packages:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;sudo&lt;span class="hl-w"&gt; &lt;/span&gt;apt&lt;span class="hl-w"&gt; &lt;/span&gt;update
sudo&lt;span class="hl-w"&gt; &lt;/span&gt;apt&lt;span class="hl-w"&gt; &lt;/span&gt;upgrade&lt;span class="hl-w"&gt; &lt;/span&gt;--yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now your VM is ready to use.&lt;/p&gt;
&lt;p&gt;Next time you run the VM, you don't need to provide the &lt;code&gt;CIDATA&lt;/code&gt; image, since
its job is done.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="libvirt"&gt;
&lt;h2&gt;libvirt&lt;a class="section-link" href="#libvirt"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This section runs VMs using &lt;a href="https://libvirt.org/"&gt;libvirt&lt;/a&gt;, which is a way of
managing VMs without lengthy KVM invocations. libvirt isn't necessary, but it's
probably a better approach if you want to manage long-lived VMs with a variety
of operating systems. It also configures KVM with more modern defaults.&lt;/p&gt;
&lt;p&gt;Libvirt is a collection of software, named after the underlying library:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://manpages.debian.org/bookworm/libvirt-clients/virsh.1.en.html"&gt;&lt;code&gt;virsh&lt;/code&gt;&lt;/a&gt;
is its main command-line interface.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://manpages.debian.org/bookworm/virtinst/virt-install.1.en.html"&gt;&lt;code&gt;virt-install&lt;/code&gt;&lt;/a&gt;
is a command-line tool to create the VMs (as this is difficult to do with
&lt;code&gt;virsh&lt;/code&gt; directly).&lt;/li&gt;
&lt;li&gt;&lt;a href="https://manpages.debian.org/bookworm/virt-viewer/virt-viewer.1.en.html"&gt;&lt;code&gt;virt-viewer&lt;/code&gt;&lt;/a&gt;
provides a virtual keyboard, video, and mouse.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://virt-manager.org/"&gt;&lt;code&gt;virt-manager&lt;/code&gt;&lt;/a&gt; is a GUI for managing VMs. While
you can use the GUI to do most of this, this post focuses on the command-line
tools.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Libvirt can be used in a system-wide mode (&lt;code&gt;qemu:///system&lt;/code&gt;) or for your local
user (&lt;code&gt;qemu:///session&lt;/code&gt;). This post uses the user mode. (The system mode may be
useful to bridge your VMs to the host's network. To use the system mode, you'd
need to install &lt;code&gt;libvirt-daemon-system&lt;/code&gt; and add your user to the &lt;code&gt;libvirt&lt;/code&gt;
group.)&lt;/p&gt;
&lt;p&gt;Unfortunately, different libvirt tools have different default modes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;virsh&lt;/code&gt; defaults to &lt;code&gt;qemu:///session&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;virt-install&lt;/code&gt; defaults to &lt;code&gt;qemu:///system&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;virt-manager&lt;/code&gt; defaults to showing both and connecting to neither.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;virt-viewer&lt;/code&gt; defaults to &lt;code&gt;qemu:///session&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can pass &lt;code&gt;--connect qemu:///session&lt;/code&gt; to any of these tools. You may want to
set up shell aliases for convenience.&lt;/p&gt;
&lt;p&gt;Install the host packages:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;sudo&lt;span class="hl-w"&gt; &lt;/span&gt;apt&lt;span class="hl-w"&gt; &lt;/span&gt;install&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;gir1.2-spiceclientgtk-3.0&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;libvirt-clients&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;libvirt-daemon&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;virtinst&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;virt-manager&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;virt-viewer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a VM using &lt;code&gt;virt-install&lt;/code&gt; based on Debian's &lt;code&gt;generic&lt;/code&gt; image. This uses
the image downloaded and the &lt;code&gt;cloud-init&lt;/code&gt; configuration files created in the
previous section.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;cp&lt;span class="hl-w"&gt; &lt;/span&gt;-i&lt;span class="hl-w"&gt; &lt;/span&gt;debian-12-generic-amd64.qcow2&lt;span class="hl-w"&gt; &lt;/span&gt;test.qcow2
qemu-img&lt;span class="hl-w"&gt; &lt;/span&gt;resize&lt;span class="hl-w"&gt; &lt;/span&gt;test.qcow2&lt;span class="hl-w"&gt; &lt;/span&gt;32G
virt-install&lt;span class="hl-w"&gt; &lt;/span&gt;--connect&lt;span class="hl-w"&gt; &lt;/span&gt;qemu:///session&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;--cloud-init&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s1"&gt;&amp;#39;disable=on,meta-data=./cidata/meta-data,user-data=./cidata/user-data&amp;#39;&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;--disk&lt;span class="hl-w"&gt; &lt;/span&gt;test.qcow2&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;--import&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;--memory&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-m"&gt;4096&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;--name&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nb"&gt;test&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;--os-variant&lt;span class="hl-w"&gt; &lt;/span&gt;debian11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This uses the &lt;code&gt;debian11&lt;/code&gt; OS variant since the &lt;code&gt;osinfo-db&lt;/code&gt; package in Debian 12
does not currently know about Debian 12. The OS variant doesn't appear to
matter much when importing a disk image.&lt;/p&gt;
&lt;p&gt;Use the key combination &lt;kbd&gt;&lt;kbd&gt;Ctrl&lt;/kbd&gt;-&lt;kbd&gt;]&lt;/kbd&gt;&lt;/kbd&gt; to exit the
&lt;code&gt;virsh&lt;/code&gt; console. You can pass &lt;code style="white-space: nowrap"&gt;--autoconsole
none&lt;/code&gt; to &lt;code&gt;virt-install&lt;/code&gt; if you don't want to be dropped into the console.&lt;/p&gt;
&lt;p&gt;Since &lt;code&gt;virt-install&lt;/code&gt; supports &lt;code&gt;cloud-init&lt;/code&gt;, we didn't need to have QEMU present
a &lt;code&gt;CIDATA&lt;/code&gt; drive. Actually, we don't even need the YAML files for simple
settings. The following is usually sufficient:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;--cloud-init&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;&amp;quot;disable=on,clouduser-ssh-key=&lt;/span&gt;&lt;span class="hl-nv"&gt;$HOME&lt;/span&gt;&lt;span class="hl-s2"&gt;/.ssh/id_ed25519.pub&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Forward a host port to SSH to the VM:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;virsh&lt;span class="hl-w"&gt; &lt;/span&gt;qemu-monitor-command&lt;span class="hl-w"&gt; &lt;/span&gt;--hmp&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nb"&gt;test&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s1"&gt;&amp;#39;hostfwd_add tcp::2200-:22&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since libvirt doesn't currently support forwarding host ports, you'll need to
run that &lt;code&gt;hostfwd_add&lt;/code&gt; command every time you run the VM. Note that &lt;code&gt;test&lt;/code&gt; in
the command is the name of the VM. This workaround is thanks to
&lt;a href="https://blog.adamspiers.org/2012/01/23/port-redirection-from-kvm-host-to-guest/"&gt;Adam Spiers' blog post from 2012&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Then run:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;ssh&lt;span class="hl-w"&gt; &lt;/span&gt;debian@localhost&lt;span class="hl-w"&gt; &lt;/span&gt;-p&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-m"&gt;2200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Upgrade stale packages:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;sudo&lt;span class="hl-w"&gt; &lt;/span&gt;apt&lt;span class="hl-w"&gt; &lt;/span&gt;update
sudo&lt;span class="hl-w"&gt; &lt;/span&gt;apt&lt;span class="hl-w"&gt; &lt;/span&gt;upgrade&lt;span class="hl-w"&gt; &lt;/span&gt;--yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now your VM is ready to use.&lt;/p&gt;
&lt;p&gt;These are some useful libvirt commands:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;List VM statuses:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;virsh&lt;span class="hl-w"&gt; &lt;/span&gt;list&lt;span class="hl-w"&gt; &lt;/span&gt;--all
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Boot an existing VM:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;virsh&lt;span class="hl-w"&gt; &lt;/span&gt;start&lt;span class="hl-w"&gt; &lt;/span&gt;⟪NAME⟫
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;View a VM's text console:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;virsh&lt;span class="hl-w"&gt; &lt;/span&gt;console&lt;span class="hl-w"&gt; &lt;/span&gt;⟪NAME⟫
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;View a VM's graphical console:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;virt-viewer&lt;span class="hl-w"&gt; &lt;/span&gt;⟪NAME⟫
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Shut down a VM gracefully:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;virsh&lt;span class="hl-w"&gt; &lt;/span&gt;shutdown&lt;span class="hl-w"&gt; &lt;/span&gt;⟪NAME⟫
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Shut down a VM forcefully:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;virsh&lt;span class="hl-w"&gt; &lt;/span&gt;destroy&lt;span class="hl-w"&gt; &lt;/span&gt;⟪NAME⟫
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Delete a VM and its disks:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;virsh&lt;span class="hl-w"&gt; &lt;/span&gt;undefine&lt;span class="hl-w"&gt; &lt;/span&gt;⟪NAME⟫&lt;span class="hl-w"&gt; &lt;/span&gt;--remove-all-storage
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And recall that &lt;code&gt;virt-manager&lt;/code&gt; is a useful GUI that also provides all this
&lt;code&gt;virsh&lt;/code&gt; and &lt;code&gt;virt-viewer&lt;/code&gt; functionality and more.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="someday"&gt;
&lt;h2&gt;Someday/maybe&lt;a class="section-link" href="#someday"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/virt-manager/virt-manager/issues/143"&gt;&lt;code&gt;virt-manager&lt;/code&gt; issue #143&lt;/a&gt;
would allow using &lt;code&gt;cloud-init&lt;/code&gt; when creating new VMs from the GUI.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gitlab.com/libvirt/libvirt/-/issues/285"&gt;libvirt issue #285&lt;/a&gt; would
support forwarding host ports.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://manpages.debian.org/bookworm/libguestfs-tools/virt-customize.1.en.html"&gt;&lt;code&gt;virt-customize&lt;/code&gt;&lt;/a&gt;
appears to directly modify VM disk image files and may be an alternative to
&lt;code&gt;cloud-init&lt;/code&gt;. I haven't tried it.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://libvirt.org/ssh-proxy.html"&gt;libvirt SSH proxy&lt;/a&gt; should allow
SSH to VMs over &lt;a href="https://wiki.qemu.org/Features/VirtioVsock"&gt;VSOCK&lt;/a&gt; instead
of TCP. This would remove the need for port forwarding for SSH. However, it's
not packaged for Debian or widely supported in VM images yet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Update 2025-05-08:&lt;/strong&gt; There's a follow-up post on
&lt;a href="https://ongardie.net/blog/desktop-vm/"&gt;running desktop VMs&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="images"&gt;
&lt;h2&gt;Other distributions and operating systems&lt;a class="section-link" href="#images"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Many operating systems offer cloud images and &lt;code&gt;cloud-init&lt;/code&gt; support. This
section documents a few that I've tried.&lt;/p&gt;
&lt;h4&gt;Debian&lt;/h4&gt;
&lt;p&gt;Debian images for Debian 12 (Bookworm) were covered above. For other releases,
see &lt;a href="https://cloud.debian.org/images/cloud/"&gt;https://cloud.debian.org/images/cloud/&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The default user is &lt;code&gt;debian&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Use &lt;code&gt;--os-variant debian11&lt;/code&gt; as the closest version known to Debian 12.&lt;/p&gt;
&lt;h4&gt;Fedora&lt;/h4&gt;
&lt;p&gt;These images work with and require &lt;code&gt;cloud-init&lt;/code&gt;. For other releases, see
&lt;a href="https://download-ib01.fedoraproject.org/pub/fedora/linux/releases/"&gt;https://download-ib01.fedoraproject.org/pub/fedora/linux/releases/&lt;/a&gt;. For more
info, see &lt;a href="https://fedoraproject.org/cloud/download"&gt;https://fedoraproject.org/cloud/download&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Fedora 41:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;curl&lt;span class="hl-w"&gt; &lt;/span&gt;-LO&lt;span class="hl-w"&gt; &lt;/span&gt;https://download.fedoraproject.org/pub/fedora/linux/releases/41/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-41-1.4.x86_64.qcow2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The default user is &lt;code&gt;fedora&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Use &lt;code&gt;--os-variant fedora37&lt;/code&gt; as the closest version known to Debian 12.&lt;/p&gt;
&lt;h4&gt;FreeBSD&lt;/h4&gt;
&lt;p&gt;These images allow logging in interactively as &lt;code&gt;root&lt;/code&gt; with no password, and
they grow their root filesystem automatically. Despite its name, even the
&lt;code&gt;BASIC-CLOUDINIT&lt;/code&gt; images do not appear to run &lt;code&gt;cloud-init&lt;/code&gt; yet. For other
releases, see &lt;a href="https://download.freebsd.org/releases/VM-IMAGES/"&gt;https://download.freebsd.org/releases/VM-IMAGES/&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;FreeBSD 14.1 &lt;code&gt;BASIC-CLOUDINIT&lt;/code&gt; with ZFS variant:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;curl&lt;span class="hl-w"&gt; &lt;/span&gt;-L&lt;span class="hl-w"&gt; &lt;/span&gt;https://download.freebsd.org/releases/VM-IMAGES/14.1-RELEASE/amd64/Latest/FreeBSD-14.1-RELEASE-amd64-BASIC-CLOUDINIT-zfs.qcow2.xz&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;xz&lt;span class="hl-w"&gt; &lt;/span&gt;-d&lt;span class="hl-w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="hl-w"&gt; &lt;/span&gt;FreeBSD-14.1-RELEASE-amd64-BASIC-CLOUDINIT-zfs.qcow2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FreeBSD 14.1 standard with ZFS variant:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;curl&lt;span class="hl-w"&gt; &lt;/span&gt;-L&lt;span class="hl-w"&gt; &lt;/span&gt;https://download.freebsd.org/releases/VM-IMAGES/14.1-RELEASE/amd64/Latest/FreeBSD-14.1-RELEASE-amd64-zfs.qcow2.xz&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;xz&lt;span class="hl-w"&gt; &lt;/span&gt;-d&lt;span class="hl-w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="hl-w"&gt; &lt;/span&gt;FreeBSD-14.1-RELEASE-amd64-zfs.qcow2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;--os-variant freebsd13.1&lt;/code&gt; as the closest version known to Debian 12.&lt;/p&gt;
&lt;h4&gt;Ubuntu&lt;/h4&gt;
&lt;p&gt;These images work with and require &lt;code&gt;cloud-init&lt;/code&gt; (which is a Canonical project).
For other releases, see &lt;a href="https://cloud-images.ubuntu.com/"&gt;https://cloud-images.ubuntu.com/&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Ubuntu 24.04 LTS (Noble):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;curl&lt;span class="hl-w"&gt; &lt;/span&gt;-L&lt;span class="hl-w"&gt; &lt;/span&gt;-o&lt;span class="hl-w"&gt; &lt;/span&gt;noble-server-cloudimg-amd64.qcow2&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ubuntu 24.10 (Oracular):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;curl&lt;span class="hl-w"&gt; &lt;/span&gt;-L&lt;span class="hl-w"&gt; &lt;/span&gt;-o&lt;span class="hl-w"&gt; &lt;/span&gt;oracular-server-cloudimg-amd64.qcow2&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;https://cloud-images.ubuntu.com/oracular/current/oracular-server-cloudimg-amd64.img
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The default user is &lt;code&gt;ubuntu&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Use &lt;code&gt;--os-variant ubuntu22.10&lt;/code&gt; as the closest version known to Debian 12.&lt;/p&gt;
&lt;/section&gt;
</description><guid isPermaLink="true">https://ongardie.net/blog/vm/</guid><pubDate>Mon, 02 Dec 2024 01:33:00 GMT</pubDate></item><item><title>Website Updates (2024)</title><link>https://ongardie.net/blog/website-updates-2024/</link><description>&lt;p&gt;I did a bunch of more updates to this website, including adding a dark mode.
Most of the other changes are either invisible or barely noticeable. That's OK.
My &lt;em&gt;several&lt;/em&gt; visitors will appreciate them. Or at least I'll appreciate them.&lt;/p&gt;

&lt;section id="dark-mode"&gt;
&lt;h2&gt;Dark mode and syntax highlighting&lt;a class="section-link" href="#dark-mode"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I added a dark mode using CSS, with
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme"&gt;&lt;code&gt;prefers-color-scheme&lt;/code&gt;&lt;/a&gt;
(widely availabe as of 2020). I switched syntax highlighting (using
&lt;a href="https://pygments.org/"&gt;Pygments&lt;/a&gt;) from inline styles to CSS classes, which use
different theme colors for light and dark modes. This change will cause the RSS
feed to not have syntax highlighting (since RSS readers shouldn't interpret CSS
classes), whereas before some RSS readers may have allowed the style elements.
I think RSS feeds aren't supposed to be styled, so that's probably OK.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="rss-feed"&gt;
&lt;h2&gt;RSS feed&lt;a class="section-link" href="#rss-feed"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I reduced the size of the RSS feed by only including summaries for old posts,
rather than the full content. Also, if I write too many more posts, the RSS
feed will stop including the oldest posts. I really prefer when RSS feeds
include the full content with each post, but I think that's less useful for
very old posts.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="markdown"&gt;
&lt;h2&gt;Markdown renderer&lt;a class="section-link" href="#markdown"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Many of these pages are generated from Markdown. I added support for it in 2014
using &lt;a href="https://python-markdown.github.io/"&gt;Python-Markdown&lt;/a&gt;, which was probably
the obvious choice. Since then, some folks standardized Markdown as
&lt;a href="https://commonmark.org/"&gt;CommonMark&lt;/a&gt;, which is used widely in VS Code and
GitHub (GitHub Flavored Markdown extends CommonMark), among others. The
differences between Python-Markdown and CommonMark are small but annoying, so
I've switched to &lt;a href="https://markdown-it-py.readthedocs.io/"&gt;markdown-it-py&lt;/a&gt;.
Since this website is generated as static pages, it was relatively easy to diff
the HTML and RSS across this change for manual review.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="mako-conflicts"&gt;
&lt;h2&gt;Mako parsing conflicts&lt;a class="section-link" href="#mako-conflicts"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This website contains both HTML and Markdown pages and templates. The Markdown
pages are processed through the
&lt;a href="https://www.makotemplates.org/"&gt;Mako templating engine&lt;/a&gt; and then the
Markdown renderer. The HTML pages are just processed through Mako. Most of what
I use Mako for is basic variable substitution, loops, and if statements. It
also allows running full Python code inside templates, which I like. (I write
the templates, so I can live with myself abusing them. I wouldn't want this in
a larger team project.)&lt;/p&gt;
&lt;p&gt;&lt;a href="https://docs.makotemplates.org/en/latest/syntax.html"&gt;Mako's syntax&lt;/a&gt; is
usually not in conflict with Markdown or HTML, but I've found three exceptions
where there are ambiguities:&lt;/p&gt;
&lt;section id="variable-ambiguity"&gt;
&lt;h3&gt;&lt;code&gt;${variable}&lt;/code&gt;&lt;a class="section-link" href="#variable-ambiguity"&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Mako interprets the occasional &lt;code&gt;${variable}&lt;/code&gt; in a shell
script (or &lt;em&gt;this&lt;/em&gt; paragraph) as a Mako variable substitution. Sometimes I want
one behavior (Mako) and sometimes I want the other (literal). Fortunately, when
this happens, it's unlikely that the variable exists in Mako's scope, so it
usually causes a build error.&lt;/p&gt;
&lt;p&gt;I haven't found an easy way to escape &lt;code&gt;${variable}&lt;/code&gt; locally.
The best approach is to use
&lt;a href="https://docs.makotemplates.org/en/latest/syntax.html#text"&gt;&lt;code&gt;&amp;lt;%text&amp;gt;&lt;/code&gt;&lt;/a&gt;
to opt out of Mako for an entire section. Otherwise, using &lt;code&gt;&amp;amp;dollar;&lt;/code&gt; is an
option in HTML, but not within a &lt;code&gt;`code block`&lt;/code&gt; in Markdown
(&lt;a href="https://spec.commonmark.org/0.31.2/#example-35"&gt;see CommonMark example&lt;/a&gt;).
Another approach is to define a Mako variable called &lt;code&gt;dollar&lt;/code&gt; with a value of
&lt;code&gt;$&lt;/code&gt;, then write &lt;code&gt;${dollar}{variable}&lt;/code&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="backslash-ambiguity"&gt;
&lt;h3&gt;Trailing backslash&lt;a class="section-link" href="#backslash-ambiguity"&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Mako consumes a trailing backslash at the end of a line and merges the line
below it. Many programming languages use this syntax too. It's easy to forget
about this and get poor rendering of code blocks. Similar to the dollar issue,
&lt;code&gt;&amp;lt;%text&amp;gt;&lt;/code&gt; is a good way to disable it, or you can use &lt;code&gt;&amp;amp;#92;&lt;/code&gt;
in HTML but not Markdown, or you can define a backslash variable.&lt;/p&gt;
&lt;p&gt;I found that I was only using the trailing backslash feature of Mako in one
place, so I wanted to turn it off and have a default that won't keep biting me.
Unfortunately, substituting &lt;code&gt;\\&lt;/code&gt; for &lt;code&gt;\&lt;/code&gt; at the end of the line doesn't help,
as consuming the second backslash is
&lt;a href="https://github.com/sqlalchemy/mako/blob/0348fe3/mako/lexer.py#L381"&gt;baked into Mako's lexer&lt;/a&gt;.
Instead, I added a workaround that replaces a trailing &lt;code&gt;\&lt;/code&gt; with a unique string
before Mako runs and then replaces that string back to a &lt;code&gt;\&lt;/code&gt; after Mako runs. I
think that should in most contexts, including code and non-code and
Mako-enabled and Mako-disabled regions. This does completely prevent using a
trailing backslash in Mako and Mako Python blocks
(&lt;code&gt;&amp;lt;% ... %&amp;gt;&lt;/code&gt;), but those are usually unnecessary. If I forget
and try to use a trailing backslash in Mako, it's likely to cause a build
error, which is the behavior I want.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="hash-hash-ambiguity"&gt;
&lt;h3&gt;Leading hashes&lt;a class="section-link" href="#hash-hash-ambiguity"&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Mako interprets &lt;code&gt;##&lt;/code&gt; at the start of a line as a comment, which Markdown uses
for &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt;. I found out about this one almost immediately after starting to use
Markdown on this site. I don't need that style of Mako comments, so I wanted to
disable them.&lt;/p&gt;
&lt;p&gt;I've had a workaround in place for a while that pre-processed the input and
injected &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt; tags before Mako ran. That worked well for me, but in theory it
would break &lt;code&gt;##&lt;/code&gt; if used in code blocks or in Mako Python blocks
(&lt;code&gt;&amp;lt;% ... %&amp;gt;&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;I've updated my workaround to replace a leading &lt;code&gt;##&lt;/code&gt; with &lt;code&gt;#&lt;/code&gt; followed by a
unique string before Mako runs, and then replace it back after Mako runs. I
think that'll work in all contexts.&lt;/p&gt;
&lt;/section&gt;
&lt;/section&gt;
&lt;section id="metadata"&gt;
&lt;h2&gt;Social sharing metadata&lt;a class="section-link" href="#metadata"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I added some meta tags for social media. The
&lt;a href="https://ogp.me/"&gt;Open Graph Protocol&lt;/a&gt;
metadata is used by Meta,
&lt;a href="https://www.linkedin.com/help/linkedin/answer/a521928/making-your-website-shareable-on-linkedin?lang=en"&gt;LinkedIn&lt;/a&gt;,
and others, but that site is
&lt;a href="https://github.com/facebookarchive/open-graph-protocol"&gt;archived on GitHub&lt;/a&gt;.
&lt;a href="https://developers.facebook.com/docs/sharing/webmasters"&gt;Facebook's page&lt;/a&gt; is
another resource. Twitter has
&lt;a href="https://developer.x.com/en/docs/x-for-websites/cards/guides/getting-started"&gt;its own metadata&lt;/a&gt;
but will largely use Open Graph metadata if available.
&lt;a href="https://blog.joinmastodon.org/2024/07/highlighting-journalism-on-mastodon/"&gt;&lt;code&gt;fediverse:creator&lt;/code&gt;&lt;/a&gt;
is used on Mastodon.&lt;/p&gt;
&lt;p&gt;Note: there's a
&lt;a href="https://stackoverflow.com/questions/22350105/whats-the-difference-between-meta-name-and-meta-property"&gt;technical distinction&lt;/a&gt;
between:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;&lt;span class="hl-p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="hl-nt"&gt;meta&lt;/span&gt; &lt;span class="hl-na"&gt;name&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;&amp;quot;...&amp;quot;&lt;/span&gt; &lt;span class="hl-na"&gt;content&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;&amp;quot;...&amp;quot;&lt;/span&gt;&lt;span class="hl-p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;&lt;span class="hl-p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="hl-nt"&gt;meta&lt;/span&gt; &lt;span class="hl-na"&gt;property&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;&amp;quot;...&amp;quot;&lt;/span&gt; &lt;span class="hl-na"&gt;content&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;&amp;quot;...&amp;quot;&lt;/span&gt;&lt;span class="hl-p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;where some fields want one or the other.&lt;/p&gt;
&lt;p&gt;This metadata required some code generator changes. Similar to the page title,
the metadata is set above where the main page content is rendered. That
information has to be &amp;quot;pushed up&amp;quot; to be available outside the main content.
Also, the pages had to be made aware of their URLs for &lt;code&gt;og:url&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The OpenGraph Protocol requires &lt;code&gt;og:image&lt;/code&gt; to be set, yet most of my pages
don't have an image. I created a larger version of the
&lt;a href="https://en.wikipedia.org/wiki/Favicon"&gt;favicon&lt;/a&gt; as a default.&lt;/p&gt;
&lt;p&gt;Here's an example of a link to this blog post on Mastodon:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://ongardie.net/var/blog/website-updates-2024/mastodon-share.png" alt="Mastodon share example" /&gt;&lt;/p&gt;
&lt;/section&gt;
&lt;section id="favicon"&gt;
&lt;h2&gt;Favicon&lt;a class="section-link" href="#favicon"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Since I redrew the &lt;a href="https://en.wikipedia.org/wiki/Favicon"&gt;favicon&lt;/a&gt; as an SVG
for the social sharing image, I also added the SVG for browsers that support
it. I created a dark mode variant, but I don't think mainstream browsers
support that yet (see the links from
&lt;a href="https://github.com/whatwg/html/issues/6408"&gt;this issue&lt;/a&gt;). Firefox seems to
ignore the &lt;code&gt;prefers-color-scheme&lt;/code&gt; and take the last SVG. Other browsers may
default to the first SVG, so I have sandwiched the dark favicon declaration in
between two light ones:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;&lt;span class="hl-p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="hl-nt"&gt;link&lt;/span&gt; &lt;span class="hl-na"&gt;rel&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;&amp;quot;icon&amp;quot;&lt;/span&gt; &lt;span class="hl-na"&gt;type&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;&amp;quot;image/svg+xml&amp;quot;&lt;/span&gt; &lt;span class="hl-na"&gt;sizes&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;&amp;quot;any&amp;quot;&lt;/span&gt;
    &lt;span class="hl-na"&gt;media&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;&amp;quot;(prefers-color-scheme: light)&amp;quot;&lt;/span&gt; &lt;span class="hl-na"&gt;href&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;&amp;quot;favicon-light.svg&amp;quot;&lt;/span&gt; &lt;span class="hl-p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="hl-p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="hl-nt"&gt;link&lt;/span&gt; &lt;span class="hl-na"&gt;rel&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;&amp;quot;icon&amp;quot;&lt;/span&gt; &lt;span class="hl-na"&gt;type&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;&amp;quot;image/svg+xml&amp;quot;&lt;/span&gt; &lt;span class="hl-na"&gt;sizes&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;&amp;quot;any&amp;quot;&lt;/span&gt;
    &lt;span class="hl-na"&gt;media&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;&amp;quot;(prefers-color-scheme: dark)&amp;quot;&lt;/span&gt; &lt;span class="hl-na"&gt;href&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;&amp;quot;favicon-dark.svg&amp;quot;&lt;/span&gt; &lt;span class="hl-p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="hl-p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="hl-nt"&gt;link&lt;/span&gt; &lt;span class="hl-na"&gt;rel&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;&amp;quot;icon&amp;quot;&lt;/span&gt; &lt;span class="hl-na"&gt;type&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;&amp;quot;image/svg+xml&amp;quot;&lt;/span&gt; &lt;span class="hl-na"&gt;sizes&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;&amp;quot;any&amp;quot;&lt;/span&gt;
    &lt;span class="hl-na"&gt;media&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;&amp;quot;(prefers-color-scheme: light)&amp;quot;&lt;/span&gt; &lt;span class="hl-na"&gt;href&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;&amp;quot;favicon-light.svg&amp;quot;&lt;/span&gt; &lt;span class="hl-p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="hl-p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="hl-nt"&gt;link&lt;/span&gt; &lt;span class="hl-na"&gt;rel&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;&amp;quot;icon&amp;quot;&lt;/span&gt; &lt;span class="hl-na"&gt;type&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;&amp;quot;image/png&amp;quot;&lt;/span&gt; &lt;span class="hl-na"&gt;sizes&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;&amp;quot;16x16&amp;quot;&lt;/span&gt;
    &lt;span class="hl-na"&gt;href&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;&amp;quot;favicon.png&amp;quot;&lt;/span&gt; &lt;span class="hl-p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hopefully dark mode favicons will start working in more browsers over the next
few years.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="heading-links"&gt;
&lt;h2&gt;Heading links&lt;a class="section-link" href="#html-css"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I added links to many headings to make sharing URLs that jump to a place on the
page easier. On mobile, you can see an example &lt;code&gt;#&lt;/code&gt; link above this paragraph.
On desktop, hover over the heading to see it.&lt;/p&gt;
&lt;p&gt;I chose not to make the entire heading a link because sometimes headings do
legitimately link to things, and that could get confusing. I like that the &lt;code&gt;#&lt;/code&gt;
is hidden until hovering on desktop, but hovering is not really an available
gesture on touch-screens.&lt;/p&gt;
&lt;p&gt;I didn't want the RSS feed to include these links because they might not work,
so the &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; tags have no content in the HTML, and CSS fills in the &lt;code&gt;#&lt;/code&gt;. Since
RSS feeds don't use the CSS, they will just get an invisible empty link.&lt;/p&gt;
&lt;p&gt;I added these links manually to a bunch of headings. I didn't see a good way to
automate that, since the value of the &lt;code&gt;id&lt;/code&gt; attribute is worth customizing and
the placement would not be obvious to an algorithm. I ended up adding a bunch
of
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/section"&gt;&lt;code&gt;&amp;lt;section&amp;gt;&lt;/code&gt; elements&lt;/a&gt;
(2015) for this.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="html-css"&gt;
&lt;h2&gt;HTML and CSS updates&lt;a class="section-link" href="#html-css"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I updated and modernized some HTML and CSS, including use of these newer
features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/var"&gt;&lt;code&gt;var()&lt;/code&gt; and custom properties&lt;/a&gt;
(2017) are useful for defining theme colors for light/dark modes.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:has"&gt;&lt;code&gt;:has()&lt;/code&gt;&lt;/a&gt; (2023) was
useful in styling just the &lt;code&gt;&amp;lt;pre&amp;gt;&lt;/code&gt; elements that contain &lt;code&gt;&amp;lt;code&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:is"&gt;&lt;code&gt;:is()&lt;/code&gt;&lt;/a&gt; (2021) helped
save some duplication for styles related to &lt;code&gt;h1&lt;/code&gt;-&lt;code&gt;h6&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:not"&gt;&lt;code&gt;:not()&lt;/code&gt;&lt;/a&gt; (2021)
helped keep some styles self-contained.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/box-sizing"&gt;&lt;code&gt;box-sizing: border-box&lt;/code&gt;&lt;/a&gt;
(2015) is old news but I started this website in 2007.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For testing dark mode and the styling updates, it was convenient to have the
blog index page render the entire contents of the blog on one page that I could
scroll through quickly.&lt;/p&gt;
&lt;p&gt;I tried to keep the website usable with older phones. For example, I assume iOS
users have Safari 15 but not necessarily Safari 16 or newer yet. These are some
CSS features that look nice but that I've avoided for now until they're more
widely available:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@scope"&gt;&lt;code&gt;@scope&lt;/code&gt;&lt;/a&gt; (not
available yet in Firefox) would be helpful to create a namespace for
Pygments' classes.&lt;/li&gt;
&lt;li&gt;&lt;a href=""&gt;nesting&lt;/a&gt; (2023) and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Nesting_selector"&gt;&lt;code&gt;&amp;amp;&lt;/code&gt; nesting selector&lt;/a&gt; (2023) would improve readability significantly.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media"&gt;Range comparisons in &lt;code&gt;@media&lt;/code&gt; queries&lt;/a&gt;
(2023) would be a little easier to read.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/light-dark"&gt;&lt;code&gt;light-dark&lt;/code&gt;&lt;/a&gt;
(2024) might avoid some variable definitions for dark mode.&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
</description><guid isPermaLink="true">https://ongardie.net/blog/website-updates-2024/</guid><pubDate>Sun, 24 Nov 2024 03:03:00 GMT</pubDate></item><item><title>Command Not Found</title><link>https://ongardie.net/blog/command-not-found/</link><description>&lt;p&gt;On Debian/Ubuntu, &lt;code&gt;command-not-found&lt;/code&gt; tells you what package to install when
you try to run a program you don't have. I find this helpful, but it takes a
long time to maintain its index for lookups. This post tells the meandering
story of how I first optimized &lt;code&gt;command-not-found&lt;/code&gt;, then replaced it with a
script that doesn't use an index at all.&lt;/p&gt;

                &lt;p&gt;&lt;a href="https://ongardie.net/blog/command-not-found/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/command-not-found/</guid><pubDate>Sat, 16 Jan 2021 01:12:00 GMT</pubDate></item><item><title>Website Updates (2020)</title><link>https://ongardie.net/blog/website-updates/</link><description>&lt;p&gt;The code for this website has been running without any major updates since 2009.
Back then, I wrote it as a Python 2 program using FastCGI, served originally by
&lt;a href="https://www.lighttpd.net/"&gt;lighttpd&lt;/a&gt; and more recently by
&lt;a href="https://caddyserver.com/"&gt;Caddy&lt;/a&gt;.&lt;/p&gt;

                &lt;p&gt;&lt;a href="https://ongardie.net/blog/website-updates/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/website-updates/</guid><pubDate>Fri, 07 Aug 2020 23:30:00 GMT</pubDate></item><item><title>LogCabin 1.1</title><link>https://ongardie.net/blog/logcabin-1.1/</link><description>&lt;p&gt;Announcing the release of LogCabin 1.1! This is the second stable release of the
&lt;a href="https://github.com/logcabin/logcabin"&gt;LogCabin&lt;/a&gt; coordination service, which
includes a C++ implementation of the
&lt;a href="https://raft.github.io"&gt;Raft consensus algorithm&lt;/a&gt;.&lt;/p&gt;

                &lt;p&gt;&lt;a href="https://ongardie.net/blog/logcabin-1.1/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/logcabin-1.1/</guid><pubDate>Sun, 26 Jul 2015 18:27:00 GMT</pubDate></item><item><title>LogCabin 1.0</title><link>https://ongardie.net/blog/logcabin-1.0/</link><description>&lt;p&gt;LogCabin 1.0 is out! This is the first stable release of the
&lt;a href="https://github.com/logcabin/logcabin"&gt;LogCabin&lt;/a&gt; coordination service, which
includes a C++ implementation of the
&lt;a href="https://raft.github.io"&gt;Raft consensus algorithm&lt;/a&gt;. If you're new
to these, I recently spoke about Raft and a little about LogCabin at the
&lt;a href="https://www.meetup.com/Sourcegraph-Tech-Talks/events/221199291/"&gt;Sourcegraph Hacker
Meetup&lt;/a&gt; in
San Francisco; watch the &lt;a href="https://www.youtube.com/watch?v=2dfSOFqOhOU"&gt;video&lt;/a&gt; for a visual
walk-through of how Raft works.&lt;/p&gt;

                &lt;p&gt;&lt;a href="https://ongardie.net/blog/logcabin-1.0/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/logcabin-1.0/</guid><pubDate>Fri, 01 May 2015 23:57:00 GMT</pubDate></item><item><title>LogCabin.appendEntry(6, "Preparing for 1.0")</title><link>https://ongardie.net/blog/logcabin-2015-04-03/</link><description>&lt;p&gt;This is the sixth in a series of blog posts detailing the ongoing development
of &lt;a href="https://github.com/logcabin/logcabin"&gt;LogCabin&lt;/a&gt;. This entry describes
progress towards the upcoming 1.0.0 release of LogCabin, a useful new
command-line client to access LogCabin, and several other improvements.&lt;/p&gt;

                &lt;p&gt;&lt;a href="https://ongardie.net/blog/logcabin-2015-04-03/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/logcabin-2015-04-03/</guid><pubDate>Fri, 03 Apr 2015 22:06:00 GMT</pubDate></item><item><title>LevelDB with Transactions on Node.js</title><link>https://ongardie.net/blog/node-leveldb-transactions/</link><description>&lt;p&gt;This post surveys the libraries available in Node.js for LevelDB, shows that
support for transactions is missing, and lays out a path to get there. In
short, we need the Node bindings for LevelDB to expose snapshots, then we can
build transactions with snapshot isolation on top without much trouble.&lt;/p&gt;

                &lt;p&gt;&lt;a href="https://ongardie.net/blog/node-leveldb-transactions/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/node-leveldb-transactions/</guid><pubDate>Wed, 11 Mar 2015 00:17:00 GMT</pubDate></item><item><title>LogCabin.appendEntry(5, "Cluster Clock, etc")</title><link>https://ongardie.net/blog/logcabin-2015-02-27/</link><description>&lt;p&gt;This is the fifth in a series of blog posts detailing the ongoing development
of &lt;a href="https://github.com/logcabin/logcabin"&gt;LogCabin&lt;/a&gt;. This entry describes a new
cluster-wide monotonic clock used for client session expiry, a new tool to dump
out the contents of a LogCabin server's log and snapshot, a couple of
performance improvements, and several changes around how server IDs and
addresses are assigned and used.&lt;/p&gt;

                &lt;p&gt;&lt;a href="https://ongardie.net/blog/logcabin-2015-02-27/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/logcabin-2015-02-27/</guid><pubDate>Sat, 28 Feb 2015 02:24:00 GMT</pubDate></item><item><title>LogCabin.appendEntry(4, "SegmentedLog")</title><link>https://ongardie.net/blog/logcabin-2015-02-11/</link><description>&lt;p&gt;This is the fourth in a series of blog posts detailing the ongoing development
of &lt;a href="https://github.com/logcabin/logcabin"&gt;LogCabin&lt;/a&gt;. This entry describes
LogCabin's new storage module and several other recent improvements.&lt;/p&gt;

                &lt;p&gt;&lt;a href="https://ongardie.net/blog/logcabin-2015-02-11/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/logcabin-2015-02-11/</guid><pubDate>Thu, 12 Feb 2015 03:49:00 GMT</pubDate></item><item><title>Stanford.edu Email Account Deleted</title><link>https://ongardie.net/blog/stanford-email/</link><description>&lt;p&gt;Now that I've graduated, Stanford has deleted my university email account,
&lt;code&gt;ongaro@stanford.edu&lt;/code&gt;. If you tried to send to that address and received an
autoreply, please resend your email to the same username at &lt;code&gt;cs.stanford.edu&lt;/code&gt;
instead, which should remain valid for life. (Yes, I had a &lt;a href="https://ongardie.net/blog/rice-email"&gt;very similar
post&lt;/a&gt; for my Rice email account a few years
ago.)&lt;/p&gt;
</description><guid isPermaLink="true">https://ongardie.net/blog/stanford-email/</guid><pubDate>Tue, 27 Jan 2015 19:15:00 GMT</pubDate></item><item><title>LogCabin.appendEntry(3, "Server Stats")</title><link>https://ongardie.net/blog/logcabin-2015-01-19/</link><description>&lt;p&gt;This is the third in a series of blog posts detailing the ongoing development
of &lt;a href="https://github.com/logcabin/logcabin"&gt;LogCabin&lt;/a&gt;. This entry describes a new
tool to extract information from LogCabin servers and a tricky bug that
occurred when unregistering handlers from the event loop.&lt;/p&gt;

                &lt;p&gt;&lt;a href="https://ongardie.net/blog/logcabin-2015-01-19/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/logcabin-2015-01-19/</guid><pubDate>Tue, 20 Jan 2015 05:02:00 GMT</pubDate></item><item><title>LogCabin.appendEntry(2, "Timeouts")</title><link>https://ongardie.net/blog/logcabin-2015-01-05/</link><description>&lt;p&gt;This is the second in a series of blog posts detailing the ongoing development
of &lt;a href="https://github.com/logcabin/logcabin"&gt;LogCabin&lt;/a&gt;. This entry describes the
battle of adding timeouts to the client library API. Timeouts are useful for
implementing leases in client applications. For example, a client might want to
assert its lease but give up after few seconds, and in case of a timeout, it
might need to crash or stop other processes from doing things that may no
longer be safe.&lt;/p&gt;

                &lt;p&gt;&lt;a href="https://ongardie.net/blog/logcabin-2015-01-05/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/logcabin-2015-01-05/</guid><pubDate>Tue, 06 Jan 2015 00:23:00 GMT</pubDate></item><item><title>LogCabin.appendEntry(1, "Hello, world!")</title><link>https://ongardie.net/blog/logcabin-2014-12-16/</link><description>&lt;p&gt;This is the first in a series of blog posts detailing the ongoing development
of &lt;a href="https://github.com/logcabin/logcabin"&gt;LogCabin&lt;/a&gt;. This first entry catches
up on the developments from when I started working with &lt;a href="https://www.scalecomputing.com"&gt;Scale
Computing&lt;/a&gt; in November, so it's longer than most
of the future updates will be.&lt;/p&gt;
&lt;p&gt;The theme of this entry is getting started in a new environment. Up until now,
I'd done nearly all of the development of LogCabin on my laptop and on the
RAMCloud cluster. Running it somewhere new uncovered a bunch of implicit
assumptions baked-in about the environment, so it exposed a new set of issues
and bugs. This is fairly inevitable when it comes to low-level systems code,
and there's a lot of value in working through it. LogCabin is significantly
easier to run now than it was before, and it should be easier for the rest of
you to install on your systems, too.&lt;/p&gt;

                &lt;p&gt;&lt;a href="https://ongardie.net/blog/logcabin-2014-12-16/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/logcabin-2014-12-16/</guid><pubDate>Tue, 16 Dec 2014 21:20:00 GMT</pubDate></item><item><title>Grad School</title><link>https://ongardie.net/blog/phd/</link><description>&lt;p&gt;&lt;img src="https://ongardie.net/var/blog/phd/hooding.jpg" alt="Ousterhout placing academic hood on Ongaro" /&gt;&lt;/p&gt;
&lt;p&gt;Well, I spent the last five years getting my Ph.D. in Stanford's Computer
Science department. I won't do that justice here, but I'll fill in the story
briefly so that subsequent posts make sense. I was part of
&lt;a href="https://www.stanford.edu/~ouster/"&gt;Professor John Ousterhout&lt;/a&gt;'s group,
which is primarily focused on
&lt;a href="https://web.stanford.edu/~ouster/cgi-bin/projects.php#ramcloud"&gt;RAMCloud&lt;/a&gt;,
a large-scale in-memory distributed storage system.&lt;/p&gt;

                &lt;p&gt;&lt;a href="https://ongardie.net/blog/phd/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/phd/</guid><pubDate>Fri, 12 Dec 2014 04:55:00 GMT</pubDate></item><item><title>Tips for Running PowerPoint in CrossOver Office</title><link>https://ongardie.net/blog/powerpnt/</link><description>&lt;p&gt;
  I use Linux exclusively but run Microsoft PowerPoint for some presentations.
  I won't go into the details of why here, but I wanted to record and share a
  few tips I use for getting it to run well. Your mileage may vary, obviously.
&lt;/p&gt;


                &lt;p&gt;&lt;a href="https://ongardie.net/blog/powerpnt/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/powerpnt/</guid><pubDate>Wed, 23 Apr 2014 00:16:00 GMT</pubDate></item><item><title>Manual Window Placement in i3 (Part 2)</title><link>https://ongardie.net/blog/i3-manual-placement2/</link><description>&lt;p&gt;
  This is the second part of a series on making the i3 window manager work the
  way I want. I left off
  &lt;a href="https://ongardie.net/blog/i3-manual-placement"&gt;last time&lt;/a&gt;
  with the goal of changing the way windows are placed as they are created, and
  I had a couple of pointers from the i3 hacking howto for where to start
  looking. This post covers how I've set up my test environment.
&lt;/p&gt;


                &lt;p&gt;&lt;a href="https://ongardie.net/blog/i3-manual-placement2/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/i3-manual-placement2/</guid><pubDate>Tue, 07 May 2013 04:39:00 GMT</pubDate></item><item><title>Manual Window Placement in i3 (Part 1)</title><link>https://ongardie.net/blog/i3-manual-placement/</link><description>&lt;p&gt;
  I've been using
  &lt;a href="https://en.wikipedia.org/wiki/Tiling_window_manager"
    &gt;tiling window managers&lt;/a
  &gt;
  for the past couple of years. I started with
  &lt;a href="https://en.wikipedia.org/wiki/Awesome_(window_manager)"&gt;awesome&lt;/a&gt;,
  then &lt;a href="https://notionwm.net/"&gt;Notion&lt;/a&gt; (a fork of
  &lt;a href="https://en.wikipedia.org/wiki/Ion_(window_manager)"&gt;Ion&lt;/a&gt;; Ion is
  no longer maintained), and now I'm in the process of moving to
  &lt;a href="https://en.wikipedia.org/wiki/I3_(window_manager)"&gt;i3&lt;/a&gt;. For those
  of you that aren't familiar with it, the screenshots all look the same. They
  all behave differently, though, and I guess you just have to find one that
  fits your mental model.
&lt;/p&gt;

&lt;p&gt;
  When you open a new window in most tiling window managers, your existing
  windows get rearranged or resized to make room for it. This is kind of one
  main idea, actually, and it works reasonably well when opening your second or
  third window. Beyond two or three, depending on the screen size and
  applications, it starts to suck.
&lt;/p&gt;


                &lt;p&gt;&lt;a href="https://ongardie.net/blog/i3-manual-placement/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/i3-manual-placement/</guid><pubDate>Wed, 01 May 2013 07:22:00 GMT</pubDate></item><item><title>Is It Worth the Time?</title><link>https://ongardie.net/blog/worth-the-time/</link><description>&lt;p&gt;
  Today's &lt;a href="https://xkcd.com"&gt;XKCD&lt;/a&gt; starts to answer: how much time
  should I spend making a routine task faster?
&lt;/p&gt;

&lt;a href="https://xkcd.com/1205/"
  &gt;&lt;img
    src="https://imgs.xkcd.com/comics/is_it_worth_the_time.png"
    alt="XKCD 1205: Is It Worth the Time?"
/&gt;&lt;/a&gt;


                &lt;p&gt;&lt;a href="https://ongardie.net/blog/worth-the-time/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/worth-the-time/</guid><pubDate>Mon, 29 Apr 2013 23:38:00 GMT</pubDate></item><item><title>Bullets in Inkscape</title><link>https://ongardie.net/blog/inkscape-bullets/</link><description>&lt;p&gt;&lt;a href="https://inkscape.org"&gt;Inkscape&lt;/a&gt; is a good open-source drawing program for
vector graphics. I'm currently using it to make a research poster, but
unfortunately, Inkscape doesn't do bullets. This post discusses your options if
you want to use bullets in your Inkscape drawing and introduces a simple
Inkscape extension that makes this much easier.&lt;/p&gt;

                &lt;p&gt;&lt;a href="https://ongardie.net/blog/inkscape-bullets/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/inkscape-bullets/</guid><pubDate>Sun, 14 Apr 2013 03:12:00 GMT</pubDate></item><item><title>Color GDB Prompt</title><link>https://ongardie.net/blog/gdb-color/</link><description>&lt;p&gt;To add color to your GDB prompt, place the following in your &lt;code&gt;~/.gdbinit&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set prompt \033[0;33m(gdb)\033[0m\040
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes it easier to find your place visually.&lt;/p&gt;
</description><guid isPermaLink="true">https://ongardie.net/blog/gdb-color/</guid><pubDate>Sun, 07 Apr 2013 21:50:00 GMT</pubDate></item><item><title>Leveraging Web Technologies for Local Programs</title><link>https://ongardie.net/blog/node-webkit/</link><description>Client-side web programming (HTML, JavaScript, CSS) has improved greatly in recent years. Meanwhile, node.js has become a reasonable platform for server-side JavaScript that interacts with the outside world. This environment already makes sense for building local applications. The node-webkit project takes this one step forward: it integrates the node.js libraries into a Webkit environment.
                &lt;p&gt;&lt;a href="https://ongardie.net/blog/node-webkit/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/node-webkit/</guid><pubDate>Thu, 04 Apr 2013 04:38:00 GMT</pubDate></item><item><title>SQLite Database in Git</title><link>https://ongardie.net/blog/sqlite-in-git/</link><description>&lt;p&gt;
  I store nearly all files of even moderate importance in
  &lt;a href="https://git-scm.com"&gt;Git&lt;/a&gt; (including this blog post). These are
  usually plain-text files, but sometimes it's necessary to put binary files
  under version control. Unfortunately, those are typically difficult to diff
  and merge, but I recently discovered some features of Git that make this less
  painful. This blog post focuses on
  &lt;a href="https://www.sqlite.org/index.html"&gt;SQLite&lt;/a&gt;
  database files, but at least some of it applies to other binary file types.
&lt;/p&gt;


                &lt;p&gt;&lt;a href="https://ongardie.net/blog/sqlite-in-git/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/sqlite-in-git/</guid><pubDate>Thu, 04 Apr 2013 02:16:00 GMT</pubDate></item><item><title>rlwrap: readline Wrapper Program</title><link>https://ongardie.net/blog/rlwrap/</link><description>&lt;p&gt;
  Dealing with a basic command-line prompt in a loop can be painful, so many
  programs, such as shells, interactive programming languages and debuggers,
  provide a more featureful prompt. For example, pressing the up and down
  arrows in a good prompt will flip through previously entered input lines.
  Programs will often make use of the
  &lt;a href="https://www.gnu.org/s/readline/"&gt;GNU readline library&lt;/a&gt; for this
  functionality.
&lt;/p&gt;

&lt;p&gt;
  If you need to use a program that only has a basic prompt, you may be able to
  wrap it with the program
  &lt;a href="https://github.com/hanslub42/rlwrap"&gt;rlwrap&lt;/a&gt; to get some more
  advanced features. From the man page:
&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;
    rlwrap runs the specified command, intercepting user input in order to
    provide readline's line editing, persistent history and completion.
  &lt;/p&gt;
  &lt;p&gt;[...]&lt;/p&gt;
  &lt;p&gt;
    There are many options to add (programmable) completion, handle multi-line
    input, colour and re-write prompts. If you don't need them (and you
    probably don't), you can skip the rest of this manpage.
  &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;
  For example, I recently used rlwrap with jdb, the Java debugger, and Ikarus,
  a Scheme compiler.
&lt;/p&gt;
</description><guid isPermaLink="true">https://ongardie.net/blog/rlwrap/</guid><pubDate>Thu, 29 Dec 2011 01:56:00 GMT</pubDate></item><item><title>The Cost of Exceptions in C++</title><link>https://ongardie.net/blog/exceptions/</link><description>&lt;p&gt;Most people seem to have an opinion as to whether exceptions in
&lt;span style="white-space: nowrap"&gt;C++&lt;/span&gt; are slow or fast, but very few
people have put any useful numbers out there. Here's a lower bound.&lt;/p&gt;

                &lt;p&gt;&lt;a href="https://ongardie.net/blog/exceptions/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/exceptions/</guid><pubDate>Thu, 10 Nov 2011 19:25:00 GMT</pubDate></item><item><title>Book Log</title><link>https://ongardie.net/blog/book-log/</link><description>&lt;p&gt;
  In addition to my &lt;a href="https://ongardie.net/misc/movies/"&gt;movie log&lt;/a&gt;, I've
  now started a &lt;a href="https://ongardie.net/misc/books/"&gt;book log&lt;/a&gt; going back to
  last summer.
&lt;/p&gt;


                &lt;p&gt;&lt;a href="https://ongardie.net/blog/book-log/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/book-log/</guid><pubDate>Sun, 14 Feb 2010 05:19:00 GMT</pubDate></item><item><title>Rice.edu Email Account Deleted</title><link>https://ongardie.net/blog/rice-email/</link><description>&lt;p&gt;
  Rice has deleted my undergraduate email account,
  &lt;code&gt;diego.ongaro@rice.edu&lt;/code&gt;,
  since I am no longer a student there. If you tried to send to that address
  and received a bounce notification, please resend your email to the same
  username at &lt;code&gt;alumni.rice.edu&lt;/code&gt; instead.
&lt;/p&gt;
</description><guid isPermaLink="true">https://ongardie.net/blog/rice-email/</guid><pubDate>Mon, 30 Nov 2009 07:21:00 GMT</pubDate></item><item><title>Twin Peaks iPhone Panorama</title><link>https://ongardie.net/blog/twin-peaks/</link><description>&lt;p&gt;
  I went up to
  &lt;a
    href="https://en.wikipedia.org/wiki/Twin_Peaks_(San_Francisco,_California)"
    &gt;Twin Peaks&lt;/a
  &gt;
  in San Francisco with &lt;a href="https://github.com/jicksta"&gt;Jay&lt;/a&gt; a few
  weeks ago. It was a nice view but kind of a worst-case scenario for a photo:
  my iPhone camera (VGA), poor lighting as the sun was setting, and stong
  winds.
&lt;/p&gt;
&lt;p&gt;
  That day I took a bunch of overlapping shots with my phone. Then I used the
  &lt;a href="https://www.gimp.org"&gt;GIMP&lt;/a&gt;'s automatic white balance correction
  on each of them. Next I stitched them together with
  &lt;a href="https://hugin.sourceforge.io/"&gt;Hugin&lt;/a&gt;, and finally I edited the
  stitched image with the GIMP. The following mediocre image is the result (&lt;a
    href="https://ongardie.net/var/blog/twin-peaks/pan_large.jpg"
    &gt;click&lt;/a
  &gt;
  for the full 1534x652 image):
&lt;/p&gt;

&lt;a href="https://ongardie.net/var/blog/twin-peaks/pan_large.jpg"&gt;
  &lt;img src="https://ongardie.net/var/blog/twin-peaks/pan_small.jpg" alt="panorama" /&gt;
&lt;/a&gt;


                &lt;p&gt;&lt;a href="https://ongardie.net/blog/twin-peaks/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/twin-peaks/</guid><pubDate>Sun, 08 Nov 2009 02:27:00 GMT</pubDate></item><item><title>Xfce Stopwatch Plugin</title><link>https://ongardie.net/blog/stopwatch/</link><description>&lt;p&gt;
  I needed an excuse to try &lt;a href="http://blog.m8t.in/"&gt;Mike&lt;/a&gt;'s
  &lt;a href="https://git.xfce.org/bindings/xfce4-vala/"&gt;Vala bindings for Xfce&lt;/a
  &gt;, so I created a new little plugin for the panel, the
  &lt;a href="https://docs.xfce.org/panel-plugins/xfce4-stopwatch-plugin"
    &gt;xfce4-stopwatch-plugin&lt;/a
  &gt;.
&lt;/p&gt;

&lt;p&gt;In the original release announcement on July 28th, I wrote:&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;
    This is the first release of the stopwatch panel plugin, which you can use
    to time yourself on different tasks. It's stable and usable, but quite
    minimal still.
  &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;
  The functionality is best summarized with this image from the web site:
&lt;/p&gt;

&lt;img src="https://ongardie.net/var/blog/stopwatch/help.png" alt="screenshots" /&gt;


                &lt;p&gt;&lt;a href="https://ongardie.net/blog/stopwatch/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/stopwatch/</guid><pubDate>Mon, 17 Aug 2009 03:49:00 GMT</pubDate></item><item><title>Lighttpd Fails to Bind to Localhost</title><link>https://ongardie.net/blog/lighttpd-bind-localhost/</link><description>&lt;img
  style="float: left"
  src="https://ongardie.net/var/blog/lighttpd-bind-localhost/lighttpd_logo.png"
  alt="lighttpd logo"
/&gt;

&lt;p&gt;
  I installed the web server &lt;a href="https://www.lighttpd.net"&gt;lighttpd&lt;/a&gt;
  on my laptop to test some configuration settings. As I didn't want to expose
  the server on the network, I uncommented
  &lt;code&gt;server.bind = "localhost"&lt;/code&gt; from
  &lt;code&gt;/etc/lighttpd/lighttpd.conf&lt;/code&gt;.
&lt;/p&gt;

&lt;p style="clear: both"&gt;
  Then, restarting lighttpd failed with the following error:
&lt;/p&gt;
&lt;pre&gt;
&lt;span style="display:none"&gt;(network.c.201)&lt;/span&gt;getaddrinfo failed:  Name or service not known ' localhost '
&lt;/pre&gt;


                &lt;p&gt;&lt;a href="https://ongardie.net/blog/lighttpd-bind-localhost/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/lighttpd-bind-localhost/</guid><pubDate>Wed, 05 Aug 2009 15:21:00 GMT</pubDate></item><item><title>Chef Roger's Knife List</title><link>https://ongardie.net/blog/chef-roger-knives/</link><description>&lt;a
  style="float: right"
  href="https://commons.wikimedia.org/wiki/File:Chef's_Knife.jpg"
  &gt;&lt;img
    src="https://ongardie.net/var/blog/chef-roger-knives/chefs_knife.jpg"
    alt="chef's knife"
/&gt;&lt;/a&gt;

&lt;p&gt;
  Last semester at Rice, I took the class Cooking with Chef Roger. The man is
  passionate about his knives, and he gave us a list of brands he recommends.
  From one of the few scraps of notes that survived, here is Chef Roger
  Elkhouri's list of quality brands for chef's knives (French knives).
&lt;/p&gt;


                &lt;p&gt;&lt;a href="https://ongardie.net/blog/chef-roger-knives/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/chef-roger-knives/</guid><pubDate>Sun, 02 Aug 2009 17:56:00 GMT</pubDate></item><item><title>Cgit Hacking</title><link>https://ongardie.net/blog/cgit-hacking/</link><description>&lt;img
  src="https://ongardie.net/var/blog/cgit-hacking/cgit-logo.png"
  alt="cgit logo"
  style="float: right"
/&gt;
&lt;p&gt;
  Last week I hacked a couple new features into
  &lt;a href="https://git.zx2c4.com/cgit/about/"&gt;cgit&lt;/a&gt;, a web interface for
  Git, since it's the one I [previously used] on ongardie.net. I added
  &lt;code&gt;https://&lt;/code&gt; URLs for the Atom feed and also syntax highlighting
  when viewing files.
&lt;/p&gt;


                &lt;p&gt;&lt;a href="https://ongardie.net/blog/cgit-hacking/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/cgit-hacking/</guid><pubDate>Wed, 17 Jun 2009 16:45:00 GMT</pubDate></item><item><title>Tabs in Vim</title><link>https://ongardie.net/blog/vim-tabs/</link><description>&lt;img
  src="https://ongardie.net/var/blog/vim-tabs/vim-logo.png"
  alt="Vim logo"
  style="float: right"
/&gt;
&lt;p&gt;
  Version 7 of Vim introduced tabs to the editor, and these are a few of my
  tab-related tips.
&lt;/p&gt;


                &lt;p&gt;&lt;a href="https://ongardie.net/blog/vim-tabs/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/vim-tabs/</guid><pubDate>Fri, 29 May 2009 04:51:00 GMT</pubDate></item><item><title>Overlooked Python Built-Ins</title><link>https://ongardie.net/blog/python-builtins/</link><description>&lt;img src="https://ongardie.net/var/blog/python-builtins/python-logo.png" alt="Python logo" style="float:right" /&gt;
&lt;p&gt;So, I just realized that I re-implemented two built-in Python functions on a
small project I'm working on for &lt;a href="https://etszone.com"&gt;ETSZONE&lt;/a&gt;. I just
didn't know that these existed, so I'm writing about them here in case you've
overlooked them too.&lt;/p&gt;

                &lt;p&gt;&lt;a href="https://ongardie.net/blog/python-builtins/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/python-builtins/</guid><pubDate>Sat, 23 May 2009 04:52:00 GMT</pubDate></item><item><title>Extract Unique Lines From a File</title><link>https://ongardie.net/blog/sort-uniq/</link><description>How to get rid of duplicate lines from a file or pipe.
                &lt;p&gt;&lt;a href="https://ongardie.net/blog/sort-uniq/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/sort-uniq/</guid><pubDate>Fri, 08 May 2009 04:52:00 GMT</pubDate></item><item><title>Off-Brand Q-tips</title><link>https://ongardie.net/blog/q-tips/</link><description>&lt;p&gt;
  To start off this blog, I'm writing about things you stick in your ear. I
  suspect I'll end up writing about techier subjects soon enough. Nevertheless,
  it's probably worthwhile to &lt;em&gt;attempt&lt;/em&gt; to set a precedent of, at least
  occasionally, writing about something low-tech.
&lt;/p&gt;


                &lt;p&gt;&lt;a href="https://ongardie.net/blog/q-tips/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;
            </description><guid isPermaLink="true">https://ongardie.net/blog/q-tips/</guid><pubDate>Sat, 29 Dec 2007 05:50:00 GMT</pubDate></item></channel></rss>