aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJim Porter2023-03-20 17:25:54 -0700
committerJim Porter2023-03-28 12:03:29 -0700
commitcde38f0df3fa3540ce411a48d95da1c2f1be1b60 (patch)
tree7cf41e920e16da219f6a2bc26dcb803bc2538916
parentbb088885df7a8e8a32670286a8636db8c6fadcf4 (diff)
downloademacs-cde38f0df3fa3540ce411a48d95da1c2f1be1b60.tar.gz
emacs-cde38f0df3fa3540ce411a48d95da1c2f1be1b60.zip
Avoid parsing some Eshell forms when performing completion
During completion, we want to evaluate most Eshell forms (e.g. variable references), but skip others (e.g. globbing, subcommands). For globbing, we want to pass the literal glob to Pcomplete so it can use the glob for selecting completion candidates. For subcommands (including Lisp forms), we especially want to avoid evaluation, since they can produce arbitary side effects! (Bug#50470) * lisp/eshell/esh-cmd.el (eshell-allow-commands): New variable... (eshell-commands-forbidden): New error... (eshell-named-command, eshell-lisp-command): ... use them. * lisp/eshell/em-cmpl.el (eshell-complete--eval-argument-form): Disallow command forms and handle errors ourselves. (eshell-complete-parse-arguments): Don't parse glob characters. * test/lisp/eshell/em-cmpl-tests.el (em-cmpl-test/parse-arguments/unevaluated-subcommand) (em-cmpl-test/parse-arguments/unevaluated-lisp-form) (em-cmpl-test/parse-arguments/unevaluated-inner-subcommand) (em-cmpl-test/file-completion/glob, em-cmpl-test/command-completion) (em-cmpl-test/subcommand-completion): New tests. (em-cmpl-test/parse-arguments/pipeline): Remove superfluous let-binding. (em-cmpl-test/file-completion/after-list): Use a list variable rather than a subexpression; the latter is no longer evaluated during completion. (em-cmpl-test/lisp-function-completion): Check "$(func)" syntax.
-rw-r--r--lisp/eshell/em-cmpl.el60
-rw-r--r--lisp/eshell/esh-cmd.el15
-rw-r--r--test/lisp/eshell/em-cmpl-tests.el86
3 files changed, 131 insertions, 30 deletions
diff --git a/lisp/eshell/em-cmpl.el b/lisp/eshell/em-cmpl.el
index b65652019d4..732bbb3f1fa 100644
--- a/lisp/eshell/em-cmpl.el
+++ b/lisp/eshell/em-cmpl.el
@@ -306,9 +306,24 @@ to writing a completion function."
306 306
307(defun eshell-complete--eval-argument-form (arg) 307(defun eshell-complete--eval-argument-form (arg)
308 "Evaluate a single Eshell argument form ARG for the purposes of completion." 308 "Evaluate a single Eshell argument form ARG for the purposes of completion."
309 (let ((result (eshell-do-eval `(eshell-commands ,arg) t))) 309 (condition-case err
310 (cl-assert (eq (car result) 'quote)) 310 (let* (;; Don't allow running commands; they could have
311 (cadr result))) 311 ;; arbitrary side effects, which we don't want when we're
312 ;; just performing completions!
313 (eshell-allow-commands)
314 ;; Handle errors ourselves so that we can properly catch
315 ;; `eshell-commands-forbidden'.
316 (eshell-handle-errors)
317 (result (eshell-do-eval `(eshell-commands ,arg) t)))
318 (cl-assert (eq (car result) 'quote))
319 (cadr result))
320 (eshell-commands-forbidden
321 (propertize "\0" 'eshell-argument-stub
322 (intern (format "%s-command" (cadr err)))))
323 (error
324 (lwarn 'eshell :error
325 "Failed to evaluate argument form during completion: %S" arg)
326 (propertize "\0" 'eshell-argument-stub 'error))))
312 327
313(defun eshell-complete-parse-arguments () 328(defun eshell-complete-parse-arguments ()
314 "Parse the command line arguments for `pcomplete-argument'." 329 "Parse the command line arguments for `pcomplete-argument'."
@@ -325,23 +340,28 @@ to writing a completion function."
325 (if (= begin end) 340 (if (= begin end)
326 (end-of-line)) 341 (end-of-line))
327 (setq end (point-marker))) 342 (setq end (point-marker)))
328 (if (setq delim 343 ;; Don't expand globs when parsing arguments; we want to pass any
329 (catch 'eshell-incomplete 344 ;; globs to Pcomplete unaltered.
330 (ignore 345 (declare-function eshell-parse-glob-chars "em-glob" ())
331 (setq args (eshell-parse-arguments begin end))))) 346 (let ((eshell-parse-argument-hook (remq #'eshell-parse-glob-chars
332 (cond ((member (car delim) '("{" "${" "$<")) 347 eshell-parse-argument-hook)))
333 (setq begin (1+ (cadr delim)) 348 (if (setq delim
334 args (eshell-parse-arguments begin end))) 349 (catch 'eshell-incomplete
335 ((member (car delim) '("$'" "$\"" "#<")) 350 (ignore
336 ;; Add the (incomplete) argument to our arguments, and 351 (setq args (eshell-parse-arguments begin end)))))
337 ;; note its position. 352 (cond ((member (car delim) '("{" "${" "$<"))
338 (setq args (append (nth 2 delim) (list (car delim))) 353 (setq begin (1+ (cadr delim))
339 incomplete-arg t) 354 args (eshell-parse-arguments begin end)))
340 (push (- (nth 1 delim) 2) posns)) 355 ((member (car delim) '("$'" "$\"" "#<"))
341 ((member (car delim) '("(" "$(")) 356 ;; Add the (incomplete) argument to our arguments, and
342 (throw 'pcompleted (elisp-completion-at-point))) 357 ;; note its position.
343 (t 358 (setq args (append (nth 2 delim) (list (car delim)))
344 (eshell--pcomplete-insert-tab)))) 359 incomplete-arg t)
360 (push (- (nth 1 delim) 2) posns))
361 ((member (car delim) '("(" "$("))
362 (throw 'pcompleted (elisp-completion-at-point)))
363 (t
364 (eshell--pcomplete-insert-tab)))))
345 (when (get-text-property (1- end) 'comment) 365 (when (get-text-property (1- end) 'comment)
346 (eshell--pcomplete-insert-tab)) 366 (eshell--pcomplete-insert-tab))
347 (let ((pos (1- end))) 367 (let ((pos (1- end)))
diff --git a/lisp/eshell/esh-cmd.el b/lisp/eshell/esh-cmd.el
index 1a458290dfe..d5237ee1f04 100644
--- a/lisp/eshell/esh-cmd.el
+++ b/lisp/eshell/esh-cmd.el
@@ -293,6 +293,17 @@ CDR are the same process.
293 293
294When the process in the CDR completes, resume command evaluation.") 294When the process in the CDR completes, resume command evaluation.")
295 295
296(defvar eshell-allow-commands t
297 "If non-nil, allow evaluating command forms (including Lisp forms).
298If you want to forbid command forms, you can let-bind this to a
299non-nil value before calling `eshell-do-eval'. Then, any command
300forms will signal `eshell-commands-forbidden'. This is useful
301if, for example, you want to evaluate simple expressions like
302variable expansions, but not fully-evaluate the command. See
303also `eshell-complete-parse-arguments'.")
304
305(define-error 'eshell-commands-forbidden "Commands forbidden")
306
296;;; Functions: 307;;; Functions:
297 308
298(defsubst eshell-interactive-process-p () 309(defsubst eshell-interactive-process-p ()
@@ -1328,6 +1339,8 @@ have been replaced by constants."
1328(defun eshell-named-command (command &optional args) 1339(defun eshell-named-command (command &optional args)
1329 "Insert output from a plain COMMAND, using ARGS. 1340 "Insert output from a plain COMMAND, using ARGS.
1330COMMAND may result in an alias being executed, or a plain command." 1341COMMAND may result in an alias being executed, or a plain command."
1342 (unless eshell-allow-commands
1343 (signal 'eshell-commands-forbidden '(named)))
1331 (setq eshell-last-arguments args 1344 (setq eshell-last-arguments args
1332 eshell-last-command-name (eshell-stringify command)) 1345 eshell-last-command-name (eshell-stringify command))
1333 (run-hook-with-args 'eshell-prepare-command-hook) 1346 (run-hook-with-args 'eshell-prepare-command-hook)
@@ -1465,6 +1478,8 @@ via `eshell-errorn'."
1465 1478
1466(defun eshell-lisp-command (object &optional args) 1479(defun eshell-lisp-command (object &optional args)
1467 "Insert Lisp OBJECT, using ARGS if a function." 1480 "Insert Lisp OBJECT, using ARGS if a function."
1481 (unless eshell-allow-commands
1482 (signal 'eshell-commands-forbidden '(lisp)))
1468 (catch 'eshell-external ; deferred to an external command 1483 (catch 'eshell-external ; deferred to an external command
1469 (setq eshell-last-command-status 0 1484 (setq eshell-last-command-status 0
1470 eshell-last-arguments args) 1485 eshell-last-arguments args)
diff --git a/test/lisp/eshell/em-cmpl-tests.el b/test/lisp/eshell/em-cmpl-tests.el
index ea907f1945d..29a41625d5e 100644
--- a/test/lisp/eshell/em-cmpl-tests.el
+++ b/test/lisp/eshell/em-cmpl-tests.el
@@ -69,11 +69,10 @@ ACTUAL and EXPECTED should both be lists of strings."
69(ert-deftest em-cmpl-test/parse-arguments/pipeline () 69(ert-deftest em-cmpl-test/parse-arguments/pipeline ()
70 "Test that parsing arguments for completion discards earlier commands." 70 "Test that parsing arguments for completion discards earlier commands."
71 (with-temp-eshell 71 (with-temp-eshell
72 (let ((eshell-test-value '("foo" "bar"))) 72 (insert "echo hi | cat")
73 (insert "echo hi | cat") 73 (should (eshell-arguments-equal
74 (should (eshell-arguments-equal 74 (car (eshell-complete-parse-arguments))
75 (car (eshell-complete-parse-arguments)) 75 '("cat")))))
76 '("cat"))))))
77 76
78(ert-deftest em-cmpl-test/parse-arguments/multiple-dots () 77(ert-deftest em-cmpl-test/parse-arguments/multiple-dots ()
79 "Test parsing arguments with multiple dots like \".../\"." 78 "Test parsing arguments with multiple dots like \".../\"."
@@ -123,6 +122,45 @@ ACTUAL and EXPECTED should both be lists of strings."
123 (car (eshell-complete-parse-arguments)) 122 (car (eshell-complete-parse-arguments))
124 '("echo" "foo" "bar")))))) 123 '("echo" "foo" "bar"))))))
125 124
125(ert-deftest em-cmpl-test/parse-arguments/unevaluated-subcommand ()
126 "Test that subcommands return a stub when parsing for completion."
127 (with-temp-eshell
128 (insert "echo {echo hi}")
129 (should (eshell-arguments-equal
130 (car (eshell-complete-parse-arguments))
131 `("echo" ,(propertize
132 "\0" 'eshell-argument-stub 'named-command)))))
133 (with-temp-eshell
134 (insert "echo ${echo hi}")
135 (should (eshell-arguments-equal
136 (car (eshell-complete-parse-arguments))
137 `("echo" ,(propertize
138 "\0" 'eshell-argument-stub 'named-command))))))
139
140(ert-deftest em-cmpl-test/parse-arguments/unevaluated-lisp-form ()
141 "Test that Lisp forms return a stub when parsing for completion."
142 (with-temp-eshell
143 (insert "echo (concat \"hi\")")
144 (should (eshell-arguments-equal
145 (car (eshell-complete-parse-arguments))
146 `("echo" ,(propertize
147 "\0" 'eshell-argument-stub 'lisp-command)))))
148 (with-temp-eshell
149 (insert "echo $(concat \"hi\")")
150 (should (eshell-arguments-equal
151 (car (eshell-complete-parse-arguments))
152 `("echo" ,(propertize
153 "\0" 'eshell-argument-stub 'lisp-command))))))
154
155(ert-deftest em-cmpl-test/parse-arguments/unevaluated-inner-subcommand ()
156 "Test that nested subcommands return a stub when parsing for completion."
157 (with-temp-eshell
158 (insert "echo $exec-path[${echo 0}]")
159 (should (eshell-arguments-equal
160 (car (eshell-complete-parse-arguments))
161 `("echo" ,(propertize
162 "\0" 'eshell-argument-stub 'named-command))))))
163
126(ert-deftest em-cmpl-test/file-completion/unique () 164(ert-deftest em-cmpl-test/file-completion/unique ()
127 "Test completion of file names when there's a unique result." 165 "Test completion of file names when there's a unique result."
128 (with-temp-eshell 166 (with-temp-eshell
@@ -150,14 +188,39 @@ ACTUAL and EXPECTED should both be lists of strings."
150 (forward-line -1) 188 (forward-line -1)
151 (should (looking-at "Complete, but not unique"))))))) 189 (should (looking-at "Complete, but not unique")))))))
152 190
191(ert-deftest em-cmpl-test/file-completion/glob ()
192 "Test completion of file names using a glob."
193 (with-temp-eshell
194 (ert-with-temp-directory default-directory
195 (write-region nil nil (expand-file-name "file.txt"))
196 (write-region nil nil (expand-file-name "file.el"))
197 (should (equal (eshell-insert-and-complete "echo fi*.el")
198 "echo file.el ")))))
199
153(ert-deftest em-cmpl-test/file-completion/after-list () 200(ert-deftest em-cmpl-test/file-completion/after-list ()
154 "Test completion of file names after previous list arguments. 201 "Test completion of file names after previous list arguments.
155See bug#59956." 202See bug#59956."
156 (with-temp-eshell 203 (with-temp-eshell
157 (ert-with-temp-directory default-directory 204 (let ((eshell-test-value '("foo" "bar")))
158 (write-region nil nil (expand-file-name "file.txt")) 205 (ert-with-temp-directory default-directory
159 (should (equal (eshell-insert-and-complete "echo (list 1 2) fi") 206 (write-region nil nil (expand-file-name "file.txt"))
160 "echo (list 1 2) file.txt "))))) 207 (should (equal (eshell-insert-and-complete "echo $eshell-test-value fi")
208 "echo $eshell-test-value file.txt "))))))
209
210(ert-deftest em-cmpl-test/command-completion ()
211 "Test completion of command names like \"command\"."
212 (with-temp-eshell
213 (should (equal (eshell-insert-and-complete "listif")
214 "listify "))))
215
216(ert-deftest em-cmpl-test/subcommand-completion ()
217 "Test completion of command names like \"{command}\"."
218 (with-temp-eshell
219 (should (equal (eshell-insert-and-complete "{ listif")
220 "{ listify ")))
221 (with-temp-eshell
222 (should (equal (eshell-insert-and-complete "echo ${ listif")
223 "echo ${ listify "))))
161 224
162(ert-deftest em-cmpl-test/lisp-symbol-completion () 225(ert-deftest em-cmpl-test/lisp-symbol-completion ()
163 "Test completion of Lisp forms like \"#'symbol\" and \"`symbol\". 226 "Test completion of Lisp forms like \"#'symbol\" and \"`symbol\".
@@ -174,7 +237,10 @@ See <lisp/eshell/esh-cmd.el>."
174See <lisp/eshell/esh-cmd.el>." 237See <lisp/eshell/esh-cmd.el>."
175 (with-temp-eshell 238 (with-temp-eshell
176 (should (equal (eshell-insert-and-complete "echo (eshell/ech") 239 (should (equal (eshell-insert-and-complete "echo (eshell/ech")
177 "echo (eshell/echo")))) 240 "echo (eshell/echo")))
241 (with-temp-eshell
242 (should (equal (eshell-insert-and-complete "echo $(eshell/ech")
243 "echo $(eshell/echo"))))
178 244
179(ert-deftest em-cmpl-test/special-ref-completion/type () 245(ert-deftest em-cmpl-test/special-ref-completion/type ()
180 "Test completion of the start of special references like \"#<buffer\". 246 "Test completion of the start of special references like \"#<buffer\".