EINVAL: Valid solutions for invalid problems

Skeletons in the Closet

After my previous post I continued to dig deeper into the Emacs skeleton system. Not all of it was beneficial for my sanity, so strap in and let me explain some more cursed parts of it!

Just as before, all the examples should be ready to evaluate within Emacs. Consider reading this post in eww so that C-x C-e is readily available.

As explained in the previous post, within a skeleton str represents the value taken from the skeleton interactor, usually an interactive prompt. A quick example:

(with-temp-buffer
  (skeleton-insert
   '("Name: "
     "Hello, my name is " str ".  Nice to meet you!"))
  (buffer-string))

It can be used more than once and seemingly even as an argument of a function (because why not!).

(with-temp-buffer
  (skeleton-insert
   '("Name: "
     "Hello, my name is " str ".  Nice to meet you!  "
     "Yours truly, " (upcase str) "."))
  (buffer-string))

As it turns out, this isn’t entirely true. Let’s try removing the first mention of str from the previous example.

(with-temp-buffer
  (skeleton-insert
   '("Name: "
     "Hello, nice to meet you!  "
     "Yours truly, " (upcase str) "."))
  (buffer-string))

This is where the cursed nature of str shows its face. Once str is set (i.e. it’s been used at least once already within that skeleton), it’s effectively a regular variable1. Before that, it expands to roughly the following list:

(setq str (skeleton-read "Name: "))

Let me stress that this is a list, not code that gets evaluated. Or rather not yet. This utilizes the fact that when skeleton receives a list as its element, it evaluates it2 and uses the result as the actual skeleton element. If str is used as a top-level element of a skeleton, this behavior makes it work. But if it’s inside a function call, the first occurrence of str won’t work as expected as it’s literally just this unevaluated list.

As a result, in our example upcase gets passed a list, not a string. How do we fix it? We eval it ourselves!

(with-temp-buffer
  (skeleton-insert
   '("Name: "
     "Hello, nice to meet you!  "
     "Yours truly, " (upcase (eval str)) "."))
  (buffer-string))

This can be also written like this, so all the str can be freely reordered without worrying about the special case of the first one:

(with-temp-buffer
  (skeleton-insert
   '("Name: "
     '(eval str)
     "Hello, nice to meet you!  "
     "Yours truly, " (upcase str) "."))
  (buffer-string))

This last skeleton evaluates the form that sets str for all its future references, and discards the result.

I’m still processing how cursed this is but I’m glad I have finally understood the weird str semantics I already encountered before. Thank you for joining me on this short journey!


  1. Inspect the skeleton-internal-list function for the details. ↩︎

  2. Or treats it as a subskeleton if the list doesn’t look like a function call, as explained in the previous post. ↩︎