An Introduction to Emacs Lisp: Working with Files and Their Attributes

A hands-on introduction to the Emacs Lisp scripting language and working with files in Emacs

Dane Bulat
21 min readJan 31, 2021

Introduction

Emacs Lisp is a scripting language built into Emacs. Most of the editing functionality Emacs provides is implemented in Emacs Lisp in the form of callable functions. As well as being able to call any built-in Emacs function, it is also possible to define functions ourselves using the Emacs Lisp scripting language.

This article provides an introduction to working with the Emacs Lisp language as a whole. We go through how to develop several functions that deal with accessing and outputting items on the file system. Essential features of the Emacs Lisp language will be discussed along the way, such as defining functions, creating variables, looping, and list manipulation to name a few. With this in mind, each section of the article can serve as either an introduction or refresher on using certain features of Emacs Lisp. In addition to fundamental language features, we will also take advantage of built-in Emacs functions that deal with the file system.

Going into more detail, we will implement some functions that deal with retrieving and outputting the attributes of a requested file, directory, or symbolic link on the file system. These attributes convey important information such as the item’s owner, the group it belongs to, its byte size, permissions, and so on — very similar to what is output after running the ls -l command in a terminal. At a high level, we will use Emacs Lisp to develop functions that do the following tasks:

  • Retrieve the name of a requested file along with its attributes.
  • Format output correctly such that information is easy to read.
  • Output file and directory information to the *Messages* buffer.

Our functions will also be interactive, which means that they can be invoked just like any other command in Emacs.

Source Code

A Git repository has been set up on Github to accompany this article. You can find all the discussed code in a single source file called file-attributes.el, which can be downloaded in a couple of ways.

One option is to invoke cURL on the command line to download the file without its parent repository:

$ curl -L https://raw.githubusercontent.com/danebulat/elisp-samples/master/file-attributes/file-attributes.el > file-attributes.el

Alternatively, invoke git if you would like to download the entire elisp-samples repository. Keep in mind that this repository may contain additional directories and source code for other published Emacs Lisp articles:

$ git clone https://github.com/danebulat/elisp-samples

Feel free to refer to the source code whilst reading through this article.

Emacs Refresher

This section will serve as a quick refresher on how to accomplish certain tasks within Emacs and Doom Emacs — the Emacs distribution I currently use for development. if you are completely new to Emacs, I certainly recommend installing Doom Emacs, especially if you plan on following this tutorial.

Doom Emacs includes many fantastic development modules and sensible default key bindings out-of-the-box. If you are a developer who wants an open source, extensible, and configurable IDE, Doom Emacs will be my number one recommendation. The getting started guide on Github is a great resource for setting it up on your system.

If you are using another distribution of Emacs or your own configuration, I trust you will be able to find the correct key bindings for certain commands mentioned in this article.

Evaluating Emacs Lisp Code

Functions and variables written in Emacs Lisp must be installed from within an Emacs session before they can be accessed. In other words, functions and variables must be evaluated by the Emacs Lisp interpreter so they can be utilized in either source code or via a regular Emacs command.

We will firstly take a look at evaluating a variable. Variables in Emacs Lisp are usually created by making a call to the setq function. A variable name along with its initial value are passed as arguments to this function. Its syntax can be demonstrated with some pseudo code:

(setq variable-name initial-value)

Go ahead and create an Emacs Lisp file called something like intro.el so we can evaluate some code. Doom Emacs will recognize that files with an .el extension contain Emacs Lisp source code. Consequently, a major mode called Emacs Lisp mode will be enabled for the buffer, which will add functionality such as syntax highlighting, correct tabbing behavior, and a completion menu that will pop up as we type Emacs Lisp code.

Let’s make a call to the setq function to define a variable called greeting and initialize it to the string "Hello, world!". Notice that strings in Emacs Lisp are surrounded by double quotes:

(setq greeting "Hello, world!")

