An ugly alias


I made a shortcut for posting on my new microblog:

\alias nug='nuggets_path=$HOME/wonger.dev/dist/nuggets.html

export new_nugget=$(
  cat $nuggets_path | 
    htmlq .card:first-of-type -r '\''.card > *:not(.bottom-line)'\'' | 
    sed '\''s/datetime=".*"/datetime="'\''$(date +%Y-%m-%d)'\''"/'\'' | 
    perl -pe '\''s/(#|n)([0-9]+)/$1.($2+1)/e'\'' 
) 

vim $nuggets_path \
  -c /class=\"card \
  -c "let @/ = \"\"" \
  -c "normal O" \
  -c "r !echo \"\$new_nugget\"" \
  -c "normal 2o" \
  -c "normal 6ki<p></p>" \
  -c "normal ^f<" \
  -c startinsert'

It's ugly, I know. But it's a nice opportunity to share what I've learned about shell scripting.

OVERVIEW a

The goal: easily add a new entry to the microblog.

The old way: open nuggets.html, copy-paste the latest entry, manually change the ID and date, then start typing.

This takes me ~50 keystrokes, or ~8 steps: vim /path/to/nuggets.html, then search for class="nugget", then copy around tag, then paste above, then goto inner tag and delete, then goto both IDs and increment them, then figure out today's date and change the datestamp.

Normally I don't mind copy-paste. I write all the HTML on this site by hand. I go manual until it hurts. But nuggets are supposed to be a low-friction outlet for me, so I need to eliminate these steps.

The new way: nug, then start typing. So effortless! (screencast.mp4)

STARTING WITH HTMLQ a

htmlq is a tool for extracting bits of HTML. I used htmlq to automate the "copy" part of my copy-paste workflow:

cat nuggets.html |
  htmlq '.nugget:first-of-type'
  --remove-nodes '.nugget > *:not(.bottom-line)'

That means "select the first .nugget and remove all inner elements except the .bottom-line". Here's what the query returns:

<div class="nugget" id="n14">

  <div class="bottom-line">
    #14<time datetime="2025-01-28"></time>
  </div>
</div>

I feel like I should say more about htmlq, but there's nothing else to explain. It takes CSS selectors and spits out HTML. It's boring and I love it.

FIND AND REPLACE WITH REGULAR EXPRESSIONS a

The previous command returned a nugget, but the nugget had an old ID and date. This command replaces the date:

sed 's/datetime=".*"/datetime="'$(date +%Y-%m-%d)'"/'

And this command increments the IDs:

perl -pe 's/(#|n)([0-9]+)/$1.($2+1)/e'

Do these commands look intimidating? The first one isn't bad if you ignore the quotation marks. The command follows the format s/find/replace/. It finds the pattern datetime="<anything>" and replaces it with datetime="<today>".

The second command is a little more advanced. It means "anytime the character # or n is followed by a number, increase the number by one". In pseudocode:

let $1 = find # or n
let $2 = the string of digits immediately afterwards
return "$1" + "($2 + 1)"

I would've used sed for this substitution, but sed cannot perform arithmetic operations.

I tried awk too. Like sed, awk can do substitution, along with math, control flow, functions, and variables. But I couldn't fit this substitution into a simple one-liner. The closest I got was gawk '{ match($0, /(#|n)([0-9]+)/, groups); print gensub(/(#|n)([0-9]+)/, "\\1" groups[2]+1, "g")}' which felt gross.

I ended up finding a perl solution. Why did I waste time writing clumsy awk? Apparently, the Perl nerds have been saying this for decades, but I didn't hear them until now.

VIM STARTUP COMMANDS a

The final part of the alias automates the "paste" part of my copy-paste workflow. Vim inserts the $new_nugget above the other nuggets and opens with the cursor at a specfic position.

Here's the command I used:

vim nuggets.html \
  -c '/class="nugget"' \
  -c 'let @/ = ""' \
  -c 'normal O' \
  -c 'read !echo "$new_nugget"' \
  -c 'normal 4ki<p></p>' \
  -c 'normal ^f<'

This means "goto the first occurrence of class="nugget", clear the search register, insert a line above, insert the contents of $new_nugget, move four lines up, insert a paragraph element, and move to the second occurrence of < on that line."

The -c argument gives you complete control over vim. It's essentially a command to send keypresses at startup. I never knew this feature existed until today. There are other ways to control vim at startup, too. Check out :h startup-options.

Here's what vim looks like after the first -c argument. Pretend the solid rectangle is the cursor:

...
<div █lass="nugget" id="n14">
  <p>just a test, don't mind me</p>
  <div class="bottom-line">
    #14<time datetime="2025-01-28"></time>
  </div>
</div>
...

Here's the final result, ready for me to start typing:

...
<div class="nugget" id="n15">
  <p>█/p>
  <div class="bottom-line">
    #15<time datetime="2025-01-29"></time>
  </div>
</div>
<div class="nugget" id="n14">
  <p>just a test, don't mind me</p>
  <div class="bottom-line">
    #14<time datetime="2025-01-28"></time>
  </div>
</div>
...

BASH ALIAS TIPS a

I used to avoid bash because I didn't understand it. Now, having written this post and learned a few things, ...I still don't like bash. But I can endure it. Here are the things I learned while writing aliases:

1) WRAP WITH SINGLE QUOTES, NOT DOUBLE QUOTES

I wrap all my aliases in single quotes. Double quotes could work too, but they often behave unexpectedly. Consider these examples:

alias date1="echo $(date)"   # bad
alias date2='echo $(date)'   # good
alias date3="echo \$(date)"  # good

In the first alias, $(date) surprisingly evaluates when bash is initialized. In the other aliases, $(date) evaluates as expected at runtime.

To confirm this, run alias. It prints all active aliases:

$ alias
...
alias date1='echo Sun Feb  9 11:00:59 AM EST 2025'
alias date2='echo $(date)'
alias date3='echo $(date)'
...

2) NESTED SINGLE QUOTES ARE ACTUALLY ESCAPED AND APPENDED

If a command contains single quotes, then it's a pain to wrap the command in another pair of single quotes. For example, this command:

sed 's/find/replace/'

...becomes this alias:

alias uglier='sed '\''s/find/replace/'\'''

Contrary to most programming languages, the middle quotes are not actually nested. Instead, the original string terminates, a single escaped quote is appended, and a new string begins.

That makes a little more sense if you know about string concatenation in bash. These are all equivalent:

'hello world'
hello\ world
hello' 'world
'hello'' ''world'
''hell'o w'orl'd'

If it's any consolation, you don't have to manually escape commands with single quotes. The shell automatically wraps and escapes quoted expressions in the parameter expansion ${parameter@Q}. For example:

$ myvar="sed 's/find/replace/'"
$ echo ${myvar@Q}
'sed '\''s/find/replace/'\'''

3) ALIASES CAN ACCEPT ARGUMENTS

Naysayers claim aliases are inferior to functions. They point out that aliases such as alias wontwork='echo $1 $2' cannot accept arguments.

Well, aliases actually can accept arguments, despite the naysayers. The trick is to declare a temporary function. In programming lingo, this is essentially an anonymous function, or a lambda:

$ alias myalias='f(){ for arg in "$@"; do echo "$arg"; done; unset -f f; }; f'
$ myalias arg1 arg2
arg1
arg2

4) AN EASIER ALIAS WORKFLOW

