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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
;; -*- lexical-binding: t; -*-
;;; shell-quasiquote.el --- Turn s-expressions into shell command strings.
;; Copyright (C) 2015, 2025 Free Software Foundation
;; Author: Taylan Ulrich Kammer <taylan.kammer@gmail.com>
;; Version: 1.1
;; Keywords: extensions, unix
;; URL: https://git.tkammer.de/elisp/shell-quasiquote
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; "Shell quasiquote" -- turn s-expressions into shell command strings.
;;
;; Quoting is automatic for POSIX shells.
;;
;; (let ((file1 "file one")
;; (file2 "file two"))
;; (shqq (cp -r ,file1 ,file2 "My Files")))
;; => "cp -r 'file one' 'file two' 'My Files'"
;;
;; You can splice many arguments into place with ,@foo.
;;
;; (let ((files (list "file one" "file two")))
;; (shqq (cp -r ,@files "My Files")))
;; => "cp -r 'file one' 'file two' 'My Files'"
;;
;; Note that the quoting disables a variety of shell expansions like ~/foo,
;; $ENV_VAR, and e.g. {x..y} in GNU Bash.
;;
;; You can use ,,foo to escape the quoting.
;;
;; (let ((files "file1 file2"))
;; (shqq (cp -r ,,files "My Files")))
;; => "cp -r file1 file2 'My Files'"
;;
;; And ,,@foo to splice and escape quoting.
;;
;; (let* ((arglist '("-x 'foo bar' -y baz"))
;; (arglist (append arglist '("-z 'qux fux'"))))
;; (shqq (command ,,@arglist)))
;; => "command -x 'foo bar' -y baz -z 'qux fux'"
;;
;; Neat, eh?
;;; Code:
;;; Like `shell-quote-argument', but much simpler in implementation.
(defun shqq--quote-string (string)
(concat "'" (replace-regexp-in-string "'" "'\\\\''" string) "'"))
(defun shqq--atom-to-string (atom)
(cond
((symbolp atom) (symbol-name atom))
((stringp atom) atom)
((numberp atom) (number-to-string atom))
(t (error "Bad shqq atom: %S" atom))))
(defun shqq--quote-atom (atom)
(shqq--quote-string (shqq--atom-to-string atom)))
(defmacro shqq (parts)
"First, PARTS is turned into a list of strings. For this,
every element of PARTS must be one of:
- a symbol, evaluating to its name,
- a string, evaluating to itself,
- a number, evaluating to its decimal representation,
- \",expr\", where EXPR must evaluate to an atom that will be
interpreted according to the previous rules,
- \",@list-expr\", where LIST-EXPR must evaluate to a list whose
elements will each be interpreted like the EXPR in an \",EXPR\"
form, and spliced into the list of strings,
- \",,expr\", where EXPR is interpreted like in \",expr\",
- or \",,@expr\", where EXPR is interpreted like in \",@expr\".
In the resulting list of strings, all elements except the ones
resulting from \",,expr\" and \",,@expr\" forms are quoted for
shell grammar.
Finally, the resulting list of strings is concatenated with
separating spaces."
(let ((parts
(mapcar
(lambda (part)
(cond
((atom part) (shqq--quote-atom part))
;; We use the match-comma helpers because pcase can't match ,foo.
(t (pcase part
;; ,,foo i.e. (, (, foo))
(`(,`\, (,`\, ,part))
part)
;; ,,@foo i.e. (, (,@ foo))
(`(,`\, (,`\,@ ,part))
`(mapconcat #'identity ,part " "))
;; ,foo
;; Insert redundant 'and x' to work around debbugs#18554.
(`(,`\, ,part)
`(shqq--quote-atom ,part))
;; ,@foo
(`(,`\,@ ,part)
`(mapconcat #'shqq--quote-atom ,part " "))
(_
(error "Bad shqq part: %S" part))))))
parts)))
`(mapconcat #'identity (list ,@parts) " ")))
(provide 'shell-quasiquote)
;;; shell-quasiquote.el ends here
|