You will quickly realize that parenthesis are used extensively in Emacs Lisp. This is because everything we define in Emacs Lisp is in fact a list, and a list is always enclosed in opening and closing parenthesis. In addition to variables, other language facilities such function definitions, if statements, and loop constructs are also defined using list syntax. The Emacs Lisp interpreter (the program which evaluates and runs our code) has the job of deducing what each list in our source code represents.

Whitespace also separates a function’s name and its subsequent arguments. It is therefore important to remember that specifying a comma in between arguments will cause the interpreter to throw an error.

Another interesting feature within Emacs Lisp mode is the ability to evaluate source code directly from within a source file. Individual lines, blocks, or functions may be evaluated by issuing the appropriate command. For example, to evaluate our greeting variable in Doom Emacs, place your cursor on the line where it is defined — the convention is to navigate your cursor to the last closing parenthesis on the line. Then press the key combination gr followed by RETURN to invoke a command called +eval:region. A floating line of output will display the return value of the expression that was just evaluated.

If you have evil mode enabled, press S-% (hold the Shift key and press the key that is mapped to the percent sign, which is usually 5) to move your cursor to the opening parenthesis of a list. Repeat this combination to move the cursor to the closing parenthesis of a list.

Now our greeting variable has been evaluated, it can be referenced in any other part of the source file. For example, we can pass it to the message function to perform some additional formatting:

(message "The value of greeting variable: %s" greeting)

This line of code calls the message function and outputs our greeting string along with some additional text before it. Notice that we include the %s format specifier in the first argument to act as a string placeholder. Our variable is passed as the second argument to fill in this placeholder. Moving your cursor to the closing parenthesis and invoking gr will output our new message.

We know how to evaluate single lines of code, but what if we have a function that is comprised of many lines of code? Let’s define a simple function and take a look at how we can evaluate it in our source file. The syntax for defining functions in Emacs Lisp is displayed below:

(defun function-name (arg1, arg2)
"Document string explaining ARG1 and ARG2."
;; body
)

As with every other language item in Emacs Lisp, functions are enclosed in parenthesis. The defun macro is specified immediately after the opening parenthesis. This keyword lets the interpreter know that it should evaluate the list definition as a function.

The function’s name is specified next, followed by its arguments in another set of nested parenthesis. If a function doesn’t require any arguments, an empty list () is specified after the function name. A documentation string follows the argument list, which should explain what the function does as a whole, along with the type of argument(s) it is expecting. Code specific to the function follows a documentation string, which defines that function's behavior.

We also use two semi-colons in the code snippet above to demonstrate Emacs Lisp comments. An arbitrary sequence of semi-colons denote the start of a comment in Emacs Lisp. However, it is not uncommon to see comments that start with only a single semi-colon.

With this knowledge of functions under our belt, let’s define a simple function which outputs a string we pass as its only argument:

(defun output-message (string-to-output)
"A simple function that outputs STRING-TO-OUTPUT via the
`message' function."
(message "Output message: %s" string-to-output))

Notice that function arguments are specified in uppercase within a document string. This convention makes it easy for developers to find where arguments are mentioned within documentation strings— particularly long ones.

When it comes to evaluating a function, the gr command is no longer sufficient, because we need to take into account multiple commands on multiple lines. In these cases, we can use the C-M-x binding to evaluate a block of code. This means pressing and holding the Control and Alt keys, followed by pressing the x key. If done successfully, a floating prompt displaying our function's name will be output after its final closing parenthesis in the form of => output-message.

Now our output-message function is installed, it can be invoked from within Emacs in a number of ways. One way is to make a call in source code, very similar to the way we called message earlier:

(output-message "Hello, world!")

Now repeat the usual process of pressing gr RETURN after placing your cursor on the closing parenthesis of the function call. This will call our function and output its return value. The return value of a function is whatever value is returned by its last expression.

Another way to invoke functions in Emacs is via the M-: key binding, which means to hold down ALT and SHIFT, followed by pressing the semi-colon (;) key - or whatever key is mapped to the colon character. An Eval: prompt will be displayed at the bottom of Emacs and will be waiting for input. At this point, type (output-message "This is cool!") followed by pressing RETURN to invoke our function. Its output will now be shown in the status line. We can use M-: to invoke any installed command in our Emacs session.

The last key binding we will mention in this section is one that comes configured with Doom Emacs, namely SPC c e. After using this key combination, Emacs will run the +eval/buffer-or-region command. As a result, the interpreter will evaluate all Emacs Lisp code in the current buffer. This is a good way to check that no errors exist within a source file. If an error does exist, the Emacs Lisp debugger will let us know.

Working With File Attributes

This section will take a look at how to retrieve and output attributes for any given item on the file system. It would also be nice to output this data in a consistent and readable way. To meet this goal, we will develop three functions that each perform a specific task that when combined together, will enable us to output properly formatted file attribute data to the built-in *Messages* buffer.

To retrieve attributes of a given file, we will make use of a built-in Emacs function called file-attributes. The only argument we must provide this function is a string representing a valid filename on the file system. For completeness, let's take a look at its full signature:

(file-attributes FILENAME &optional ID-FORMAT)

As its name suggests, the &optional keyword tells the Emacs Lisp interpreter that subsequent arguments are completely optional, and do not need to be passed a value at invocation time. One optional argument called ID-FORMAT is given for the file-attributes function, which simply dictates whether to return a file's user ID and group ID attributes as a string or integer value. A string value is returned If nothing is given to this argument, which is what we want.

Moving forward, a populated list of data is returned after evaluating a call to file-attributes. Each item in the list corresponds to a particular attribute of the file we requested. In total, twelve items will make up the list of attributes:

Items returned by the file-attributes function.

Of particular interest is the list’s first element, which tells us whether the requested item is an actual file, directory, or symbolic link. For example, it will contain a true value if we requested a regular file — true is specified with the t character in Emacs Lisp.

Moreover, items that contain a time stamp will store the number of seconds since 1970-01-01 00:00:00. Emacs documentation points out that the current-time function returns its value in the same way. We will therefore need to format this value into a readable string before writing it to a target buffer.

At this point, we can test out file-attributes by making a call to it and examining its return value within Emacs. For example, evaluate the following code to output attributes of your .bashrc file:

(file-attributes "~/.bashrc")
-| (nil 1 1000 1000
(24588 61261 769551 700000)
(24574 56146 828467 818000)
(24574 56146 828467 818000)
1693 "-rw-r--r--" t 14686928 66310)

When observing the output, we realize that it would be beneficial to write a function that formats this data into a more readable form. If we don’t do any formatting, it will be very difficult to determine what the data in each item conveys.

Let’s now switch our focus on accessing items within the list returned by file-attributes. One way to do this would be to create a variable that points to the list, and then passing it to list functions, such as car, cdr and nthcdr to output its individual items:

;; Variable that points to the file attributes list
(setq attributes (file-attributes "~/.bashrc"))
;; Output first item
(car attributes)
;; Output fifth item (last access time)
(car (nthcdr 4 attributes))
;; Another way to output the fifth item
(car (cdr (cdr (cdr (cdr attributes)))))
;; Output tenth item (file modes)
(car (nthcdr 9 attributes))

The car function returns the first item in a list, while the cdr function returns the list starting at its second item. Multiple nested calls to cdr along with car allow us to traverse a list and access individual items.

An easier method to access list items is also demonstrated above, which is to use the nthcdr function. All we need to do here is pass an index number along with a list to work on. For example, (nthcdr 4 attributes) returns the fifth item in the attributes list, which is the "date last accessed" time stamp. Remember that a list's first item is always at index zero, meaning the nth item's index number is always n - 1.

We could certainly utilize these functions to retrieve individual items from a list. However, Emacs also provides some functions that specifically deal with accessing items from a list returned by file-attributes. These functions are described in the next table:

Built-in functions that retrieve particular file attributes.

Let’s test these functions out by evaluating some commands. We will start things off by creating a variable that points to a list of file attributes, and then pass it to a couple of the file-attribute-* functions detailed in the table above:

;; Create a variable that points to the file attributes list
(setq attributes (file-attributes "~/.bashrc"))
;; Pass variable to file attribute functions
(file-attribute-user-id attributes)
(file-attribute-size attributes)

That’s all very well and good, but we are not limited to only passing predefined variables to functions — we can also call functions directly within other functions. We know this because every expression in Emacs Lisp must return a value. As such, when a function is called within the parenthesis of another function, its return value is passed as an argument. For example, instead of passing our attributes variable to a function, we can just as easily call file-attributes directly:

(file-attribute-device-number (file-attributes "~/.bashrc"))
(file-attribute-inode-number (file-attributes "~/.bashrc"))

Taking our discussion a step further, functions can be invoked within calls to other functions at any level. In these cases, the Emacs Lisp interpreter will evaluate the inner-most invocation first, and work its way outward, gradually moving up the nested function hierarchy. With that said, consider what happens in the following examples:

(message "Last access time: %s"
(file-attribute-access-time
(file-attributes "~/.bashrc")))
(message "Last modification time: %s"
(file-attribute-modification-time
(file-attributes "~/.bashrc")))

The first call to message contains an invocation of file-attribute-access-time, which in turn contains an invocation to file-attributes. The Emacs Lisp interpreter will firstly evaluate (file-attributes "~/.bashrc"), followed by (file-attributes-access-time (...)) , and finally (message ...). The same situation occurs in the second message invocation, where the inner most function is evaluated first.

Developing Functions to Handle File Attributes

Appending a Pair of Strings to a List

The first function we will implement will have the responsibility of appending two string items to a list. This list will be passed as an argument to the function and subsequently returned once the two string items are appended to it. Because the appended string items are to represent exactly one line of output, this function will be called add-line. Let's firstly take a look at its signature and describe what particular processing will take place:

(add-line (description attribute list))

Our add-line function must receive three arguments:

  • description : A string describing the passed attribute argument.
  • attribute : An item from a list returned by the file-attributes function.
  • list : A list containing description and attribute string items.

We could construct a list by invoking add-line in the following way:

(add-line "File modes" 
(file-attribute-modes attributes) output-list)
(add-line "File size (in bytes)"
(file-attribute-size attributes) output-list)

We will discuss how add-line is used within another function we will develop very shortly. Without getting too far ahead of ourselves, we should also talk about the fact that our list should only contain string data; the reason being is that we want to eventually write our list data to a buffer. Because some attributes in a list returned by file-attributes are not of a string type, we will need to convert these values to a string before appending them to our output list.

Now we understand what the add-line function should do along with some technical considerations, let's discuss its actual implementation next. If you are implementing the functions discussed in this article yourself, go ahead and copy the following code into an .el file in Emacs:

The add-line function.

The function starts by checking if its attribute argument is a nil value. This will occur if we retrieve attributes from a physical file on the file system. We invoke the equal function within a call to the when macro to compare the value of attribute to a nil value. As a result, attribute is set to a string value of "nil" if it originally contained a nil value:

(when (equal attribute nil)
(setq attribute "nil"))

There will also be times when attribute contains a number, such as when it contains a file size or time stamp. To handle these situations, we invoke the stringp predicate function which returns t if its argument is indeed a string. Because we want to act when attribute does not point to a string, we wrap stringp inside a unless macro. In other words, unless attribute is a string, we execute some code to convert it to a string. This is accomplished by calling the number-to-string function and using its return value to overwrite attribute. In order for number-to-string to not throw an error, its argument must be an integer type:

(unless (stringp attribute)
(setq attribute (number-to-string attribute)))

At this point, we can be sure both description and attribute are string values. The cons function is then called to prepend these variables to the passed list. The first argument to cons is always a single data item that is to be added to a list - if a list is provided for this argument, it will be appended as a single item (a list within a list). The second argument is the list we wish to add this item to:

(setq list (cons description list))
(setq list (cons attribute list)))

Lastly, setq returns list with our appended description and attribute items, which will cause add-line to also return list to its calling code. Feel free to evaluate add-line using C-M-x and make some test invocations to check its output:

;; Temporary code for testing the add-line function
(setq attributes (file-attributes "~/.bashrc"))
(setq output-list ())
(setq output-list (add-line "File modes"
(file-attribute-modes attributes) output-list))
(setq output-list (add-line "File size (in bytes)"
(file-attribute-size attributes) output-list))
(message "%s" output-list)

Constructing a Formatted List

The second function we develop will have the job of utilizing our add-line function to construct a list of "attribute" and "description" string pairs. We also want this function to return the constructed list so other functions can receive it and use its data in certain ways. Let's firstly take a look at the signature of our next function, which we shall name formatted-file-attributes:

(defun formatted-file-attributes (filename))

The function contains one argument where we must pass a valid file name from which we will receive its corresponding attributes. If a valid filename is not provided, the function will be certain to throw an error.

Before we walk through how formatted-file-attributes works, its full implementation is displayed in the next gist. Go ahead and input the code into your Emacs Lisp file if you are typing out each function yourself:

The formatted-file-attributes function.

Following the documentation string, a new function is called that we have yet to discuss called interactive. In our case, it enables us to pass a string to the function's filename argument when it is called interactively. A function in Emacs is called interactively when we invoke it using either SPC : in Doom Emacs, or M-x, which is available in both Doom Emacs and vanilla Emacs. These key bindings invoke the "Meta-x" command, which calls a function called execute-extended-command behind the scenes.

After invoking Meta-x and typing formatted-file-attributes followed by hitting Return, Emacs will prompt us to also input a filename that will be given to the function as its first argument. We set up this functionality by passing a string defined as "fFile name: " to the interactive function. The lower-case f signifies that we want to pass the name of an existing file. The following File name: text will be the prompt that is displayed when we call our function interactively:

(interactive "fFile name: ")

Our function continues to set up a couple of local variables. A list of local variables can be defined and provided an initial value inside an Emacs Lisp let statement. What makes local variables special is that they can only be accessed from within the function in which they are defined, and within the let statements outer-most parenthesis. If a variable defined outside a function has an identical name as a local variable defined inside a function, that outer variable is masked by the local variable. So be aware that you will not be able to access masked variables. In addition, a function's arguments are also set up as local variables.

Moving forward, we define two local variables called attributes and out-list. The attributes variable is initialized to point to the list returned by file-attributes, that is in turn passed the function's filename argument. As a result, we are able to access the attributes of our requested file through the attributes variable in subsequent code:

(let ((attributes (file-attributes filename))
(out-list ()))

Secondly, the out-list variable is initialized to an empty list, which can be done by specifying an opening and closing parenthesis. This variable will be passed to add-line to construct a formatted list. It will be returned at the end of formatted-file-attributes so other functions can utilize its data for further processing, such as writing it to other buffers.

Notice how nested parenthesis are specified within a let statement — an outer set of parenthesis contain each variable definition, which are also wrapped with parenthesis. Be careful to specify the order of parenthesis correctly if you are implementing this function yourself.

Our function then appends string items to the out-list variable by making use of our add-line function Notice that we use the setq function to continuously set out-list to the list returned by add-line. As a result, the string items appended in add-line are not lost when the program returns to the outer formatted-file-attributes function. For each attribute, we pass a hard-coded string as the description argument for add-line . When it comes to passing an item from the attributes list, we opt to call the built-in file-attribute-* functions:

(setq out-list (add-line "File type"
(file-attribute-type attributes) out-list))
(setq out-list (add-line "Links"
(file-attribute-link-number attributes) out-list))
;; And so on

One extra step is taken when a time stamp is passed to add-line. The built-in format-time-string function is called to format a time stamp into a readable string understandable to humans. A %- sequence is passed as the first argument to format-string, which defines how a time stamp will be formatted. We specify the %D sequence, which is a synonym for %m/%d/%y. As a result, a string is returned that shows the current month, day, and year. The second argument that format-string expects is the actual time stamp to work on:

(setq out-list 
(add-line "Last Access Time"
(format-time-string "%D"
(file-attribute-access-time attributes))
out-list))

Many %- sequences are available in Emacs Lisp to format a date and time to your exact specifications. Refer to the Emacs official documentation to discover the full set of sequences available.

The last line in formatted-file-attributes reverses the order of items in out-list such that its order mimics that of the list returned by file-attributes. We call a built-in function called nreverse and pass it out-list to do the reverse. This is wrapped inside a setq command, which will cause the function to return the final state of out-list:

(setq out-list (nreverse out-list))))

Some test calls to formatted-file-attributes can be made to make sure its working correctly. Remember to firstly move your cursor within the function itself and evaluate it with the C-M-x. From there, you can make a few calls directly in your source file and evaluate them with gr RETURN. The constructed list will be returned and displayed as output:

(formatted-file-attributes "~/.bashrc")

Outputting to the Messages Buffer

The last functions we develop will be a wrapper around our formatted-file-attributes function. It will utilize the returned list to write its data somewhere. This new function will be called file-attributes-to-messages, and will be tasked to output file attribute information to the built-in Emacs *Messages* buffer:

(defun file-attributes-to-messages (filename))

Similar to our previous function, file-attributes-to-messages exposes a single argument called filename, where we supply a string representing the file whose attributes we wish to retrieve. The gist below lists the complete file-attributes-to-messages function. We will then discuss its contents:

The file-attributes-to-messages function.

Notice that file-attributes-to-messages is also an interactive function, therefore allowing us to invoke it using the Meta-x (SPC : or M-x) key binding. When the function is invoked in this way, a prompt will again be displayed, instructing us to provide a file name.

One local variable called out-list is then defined in a let statement which is set up to point to the list returned by our format-file-attributes function. This means we can reference out-list at any time to access the list data we wish to output:

(let ((out-list (format-file-attributes filename)))

Within the let statement's outer-most parenthesis, we invoke message to write the requested file name to the *Messages* buffer. Notice that we specify an initial format specifier sequence as %20s. As a result, the "description" part of output will be rendered as a column of twenty characters. In other words, %Ns will render a string of N characters. A blank space character is prepended to our passed string until it reaches N characters:

(message "%20s: %s\n\n" "Filename" filename)

Our function then enters a while loop which will continue to run as long as out-list is pointing to items in the list. A single iteration firstly calls message to output the next "description" and "attribute" pair from the list. The last command results in the out-list pointer being shifted two items up the list. When the while condition is evaluated next, out-list will either be pointing to a "description" item, or nil. When it points to nil, we know that the entire list has has been written to the *Messages* buffer. At this point, the while loop will break:

(while out-list
(message "%20s %s\n"
(car out-list) (car (nthcdr 1 out-list)))
(setq out-list (cdr (cdr out-list))))))

Notice that we make a recursive call to cdr to make out-list shift two items up the list.

At this point, feel free to test file-attributes-to-messages by calling it interactively or directly in source code. After an invocation, file attribute information will be written to the *Messages* buffer:

(file-attributes-to-messages "~/.bashrc")

The *Messages* buffer can be opened in Doom Emacs by using the SPC-b B key binding. From there, select *Messages* from the list of buffers to open it in the current window. On vanilla Emacs, use the C-x b key binding, type *Messages* and press RETURN.

Conclusion

This article has provided a hands-on introduction to Emacs Lisp by walking through the development of three functions that deal with interacting with files. From here, you can add the functions we developed to your Emacs initialization file, which will cause Emacs to automatically evaluate them when you launch a new session. For example, Doom Emacs will automatically evaluate our functions if they exist in the ~/.doom.d/config.el file.

To build upon the functions even further, I would recommend playing with the sequence options available to the format-time-string function to output timestamps in various formats. You can also read up on the many formatting options that can be passed to the message function to further customize output to the *Messages* buffer.

Also keep an eye out for future Emacs Lisp and Doom Emacs related articles from myself. We will build upon the content presented in this article when we look at developing more custom functionality within Emacs.

--

--