After writing aliases in my .bashrc, I have to remember to source ~/.bashrc so the aliases take effect. I made a shortcut so I don't have to remember that step:

alias aliases='vim ~/.bashrc -c "normal G"; source ~/.bashrc'

I have a similar gripe with the alias command. When I create aliases from the command line, I must rewrite them in my .bashrc if I want them to be saved permanently. I made a shortcut to skip that step too:

\alias alias='f(){
    _trigger="${1%%=*}"
    _command="${1#*=}"
    _quoted_command="${_command@Q}"
    echo "\\alias $_trigger=$_quoted_command" >> ~/.bashrc
    source ~/.bashrc
    unset -f f
}; f'

This replaces the builtin alias with my own implementation. Now when I create an alias from the command line, the alias is automatically appended to my .bashrc.

The builtin command can still accessed with \alias. In fact, all my aliases must use that prefix, or else they trigger an infinite loop. It's a small price to pay for a greater convenience.

REFLECTIONS... a

ON BASH

Apologies for all that code vomit. Bash is ugly, but it's important. It's the glue of so many programming environments. Sticking with bash exemplifies one of my software philosophies: use popular old tools.

On the other hand, bash suffers from a thousand papercuts. People pad bash with all sorts of bandaids, such as shellcheck linting, fzf autocompletion, and atuin history management, on top of .bashrc customizations like prompt messages and aliases. So I might return to friendly fish for my interactive shell and reserve bash for scripts.

A few final notes about shell scripting. 1) I'm not an expert. I'm just sharing what works and what makes sense to me. 2) I realize this alias could've been a vimscript, an editor snippet, or a number of other implementations. 3) I say bash throughout this page, but I'm often referring to the POSIX-subset of shell syntax, and only occasionaly referring to bash-only syntax sugar. 4) If you're using hipster shells like nushell or elvish, let me know. I'm curious how they feel as daily drivers.

ON AUTOMATION

As always, I'm thinking about xkcd 1205 — will the time saved using nug be greater than the time spent writing nug?

Yes, actually! I plan on frequent nugget-posting, which will add up to entire minutes of savings. /⁠nuggets is the first corner of my website where I can write freely. Anything goes. I have so much to share.

Also: automation is not always about saving time. Shortcuts reduce cognitive load, too. That's the stuffy way of saying "I'm crawling out from months of icky, foggy burnout, and I had zero energy to toil with computers, so please don't make me think too hard." Maybe it only takes thirty seconds to create an un-aliased nugget. But thirty seconds of sustained focus and decisionmaking could take just enough activation energy to turn me off from posting altogether.

I'm thankful to have highly configurable tools like vim and the shell environment. I can tweak them until everything is perfectly cozy. I think a lot of programmers feel that way — control freaks, in a way.

ON MICROBLOGGING

It's great! I have so much to say about short-form posting. But this page is already too long. I'll see you on the microblog instead :)