DEV Community

Giovanni Crisalfi
Giovanni Crisalfi

Posted on

Get user inputs in Emacs Lisp

Originally posted on zwitterio.it

This post is intended to give beginners (or anyone else) an introduction on how to use lisp to manage user's inputs on Emacs.

A simple interactive function

Imagine you want to write a simple interactive function to cheer up an Emacs' user.

(defun cheer-me-up ()
  (interactive)
  (message "You're great, keep it up!"))

(cheer-me-up)
Enter fullscreen mode Exit fullscreen mode

Everytime we launch this one, the same result will be showed in the minibuffer:

"You’re great, keep it up!"
Enter fullscreen mode Exit fullscreen mode

It works, but it can be boring on the long run.
Enhance the function by proposing a new catchphrase every time you launch it.

(defun cheer-me-up (goodvibes)
  (interactive "sEnter good vibes as string: ")
  (message goodvibes))
Enter fullscreen mode Exit fullscreen mode

This way, the function will reflect your communication and optimism on yourself, by repeating exactly the expression you pass.

Every time the function will be launched by the user, the minibuffer of Emacs will ask for good vibes.

That's pretty basic, because we're just asking for a string and rethrowing it to the user without any check.

But what if the user isn't writing good stuff on the first place?
Maybe he needs this function precisely because it's all melancholical and gloomy.

Demand good vibes

We could introduce a check that excludes bad words from the input and replace it with good ones, but it wouldn't be easy.
Doable, but not easy.
Let's keep it simple for the moment: what if we ask the user something else?
Not a phrase, for example, but a number?

We could list a great number of positive phrases and ask the user for a number, which will be the index of the phrase in the list.
In this case, the input should be an integer.
How do we do that?

First of all, let's declare a list named goodvibes:

;; list of positive phrases
(setq goodvibes
      '("You're great!" "Keep it up!" "People loves you!"
        ;; It's better to comment out the following ones
        ;; "Shut up, nothing you say has value" "The world is an insane fabric of pain and we should destroy everything"
        ;; Ok, we can keep the last one
        "Nature is good, she's not a cruel mother and you must be happy to be alive. Of course."))
Enter fullscreen mode Exit fullscreen mode

Now we have a total of 4 elements in the list and we want to access them from the function.
The function asks for an index, which should be an integer between 0 and 3.
What if the user doesn't know anything about the list up here and asks for a fifth element?
What if the user gives us a float number insted of an integer? Or, worst, what if the user writes off a numberless string?

(defun cheer-me-up (index)
  (interactive "sChoose a number, receive good vibes~ ")
  ;; We are asking for a string from the user, so this could be anything.
  ;; We must be sure it's an integer.
  <<format-input-as-integer>>

  ;; Isn't it possible to convert the string to an integer?
  ;; Tell the user!
  <<signal-no-number-error>>

  ;; Now we need to know the integer is not inferior to 0 or superior to 3
  (if (and (> index 0) (<= index 3))
      ;; if we pass the condition, good, print out your message
      (message (nth index goodvibes))
    ;; Else block:
    ;; if the number exceeds the range from a way or another, tell the user.
    (<<signal-out-of-range-error>>)))
Enter fullscreen mode Exit fullscreen mode

To manage those cases, we need to know two concepts:

You might say:

But reading documentation is bOrInG!

Don't worry, this time we will explore the topics practically here below; just follow me through the comments.

Formatting strings

Formatting as integer is pretty simple if you know how to format strings. From the docs:

Formatting means constructing a string by substituting computed values at various places in a constant string.

In this case, we want to require that the value is of a particular type and it can be checked specifying a format. %d, for example,

replace the specification with the base-ten representation of a signed integer. The object can also be a floating-point number that is formatted as an integer, dropping any fraction.

So, if the user gives us 1, we're fine. And what if the user gives us 1.0? We're fine too! The floating point will be converted automatically to the closest integer. This means that 1.1 or 1.2 will be both converted to 1 too. In other context, you could manage those cases differently, because if the user is giving you floating points, maybe the decimal means something and you want to tell them something's wrong. This case is simple, so we will be chill about that.

;; assign the parsed value to the index var itself;
;; we're not going to need the raw input anymore;
(setq index (format "%d" index))
Enter fullscreen mode Exit fullscreen mode

