summaryrefslogtreecommitdiff
path: root/shell-quasiquote.el
blob: d6105655b52dd0d17a09f67a917a7dde12f9dda6 (plain)
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