Elisp: Splicing conditional argument lists to pass to function calls
Published on Aug 03, 2022.
Today, I found out about the special marker ,@
in Elisp: it allows to splice an evaluated value into a quoted list while keeping the level of the “injected” elements the same as other elements on the resulting list. This solved (or at least simplified) a problem I faced a couple of times already: whenever I wanted to assemble arguments for functions that have a &rest ARGS
in their signature, such as call-process
, using conditionals and evaluated functions, the code quickly became more complicated or duplicative than felt right to me. Let me give you an example.
call-process
uses the signature call-process PROGRAM &optional INFILE DESTINATION DISPLAY &rest ARGS
. All the arguments ARGS
to the called program have to be given as individual strings. So for a call to the command line tool find
to look for files matching the pattern elisp*.org
in the current directory that have been modified today this could look like:
(with-temp-buffer (call-process "find" nil t nil "./" "-mtime" "0" "-name" "elisp*.org") (buffer-substring (point-min) (point-max)))
./elisp_splicing_conditional_argument_lists_to_pass_to_function_calls.org
Here, with-temp-buffer
and buffer-substring
are used to process the output of the command. Note that -name-
and elisp*.org
are separate strings despite the latter giving the pattern to the argument name – anything else, and find
would not recognize the argument as valid. The same goes for the -mtime
argument and its parameter.
I found it difficult to provide arguments in this particular format when I want
to fill the values from variables and make some of them optional. Consider this
example where the result should be a -name
and pattern but no mtime
arguments to find
:
(let ((pattern "elisp*.org") (n nil)) `(,(when pattern `("-name" ,pattern)) ,(when n `("-mtime" ,n)))))
(("-name" "elisp*.org") nil)
This approach almost works, but the result is a nested list and a nil
value which we have to remove before calling call-process
. This can be easily done by introducing the ,@
special marker:
(let* ((pattern "elisp*.org") (n nil)) `(,@(when pattern `("-name" ,pattern)) ,@(when n `("-mtime" ,n)))))
("-name" "elisp*.org")
Now the result is in the right format and the code had to be changed only minimally! Final puzzle piece: feed the arguments to call-process
using apply #'call-process args
:
(let* ((pattern "elisp*.org") (n nil)) (with-temp-buffer (apply #'call-process `("find" nil t nil ,@(when pattern `("-name" ,pattern)) ,@(when n `("-mtime" ,n)))) (buffer-substring (point-min) (point-max))))
./elisp_splicing_conditional_argument_lists_to_pass_to_function_calls.org
Which – to my eyes – is compact and reasonably readable code 😸
But feel free to get in touch and tell me otherwise (preferably with suggestions for improvements)!