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
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
Our functions will also be interactive, which means that they can be invoked just like any other command in Emacs.
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
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.
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.
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."
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 "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
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 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
(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
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
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:
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
-| (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
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))
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:
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
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-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-attributes "~/.bashrc")))(message "Last modification time: %s"
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))
add-line function must receive three arguments:
description: A string describing the passed
attribute: An item from a list returned by the
list: A list containing
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 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
(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
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)))
list with our appended
attribute items, which will cause
add-line to also return
list to its calling code. Feel free to evaluate
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
(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:
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 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 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
(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:
(add-line "Last Access Time"
%- 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
(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:
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
(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:
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)))
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
(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:
(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 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
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
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
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.