Emacs beyond the basics, part II: your first steps in Emacs Lisp
A gentle first step into Emacs Lisp: read the syntax, write your own commands, bind keys, use hooks and advice, build a quick-notes command and learn to debug and read Emacs’s own source. No programming background needed.
Contents
- Why bother
- Where to try things
- Reading Lisp
- Variables
- Your first command
- Giving it a key
- Asking the user for something
- Working with text
- Making decisions and repeating things
- A real command: quick notes
- Hooks: run code when something happens
- Advice: changing a command you didn't write
- Reading Emacs's own code
- When things break
- Organizing your init.el
- Where to go next
The first Emacs guide on this blog ended by pointing at a manual called "An Introduction to Programming in Emacs Lisp", which is how people go from using Emacs to bending it. This post is the friendly version of that step. No computer science background needed. If you can copy a line into your init.el, you can do everything here.
The goal is small and practical. By the end you'll read Lisp without panicking, write your own commands, give them keys, hook them into modes and know how to dig into any function Emacs has. That's most of what people ever do with Elisp.
Why bother #
Every key you press in Emacs runs a function written in Emacs Lisp. Your config is Lisp. The packages you install are Lisp. Once you can read a little of it, three things change:
- Your
init.elstops being a list of spells copied from the internet. You know what each line does, and you can fix it when it breaks. - When something annoys you, you can write the fix yourself, usually in five or ten lines.
- The source of every command is one click away, so Emacs becomes its own textbook.
Where to try things #
You don't need to create a file to experiment. Emacs gives you three playgrounds.
The *scratch* buffer opens with Emacs, in a mode made for this. Type an expression, put the cursor right after it and press C-j. The result gets printed on the next line.
M-: asks for an expression in the minibuffer, runs it and shows the result at the bottom of the screen. It works from any buffer, which makes it great for quick questions like "what's the value of this variable right now?".
C-x C-e runs the expression right before the cursor, in any Lisp file. This is the one you'll use the most in your init.el: change a line, put the cursor after it, C-x C-e, and the change is live without restarting.
And if you like a proper REPL, M-x ielm gives you one, with a prompt and history.
Reading Lisp #
Lisp looks scary because of the parentheses, but the rule is simple. A list in parentheses is a call: the first thing is the function, the rest are its arguments.
(+ 1 2) ; => 3
(* 2 (+ 3 4)) ; => 14
(concat "Hello, " "Emacs") ; => "Hello, Emacs"
(upcase "hello") ; => "HELLO"The inner parentheses run first, so (* 2 (+ 3 4)) becomes (* 2 7), which becomes 14. That's the whole grammar. Everything else is vocabulary.
A semicolon starts a comment. Strings go in double quotes. t means true, and nil means false and also "empty list". Those two show up everywhere.
Sometimes you want a list as data, not as a call. That's what the quote is for:
'(+ 1 2) ; => (+ 1 2), just a list with three things in it
(+ 1 2) ; => 3
(car '(apples pears plums)) ; => apples, the first item
(cdr '(apples pears plums)) ; => (pears plums), the restThe same quote is why config files are full of things like 'modus-vivendi or #'display-line-numbers-mode. The ' says "this name, as it is". The #' is the same idea for names of functions, and it helps Emacs warn you when you misspell one.
A note about indentation: Lisp doesn't care about it. The blocks of code in this post are written flat, and Emacs will indent them properly for you. Paste the code, select it and press TAB, or use C-M-\.
Variables #
setq sets a variable. let creates variables that only exist inside its parentheses:
(setq my-name "Pablo")
(let ((x 10)
(y 32))
(+ x y)) ; => 42, and x and y vanish after thisformat builds a string with placeholders, %s for anything and %d for whole numbers:
(format "Hi, %s! You have %d buffers open." my-name (length (buffer-list)))For settings that Emacs or a package offers as options (the ones you'd see in M-x customize), Emacs 29 added setopt. It works like setq, but it also runs any code the option needs when it changes, and it warns you when the value has the wrong type:
(setopt fill-column 80)A simple habit: setopt for options, setq for everything else.
Your first command #
A function is made with defun. Give it a name, a list of arguments, a short description and a body. Add (interactive) and it becomes a command, something you can run with M-x or bind to a key.
(defun my-insert-date ()
"Insert today's date at point, like 2026-11-06."
(interactive)
(insert (format-time-string "%Y-%m-%d")))Put the cursor after the last parenthesis and press C-x C-e. Now M-x my-insert-date works. And because you wrote a description, C-h f my-insert-date shows it, just like any built-in command.
Two habits worth having from day one. First, start your function names with a prefix, like my-, so they never clash with something Emacs or a package already uses. Second, always write the description. Future you will read it.
Giving it a key #
Emacs 29 added a clean way to bind keys:
(keymap-global-set "C-c d" #'my-insert-date)Now C-c d inserts the date anywhere. The choice of C-c and a letter is on purpose. Emacs keeps every C-c followed by a plain letter for you, the user. Neither Emacs nor well-behaved packages will ever take those keys, so they're the safe place for your own commands.
You can also bind a key only inside one mode, using that mode's keymap:
(with-eval-after-load 'org
(keymap-set org-mode-map "C-c n" #'my-new-note))with-eval-after-load waits until Org is actually loaded before touching its keymap. Without it, Emacs would complain that org-mode-map doesn't exist yet. You'll write my-new-note in a minute.
Asking the user for something #
interactive can do more than mark a function as a command. Give it a code and Emacs will ask the user for the arguments. "s" asks for a string:
(defun my-greet (name)
"Say hello to NAME."
(interactive "sYour name: ")
(message "Hello, %s!" name))Run M-x my-greet and Emacs prompts "Your name:" in the minibuffer. Other codes ask for a number ("n"), a file ("f"), a buffer ("b") and more. C-h f interactive lists them all.
Working with text #
Inside a buffer, the cursor position is called the point, and a few functions do most of the work:
-
(point): where the cursor is, as a number. -
(goto-char 1): move the cursor.(point-min)and(point-max)are the start and the end of the buffer. -
(insert "text"): write at the cursor. -
(buffer-string): the whole buffer as a string. -
(count-words (point-min) (point-max)): count words, which is a built-in command too.
One more is worth learning early: save-excursion. It runs some code and then puts the cursor back where it was, so your command doesn't leave the user lost in the file.
(defun my-add-todo-on-top ()
"Insert a TODO line at the top of the buffer, without moving the cursor."
(interactive)
(save-excursion
(goto-char (point-min))
(insert "TODO: review this file\n")))Making decisions and repeating things #
if takes a condition, what to do when it's true and what to do when it isn't. when and unless are shortcuts for when you only need one side. cond is a chain of options, checked from top to bottom:
(let ((n 7))
(cond ((< n 5) "small")
((< n 10) "medium")
(t "big"))) ; => "medium"The t at the end works as "otherwise", since t is always true.
dolist walks through a list. This one goes over every open buffer and tells you which files have unsaved changes:
(defun my-list-unsaved ()
"Show which file buffers have unsaved changes."
(interactive)
(dolist (b (buffer-list))
(when (and (buffer-file-name b) (buffer-modified-p b))
(message "Unsaved: %s" (buffer-name b)))))The messages pile up in the *Messages* buffer, which you can open with C-h e.
A real command: quick notes #
Time to put it all together. This command asks for a title, turns it into a clean file name and opens a new Org note in your notes folder, already with a title and the date.
(defvar my-notes-directory "~/notes"
"Where `my-new-note' keeps its files.")
(defun my-slugify (title)
"Turn TITLE into a lowercase, dash-separated slug."
(let* ((s (downcase title))
(s (replace-regexp-in-string "[^a-z0-9]+" "-" s)))
(string-trim s "-" "-")))
(defun my-new-note (title)
"Create a new Org note called TITLE in `my-notes-directory'."
(interactive "sNote title: ")
(let* ((date (format-time-string "%Y-%m-%d"))
(file (expand-file-name
(concat date "-" (my-slugify title) ".org")
my-notes-directory)))
(make-directory my-notes-directory t)
(find-file file)
(when (= (buffer-size) 0)
(insert "#+title: " title "\n"
"#+date: " date "\n\n"))))
(keymap-global-set "C-c n" #'my-new-note)Press C-c n, type "Ideas for the weekend", and you're editing 2026-11-06-ideas-for-the-weekend.org, with the title and the date at the top. If the file already exists, it just opens, without writing the header twice.
A few new pieces showed up here:
-
defvarcreates a variable with a description, the polite way to define a setting. -
let*is likelet, except each variable can use the ones before it. Heresgets rewritten in two steps. -
replace-regexp-in-stringswaps every run of characters that aren't letters or numbers for a dash. -
string-trimremoves leftover dashes from the edges. -
make-directorywithtcreates the folder, and doesn't complain if it already exists.
Read it slowly, change something, run it again. That loop is how you learn Elisp.
Hooks: run code when something happens #
A hook is a list of functions that Emacs runs at a specific moment. Every mode has one, named after the mode with -hook at the end. This is how you say "every time I open a text file, do this":
(add-hook 'text-mode-hook #'visual-line-mode)
(add-hook 'prog-mode-hook #'display-line-numbers-mode)The first line wraps long lines nicely in text files. The second turns on line numbers in any programming mode, since all of them build on prog-mode.
When you need more than one function call, use a lambda, which is a function without a name:
(add-hook 'prog-mode-hook
(lambda ()
(setq-local fill-column 100)
(electric-pair-local-mode 1)))setq-local changes the variable only in that buffer, so your Python files can have one width and your notes another.
There are hooks for almost everything: before-save-hook, after-save-hook, kill-emacs-hook, emacs-startup-hook. C-h v and the word "hook" will show you the list.
Advice: changing a command you didn't write #
Sometimes you want a built-in command to do one extra thing. Advice lets you attach your own function before, after or around it:
(defun my-announce-save (&rest _)
"Tell me which buffer was just saved."
(message "Saved %s" (buffer-name)))
(advice-add 'save-buffer :after #'my-announce-save)Now every save shows a little message. To undo it, advice-remove with the same arguments.
Advice is powerful, and that's exactly why it deserves care. Reach for a hook first. If there isn't one, then advice is fine, but keep each piece small and give it a clear name, because advice is invisible when you're reading the original command's code.
Reading Emacs's own code #
This is where the investment pays off. C-h f describes any function, and the description has a link to the file where it's defined. Click it and you're reading the real source. C-h k does the same for a key: press C-h k and then C-x C-s, follow the link, and you'll see how Emacs saves a file.
Three other help commands that pair well with Elisp:
-
C-h o: describe any symbol, function or variable, whatever it is. -
C-h v: describe a variable and show its current value. -
C-h i: the manuals, including the Elisp reference, inside Emacs.
You won't understand everything you read at first. That's fine. Skim, look for the parts you recognize, and every week you'll recognize more.
When things break #
Errors in Elisp are normal, and Emacs has good tools for them.
M-x toggle-debug-on-error makes Emacs open a backtrace whenever an error happens. It looks dense, but the top lines usually point straight at the function that failed.
For your own functions, there's Edebug. Put the cursor inside a defun and press C-u C-M-x. The next time the function runs, Emacs stops at the first step and lets you walk through it with SPC, one expression at a time, showing each value as it goes. q gets you out.
If your init.el breaks so badly that Emacs starts in a strange state, remember emacs -Q, which starts without any config. Open the file, fix it, restart. And emacs --debug-init starts with your config and opens the backtrace at the first error, which usually tells you the exact line.
Finally, M-x checkdoc reviews the descriptions in your file and points out small style problems. It's a good teacher.
Organizing your init.el #
Once you have a few functions, keep them tidy. Two simple rules go a long way.
Group things by topic, with a comment on top of each group, and keep your own functions together in one section. Use ;;; for section titles. Emacs even understands them: M-x outline-minor-mode lets you fold the file by those headings.
And let use-package do the bookkeeping for packages and built-in features alike. It's built into Emacs since version 29:
(use-package org
:hook (org-mode . visual-line-mode)
:bind ("C-c a" . org-agenda)
:config
(setq org-log-done 'time)):hook adds hooks, :bind sets keys and :config runs code after the package loads. It's the same Lisp you learned above, organized into labeled boxes.
Where to go next #
Inside Emacs, C-h i opens the manuals. Look for "An Introduction to Programming in Emacs Lisp", written for people who have never programmed. It's long, gentle and full of small exercises. When you want the details, the "Emacs Lisp Reference Manual" sits right next to it. Both are also online at gnu.org/software/emacs/manual.
A good next exercise: pick something you do by hand at least twice a week, and turn it into a command. Inserting a template, opening a set of files, cleaning up a pasted text. Small commands that fit your own habits are where Elisp starts to feel personal.
This is part II of a series. Part I is about living inside Emacs: feeds, gopher, the fediverse, chat and mail. Part III turns Emacs into a full programming setup.