Of course, if the user gives us something that cannot be in any way interpreted as an integer, we have to tell them. To be honest, this part is already managed by the format command itself.

If you supply a value that doesn’t fit the requirements, an error is signaled.

So, we don't have to implement this code by ourselves:

;; If user's input is not a number, the `format` function will tell them;
;; We can proceed with our function.
Enter fullscreen mode Exit fullscreen mode

Throwing an error

We still cannot vibe with our function results because another block, the last one, is missing.

What if the index is not in the desired range?
Of course, an error would be displayed this time too, but we need to explain clearly to the user what they have to do to not triggering it.

Signaling an error means beginning error processing. Error processing normally aborts all or part of the running program and returns to a point that is set up to handle the error (see How Emacs Processes Errors).

Errors are managed by error function (what a shock, isn't it?). A typical use of error from the docs:

(error "That is an error -- try something else")
     error That is an error -- try something else
Enter fullscreen mode Exit fullscreen mode

We take inspiration for our use-case:

(error "Out of range error -- choose a number between 0 and 3.")
Enter fullscreen mode Exit fullscreen mode

As nv-elisp points out on Reddit,

When using message you'll want to provide the format-string argument. Otherwise the user's strings will fail if they include format specifiers. e.g. if your goodvibes list includes "%dont mind me%", the %d will be interpreted as part of the format-string argument and will throw an error.

For an error caused by user input, it's better to use user-error. This will prevent the debugger from being hit any time a user inputs an out of range number.

So, in this case it would be more advisable to write user-error instead of simply error:

(user-error "Out of range error -- choose a number between 0 and 3.")
Enter fullscreen mode Exit fullscreen mode

Wrapping up

We learned how to use format and error, two fundamental functions to manage user's inputs or for writing lisp for Emacs in general.
In particular, we should remember that:

  • error stops the process and throws up the specified error to the user;
  • format can have various specifications like:
    • %s, for common strings;
    • %d, for integers;
    • %f, for "floats".

To be absolutely correct, I should remember you that there's not a single type of integers or floats or strings; for example, strictly abiding to what the docs are saying, %f refers to "the decimal-point notation for a floating-point number"; you can store floats in other ways, if you need.

Coming back to our function, that's how it finally appears:

(defun cheer-me-up (index)
  (interactive "sChoose a number, receive good vibes~ ")
  ;; We are asking for a string from the user, so this could be anything.
  ;; We must be sure it's an integer.
  ;; assign the parsed value to the index var itself;
  ;; we're not going to need the raw input anymore;
  (setq index (format "%d" index))

  ;; Isn't it possible to convert the string to an integer?
  ;; Tell the user!
  ;; If user's input is not a number, the `format` function will tell them;
  ;; We can proceed with our function.

  ;; Now we need to know the integer is not inferior to 0 or superior to 3
  (if (and (> index 0) (<= index 3))
      ;; if we pass the condition, good, print out your message
      (message (nth index goodvibes))
    ;; Else block:
    ;; if the number exceeds the range from a way or another, tell the user.
    ((user-error "Out of range error -- choose a number between 0 and 3."))))
Enter fullscreen mode Exit fullscreen mode

This is the moment for a confession: in order to teach how the format function works, I somehow have disguised you. The interactive function, in fact, gives the chance to filter the values in place! That's what the "s" before "Choose" is there for (see Code Characters for interactive in the docs to learn more)
Knowing this, we could write everything more concisely, like that:

(defun cheer-me-up (index)
  (interactive "nChoose a number, take good vibes~ ")
  (if (and (> index 0) (<= index 3))
      (message (nth index goodvibes))
    ((user-error "Out of range error -- choose a number between 0 and 3."))))
Enter fullscreen mode Exit fullscreen mode

I started with the "n" character, because it means you want to take a number as an argument.
The only difference from before is that this new function lacks the previous tolerance for floats, but I don't see this as a flaw.

Watch out!
format specifications and interactive first character's meanings are not exactly superimposable!
Using "f" in interactive means asking for a file, not for a float.

Keep it up!

Hoping this post was an enjoyable passage in your path through lispian parenthesis, I wish you good luck for your journey.

(nth 1 goodvibes)
;; => "Keep it up!"
Enter fullscreen mode Exit fullscreen mode

(and a big thanks to Arialdo for his help in tracking down my typos)

Top comments (0)