In this tutorial we build a function b which allows you to run shell code within elisp syntax (it looks like emacs lisp).

This tutorial is useful for learning to write emacs-lisp macros but is also useful for understanding macros of any language.

First some prerequisite functions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
  (defmacro shut-up-c (&rest body)
    "This works for c functions where shut-up does not."
    `(let* ((inhibit-message t))
       ,@body))

  (defun get-dir ()
    "Gets the current working directory.
  Takes into account the current file name."
    (shut-up-c
     (let ((filedir (if buffer-file-name
                        (file-name-directory buffer-file-name)
                      (file-name-directory (my/pwd)))))
       (if (s-blank? filedir)
           (my/pwd)
         filedir))))

  (defun my/pwd ()
    "Gets the current working directory"
    (interactive)
    (substring (pwd) 10))

  (defun str (thing)
    "Converts object or string to an unformatted string."
    (setq thing (format "%s" thing))
    (set-text-properties 0 (length thing) nil thing)
    thing)

  (defun e/q (string)
    "Puts double quote around a string and escapes any interior backslashes or double quotes."
    (let ((print-escape-newlines t))
      (prin1-to-string string)))

  (defun shellquote (input)
    "If string contains spaces or backslashes, put quotes around it."
    (if (or (string-match "\\\\" input)
            (string-match " " input)
            (string-match "\"" input))
        (e/q input)
      input))

  (defun sh-notty (&optional cmd stdin dir)
    "Runs command in shell and return the result.
This appears to strip ansi codes."
    (interactive)
    (if (not cmd)
        (setq cmd "false"))

    (if (not dir)
        (setq dir (get-dir)))

    (setq tf (make-temp-file "elispbash"))

    (setq final_cmd (concat "( cd " (e/q dir) "; " cmd ") > " tf))

    (if (not stdin)
        (progn
          (shell-command final_cmd))
      (with-temp-buffer
        (insert stdin)
        (shell-command-on-region (point-min) (point-max) final_cmd)))
    (setq output (with-temp-buffer
                   (insert-file-contents tf)
                   (buffer-string)))

    output)

We will explain how these 2 macros work

1
2
3
4
5
6
7
8
9
(defmacro quote-args (&rest body)
  "Join all the arguments in a sexp into a single string.
Be mindful of quoting arguments correctly."
  `(mapconcat (lambda (input) (shellquote (str input))) ',body " "))

(defmacro b (&rest body)
  "Runs a shell command
Write straight bash within elisp syntax (it looks like emacs-lisp)"
  `(sh-notty (concat (quote-args ,@body))))

Some macro syntax explanations

This notation is called a quasiquote or ‘backtick’ notation.

1
2
3
4
5
6
;; quote-args
;; `(...)  ;; backtick notation is not exclusive to macros. you can also use in functions
;; '(...)  ;; It does the same thing as single quote notation, and in its simplest form, is exactly the same.
;; `(a ,b @c ,@d) ;; But you can also use , and @ or , and @ together
;; , ;; while the backtick and single quote prevents the list elements from being evaluated, a single , comma before a symbol or expression allows it this one to be evaluated
;; @ ;; the at symbol unpacks the list into individual symbols. therefore, you can take advantage of the backtick notation to pass a unpack the macro's parameters into the call to another macro or function

Functions

Lets talk about functions so we can compare them with macros.

Function arguments

What is the behaviour of function arguments?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
;; function arguments are evaluated before the function starts.
(defun f1 (&rest body)
  (message (format "%s" body)))

(f1 2 1 1 1)
;; (2 1 1 1)

(f1 (+ 1 1) 1 1 1)
;; (2 1 1 1)

;; What the function sees is exactly the same because
;; the arguments are evaluated before they are passed in.

Macros

Macro arguments

Almost anything (so long that it is valid emacs lisp syntax) can legally go into the arguments of a macro because they’re NOT evaluated before the macro starts.

Lets name the macros m1, m2, m3 etc. so we can talk about them easily.

m1 – This macro returns nil

1
2
3
4
5
6
7
(defmacro m1 (&rest body)
  ;; Nothing in here
  )

(m1 echo hi && echo -n hi && echo -n hi)
;; Calling this macro doesn't complain that echo is not
;; a variable etc. A function would complain.

nil

m2 – Returns the list of the arguments as a string

1
2
3
4
(defmacro m2 (&rest body)
  (message (format "%s" body)))

(m2 echo hi && echo -n hi && echo -n hi)

”(echo hi && echo -n hi && echo -n hi)”

m3 – Returns 5. The last item is the one that is returned.

So the objective of a macro is simply to manipulate the body variable how we like them before we get to the end of the macro and then return a valid value or list as the last expression in the macro

1
2
3
4
(defmacro m3 (&rest body)
  5)

(m3 echo hi && echo -n hi && echo -n hi)

5

m4 – This gives an error: “void function body”

The error occurs because the last expression in the macro is '(body) and the last expression of a macro is always evaluated.

Remember, the backtick does the same thing as the single quote, but has some additional, optional features.

1
2
3
4
(defmacro m4 (&rest body)
  `(body))

(m4 echo hi && echo -n hi && echo -n hi)

That is what the debugger says when you try to run the above:

1
2
3
4
Debugger entered--Lisp error: (void-function body)
  (body)
  eval((body) nil)
  ...

It’s trying to run this:

1
(eval((body) nil))

m5 – This gives an error: “void expression echo” or “void-function echo”

The error occurs because the macro returns the list (echo hi && echo -n hi && echo -n hi)

When a macro is run, the last expression is evaluated by whatever called the macro.

So the objective of most macros is to turn body (i.e the arguments) into something you want evaluated (i.e. rewrite the code).

1
2
3
4
(defmacro m5 (&rest body)
  body)

(m5 echo hi && echo -n hi && echo -n hi)

That’s because it’s trying to run this:

1
(echo hi && echo -n hi && echo -n hi)

m6 – This gives an error: “void-function @body”

1
2
3
4
(defmacro m6 (&rest body)
  `(@body))

(m6 echo hi && echo -n hi && echo -n hi)

That’s because expanding the macro generates this:

1
(@body)

And that will not evaluate. @body is not a function.

m7 – This gives an error: “invalid-function (echo hi && echo -n hi && echo -n hi)”

1
2
3
4
(defmacro m7 (&rest body)
  `(,body))

(m7 echo hi && echo -n hi && echo -n hi)

The macro returned ((echo hi && echo -n hi && echo -n hi)).

The problem here is that we did not expand body with @. It’s still contained as a list. Inside the backtick, body is just a symbol until we give it a comma. But then it becomes an entire list. We put that list into another list with the backtick. When the macro ends, it returns a list inside a list and tries to evaluate it.

To expand body from inside the quasiquote ` you use ,@. See below.

m8 – This gives an error: “void-function echo”

This is an improvement on m7 as it expands body but we still see an error.

1
2
3
4
(defmacro m8 (&rest body)
  `(,@body))

(m8 echo hi && echo -n hi && echo -n hi)

The macro returns this as the last expression: (echo hi && echo -n hi && echo -n hi)

echo is not a function, however.

m9 – This is the same as m8 because you’re expanding the list into another list.

1
2
(defmacro m9 (&rest body)
  body)

m10 – Working macro but not finished yet

1
2
3
4
(defmacro m10 (&rest body)
  `(quote ,body))

(m10 echo hi && echo -n hi && echo -n hi)

(echo hi && echo -n hi && echo -n hi)

When the macro finishes, it returns (quote (echo hi && echo -n hi && echo -n hi)).

When the macro ends, it evaluates the quoted list and we get (echo hi && echo -n hi && echo -n hi)

m11 – This is the same as m10

m10 can also be written like m11.

You can quote ,body like so ',body.

1
2
(defmacro m11 (&rest body)
  `',body)

m12 – working macro

We have a list of the arguments. Now reduce the list to a quoted string

1
2
3
4
(defmacro m12 (&rest body)
  `(mapconcat 'str ',body " "))

(m12 echo hi && echo -n hi && echo -n hi)

“echo hi && echo -n hi && echo -n hi”

Now we can build quote-args

quote-args just returns the string of the arguments, but ensures they are quoted nicely.

1
2
3
4
(defmacro quote-args (&rest body)
  `(mapconcat (lambda (input) (shellquote (str input))) ',body " "))

(quote-args echo hi && echo -n hi && echo -n hi)

“echo hi && echo -n hi && echo -n hi”

Now we can explain how b works

This just feeds the string to a function, which happens to be a call to the shell.

1
2
3
4
(defmacro b (&rest body)
  `(sh-notty (concat (quote-args ,@body))))

(b echo hi && echo -n hi && echo -n hi)

“hi hihi”

In summary

Macros are different from functions in that the arguments are not evaluated at all before the function is called: the arguments are passed in and it’s then up to the code within the macro to do with the raw code what it likes.

For example, it can alter the code passed in before evaluating it, alter the code passed in without evaluating it and return altered code or simply ignore what was passed in and return something else entirely. It can print out the code you passed in to it instead of running it, for example. That would be useful for logging.

Arguments, therefore, are not like pass-by-value: Nor are they like pass-by-reference. They’re more like pass-by-raw-code.

Expand the (b)

Usually a macro is just used to rewrite code before evaluation. For example, we can expand the call to macro b to see what it turns into.

1
2
3
;; (macroexpand '(b echo hi && echo -n hi && echo -n hi))
;; =
;; (sh-notty (concat (quote-args echo hi && echo -n hi && echo -n hi)))

quote-args is also a macro. expand it

1
2
3
;; (macroexpand '(quote-args echo hi && echo -n hi && echo -n hi))
;; =
;; (mapconcat (lambda (input) (shellquote (str input))) (quote (echo hi && echo -n hi && echo -n hi)))

Lastly, a macro need not rewrite code for it to be useful

In this case, a symbol helm-map may be passed to the macro show-map and the name of the symbol is used instead of the symbol itself.

1
2
3
(defmacro show-map (m)
  (let ((mstring (concat "\\{" (symbol-name m) "}")))
    (new-buffer-from-string (substitute-command-keys mstring))))
1
(show-map helm-map)

Annex

For some of the examples above, you will see a different error message depending on how you run the sexp.

If you run them with C-x C-e, you would see the same errors that I did.

However, executing with the M-: (Eval:) repl may give errors that look more like below:

1
Symbol’s function definition is void: echo