diff options
| author | Jim Porter | 2023-03-20 17:25:54 -0700 |
|---|---|---|
| committer | Jim Porter | 2023-03-28 12:03:29 -0700 |
| commit | cde38f0df3fa3540ce411a48d95da1c2f1be1b60 (patch) | |
| tree | 7cf41e920e16da219f6a2bc26dcb803bc2538916 | |
| parent | bb088885df7a8e8a32670286a8636db8c6fadcf4 (diff) | |
| download | emacs-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.el | 60 | ||||
| -rw-r--r-- | lisp/eshell/esh-cmd.el | 15 | ||||
| -rw-r--r-- | test/lisp/eshell/em-cmpl-tests.el | 86 |
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 | ||
| 294 | When the process in the CDR completes, resume command evaluation.") | 294 | When 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). | ||
| 298 | If you want to forbid command forms, you can let-bind this to a | ||
| 299 | non-nil value before calling `eshell-do-eval'. Then, any command | ||
| 300 | forms will signal `eshell-commands-forbidden'. This is useful | ||
| 301 | if, for example, you want to evaluate simple expressions like | ||
| 302 | variable expansions, but not fully-evaluate the command. See | ||
| 303 | also `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. |
| 1330 | COMMAND may result in an alias being executed, or a plain command." | 1341 | COMMAND 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. |
| 155 | See bug#59956." | 202 | See 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>." | |||
| 174 | See <lisp/eshell/esh-cmd.el>." | 237 | See <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\". |