Shower Thought: Snippets were the original "IDE"
(The title is kind of a misnomer. Everyone knows UNIX was actually the original IDE, of course. But I didn't know what else to call it.)
Basically since I've started writing code, I've used some sort of editor that supported snippets. I never bothered with the feature. It just didn't seem really that useful to me when features like auto-complete (including IntelliSense-ish completion) exist and work extremely well for writing boilerplate code fast.
Recently though I had a Shower Thought™️ about snippets. I was thinking that I wanted to start collecting a "quote book" of code that I like or think is neat. This has both aesthetic and practical motivations. Sometimes I just like the way something looks and instead of bookmarking it, I want to do a little bit more to unpack it, so I started writing about it. On the other hand, I've been writing a lot of Bash and Ansible recently, and those two languages (systems?) are so fraught with edge cases, gotchas, papercuts, footguns, quirks, and tons of other nouns that you really need to have Google up constantly asking, "What is the best way to do this?"
A good example is reading files line-by-line in Bash. One might think you can do this:
cat << EOF > file
hello
world
how are you
doing today?
EOF
lineno="0"
IFS=$'\n'
for line in $(cat file); do
printf "Line %2d: %s\n" $lineno $line;
lineno=$(( $lineno + 1 ));
done
But nope! That prints:
Line 0: hello
Line 1: world
Line 2: how
Line 3: are
Line 4: you
Line 5: doing
Line 6: today?
So then you go to Google and figure out $IFS
exists, and re-write to this:
IFS=$'\n'
for line in $(cat file); do
printf "Line %2d: %s\n" $lineno $line;
lineno=$(( $lineno + 1 ));
done
And that gives something that looks OK:
Line 0: hello
Line 1: world
Line 2: how are you
Line 3: doing today?
Until you get a file with special characters, or need to have the final newline, or some other case, at which point you Google and find this StackOverflow page which gives you something like this monstrosity:
while IFS='' read -r line || [ -n "$line" ]; do
printf "Line %2d: %s\n" $lineno $line;
lineno=$(( $lineno + 1 ));
done < file
I will be upfront with you. I will never, ever remember how to do this first-try. The syntax is truly abhorrent in every way1. If you asked me to do this in Bash with no assistance from the Internet, a friendly *nix greybeard, or an IDE, then I would probably rather give up and re-write the entire script in Python. And I've been writing Bash weekly for several years.
So, quote book it is, then I can remember a list of all these FUN and HELPFUL little TRICKS to get Bash to do what is simple and easy in literally every other scripting language. Great! But then I have to tab out of the editor and find another window, dig through Obsidian or a folder structure or whatever I would be using, select all the text, copy and paste it back into the editor, and edit to fit my use case.
Snippets are the perfect application for this, and I feel kind of silly saying it, but having never used them I didn't even consider it until I started making the quote book. Once I made my first Sublime Text snippet, though, I converted the quote book into a set of snippets and never looked back. I have about 30 now and use them fairly regularly.
I then started making them for things that are maybe a little silly, but in
a "hey, whatever works 🤷♂️" kind of way. Let's return to Ansible. In Ansible,
the ansible.builtin.file
task is an extremely common one to use, as it lets
you create and manipulate files. For some reason the input parameter is src
and the output is dest
. dest
!? Every time I go to type this I think it's
supposed to be dst
to match src
and then I get an error when running my
playbook.
I looked around briefly for an Ansible language server or plugin or something
that would fix these problems. But YAML is not a programming language, and I'm
honestly a little tired of installing and configuring the N
th new language
server of the week. When I write Ansible playbooks, I tend to bounce between
several different machines (duh) and sometimes they don't always have fancy
tools installed. Additionaly, the Ansible documentation is not bad, but it's a
little tedious to browse. The devdocs.io
page for it is actually abysmal,
which is rare as I usually like those.
Creating a basic snippet which globs in a file
block with the correct
indentation, comments, and a list of common options took literally seconds of
copy-and-pasting out of the documentation. It looks like this:
<snippet>
<content><![CDATA[
- name: $1
ansible.builtin.template:
src: "$2"
dest: "$3"
$0
# Force re-applying the template, even if 'dest' already exists
force: true
# Create a backup if 'dest' already exists
backup: true
# Permissions of 'dest'
mode: u=rw,g=r
# ... other options redacted for readability
]]></content>
<!-- Optional: Set a tabTrigger to define how to trigger the snippet -->
<tabTrigger>!file</tabTrigger>
<!-- Optional: Set a scope to limit where the snippet will trigger -->
<scope>source.yaml</scope>
</snippet>
(In case you're also a snippet noob like myself, the $
markers indicate where
your cursor will go when you press Tab
, allowing you to quickly edit the
snippet block. $0
is the last place you will end up if you keep pressing
Tab
. I have it located above a list of options I use less frequently, so if I
don't need them I can quickly delete the lines. There's more fun to be had, but
you can go read the documentation for your favorite editor, I won't rehash
it.)
It's stupid simple, easy, fast, and effective, definitely moreso than tabbing
out to documentation and maybe even faster than using a linter. The snippet
itself is just a text file, so you reap all the benefits of version control,
etc. It's even portable to other snippet formats with only a little work, as
most use the $
convention for field insertion. The UI of ST4 (and probably
other editors) makes searching, browsing, and creating snippets pretty easy.
Snippets for the win!
At this point it occured to me that snippets are sort of like a pre-modern IDE. Before we had tools like IntelliJ which basically writes Java for you, I imagine people made heavy use of snippets. Snippets are so much easier to implement, I would think they appeared well before other kinds of tooling. People may have even used - gasp! - paper books with code examples in them, which I guess are the really OG version of snippets.
I wasn't programming in the 90's but now I'm very curious to know if this workflow was more common, and if it is dying down as IDEs and languages get better at real-time code generation and auto-complete. It kind of tickles me pink to imagine an old programmer with a dog-eared and tabbed notebook, flipping between pages to remember how to write that one crazy Bash incantation. I feel like I've been missing out 😅 But maybe snippets were always a rather niche use case, and people prefer to hammer out their boilerplate in other ways.
Man, this could be a blog post in and of itself, but: 1) The thing you're iterating over comes after the entire loop, which is unintuitive because every other language uses
for (item in collection) { loop-body }
orcollection.map(...)
, where the thing you are iterating over comes first. 2) You also use a backwards redirect which I rarely see in shell scripts, so I forget sometimes you can do that. 3)$IFS
is not a very intuitive variable name for a delimiter; I believe it stands for internal field separator but I would normally think of it as a line separator or a delimiter like a normal human being. 4) Depending on how you write the script you may or may not have to restore the previous value of$IFS
or you break stuff. Fun! 5) Don't get me started onfor-do-done
vs.case-in-esac
vsif-then-fi
, but in all fairness that's something I don't often forget because it is such a basic part of bash, so the snippet isn't really helping us fix that one issue. 6) At least on my machine,man read
redirects you tobuiltin(1)
which doesn't actually state the meaning of the-r
flag. What does it do? Time to Google! Or waste a few minutes readingbash(1)
and realizing it's not there, readingzsh(1)
and realizing it's not there, openingzshbuiltins(1)
, and then paging through 700 results after you type/read
. 7) When callingread
,line
is passed as a token but later it is a variable so has to be referenced as$line
. 8) I'm honestly not really sure what|| [ -n "$line" ]
even does even after using this snippet for some time, I guessread
can return a non-zero exit code even if it does read some new data and put it into$line
, which is just peachy.↩