This makes sure that we treat Eshell sub-forms (whether Lisp or command
forms) as values when appropriate, or as regular invocations. This
requires a bit more explicit work, but helps to resolve some of the
surprising differences between Lisp and command forms in complex Eshell
statements.
* lisp/eshell/esh-cmd.el (eshell-subcommand-arg-values): Make obsolete.
(eshell-parse-lisp-argument): Don't add 'eshell-command-to-value' here.
(eshell-rewrite-sexp-command): Don't check for 'eshell-command-to-value
here'; instead check for 'eshell-lisp-command'.
(eshell-structure-basic-command): Check for 'eshell-lisp-command'.
(eshell-term-as-value): New function...
(eshell-rewrite-named-command, eshell-rewrite-for-command): ... call it.
* lisp/eshell/esh-arg.el (eshell-parse-special-reference):
* lisp/eshell/esh-io.el (eshell-strip-redirections):
* lisp/eshell/esh-var.el (eshell-prepare-indices): Call
'eshell-term-as-value'.
* test/lisp/eshell/esh-arg-tests.el
(esh-arg-test/special-reference/command-form):
* test/lisp/eshell/esh-cmd-tests.el (esh-cmd-test/for-loop-lisp-body)
(esh-cmd-test/while-loop-lisp-body)
(esh-cmd-test/if-else-statement-lisp-body): New tests.
* test/lisp/eshell/esh-var-tests.el
(esh-var-test/interp-var-indices-subcommand): Add another command to
test.
* doc/misc/eshell.texi (Control Flow): Update documentation.
(cherry picked from commit
40ffacb34b194aa82273539ab7a5be2f485a706f)
Most of Eshell's control flow statements accept a @var{conditional}.
This can take a few different forms. If @var{conditional} is a dollar
-expansion, the condition is satisfied if the result is a
-non-@code{nil} value. If @var{conditional} is a @samp{@{
-@var{subcommand} @}} or @samp{(@var{lisp form})}, the condition is
-satisfied if the command's exit status is 0.
+expansion, the condition is satisfied if the result is a non-@code{nil}
+value. Alternately, @var{conditional} may be a subcommand, either in
+command form, e.g.@: @samp{@{@var{subcommand}@}}; or in Lisp form,
+e.g.@: @samp{(@var{lisp form})}. In that case, the condition is
+satisfied if the subcommand's exit status is 0.
@table @code
-@item if @var{conditional} @{ @var{true-commands} @}
-@itemx if @var{conditional} @{ @var{true-commands} @} @{ @var{false-commands} @}
-Evaluate @var{true-commands} if @var{conditional} is satisfied;
-otherwise, evaluate @var{false-commands}.
+@item if @var{conditional} @var{true-subcommand}
+@itemx if @var{conditional} @var{true-subcommand} @var{false-subcommand}
+Evaluate @var{true-subcommand} if @var{conditional} is satisfied;
+otherwise, evaluate @var{false-subcommand}. Both @var{true-subcommand}
+and @var{false-subcommand} should be subcommands, as with
+@var{conditional}.
-@item unless @var{conditional} @{ @var{false-commands} @}
-@itemx unless @var{conditional} @{ @var{false-commands} @} @{ @var{true-commands} @}
-Evaluate @var{false-commands} if @var{conditional} is not satisfied;
-otherwise, evaluate @var{true-commands}.
+@item unless @var{conditional} @var{false-subcommand}
+@itemx unless @var{conditional} @var{false-subcommand} @var{true-subcommand}
+Evaluate @var{false-subcommand} if @var{conditional} is not satisfied;
+otherwise, evaluate @var{true-subcommand}.
-@item while @var{conditional} @{ @var{commands} @}
-Repeatedly evaluate @var{commands} so long as @var{conditional} is
+@item while @var{conditional} @var{subcommand}
+Repeatedly evaluate @var{subcommand} so long as @var{conditional} is
satisfied.
-@item until @var{conditional} @{ @var{commands} @}
-Repeatedly evaluate @var{commands} until @var{conditional} is
+@item until @var{conditional} @var{subcommand}
+Repeatedly evaluate @var{subcommand} until @var{conditional} is
satisfied.
-@item for @var{var} in @var{list}@dots{} @{ @var{commands} @}
+@item for @var{var} in @var{list}@dots{} @var{subcommand}
Iterate over each element of @var{list}, storing the element in
-@var{var} and evaluating @var{commands}. If @var{list} is not a list,
-treat it as a list of one element. If you specify multiple
-@var{lists}, this will iterate over each of them in turn.
+@var{var} and evaluating @var{subcommand}. If @var{list} is not a list,
+treat it as a list of one element. If you specify multiple @var{lists},
+this will iterate over each of them in turn.
@end table
(eval-when-compile
(require 'cl-lib))
+(declare-function eshell-term-as-value "esh-cmd" (term))
+
(defgroup eshell-arg nil
"Argument parsing involves transforming the arguments passed on the
command line into equivalent Lisp forms that, when evaluated, will
(prog1
(cons creation-fun
(let ((eshell-current-argument-plain t))
- (eshell-parse-arguments (point) end)))
+ (mapcar #'eshell-term-as-value
+ (eshell-parse-arguments (point) end))))
(goto-char (1+ end)))
(ignore (goto-char here)))))))
(defun eshell-subcommand-arg-values (terms)
"Convert subcommand arguments {x} to ${x}, in order to take their values."
+ (declare (obsolete nil "31.1"))
(setq terms (cdr terms)) ; skip command argument
(while terms
(if (and (listp (car terms))
(defun eshell-rewrite-sexp-command (terms)
"Rewrite a sexp in initial position, such as `(+ 1 2)'."
;; this occurs when a Lisp expression is in first position
- (if (and (listp (car terms))
- (eq (caar terms) 'eshell-command-to-value))
- (car (cdar terms))))
+ (when (and (listp (car terms))
+ (eq (caar terms) 'eshell-lisp-command))
+ (car terms)))
(defun eshell-rewrite-initial-subcommand (terms)
"Rewrite a subcommand in initial position, such as `{+ 1 2}'."
(defun eshell-rewrite-named-command (terms)
"If no other rewriting rule transforms TERMS, assume a named command."
- (eshell-subcommand-arg-values terms)
- (let ((sym (if eshell-in-pipeline-p
- 'eshell-named-command*
- 'eshell-named-command))
- (grouped-terms (eshell-prepare-splice terms)))
- (cond
- (grouped-terms
- `(let ((terms (nconc ,@grouped-terms)))
- (,sym (car terms) (cdr terms))))
- ;; If no terms are spliced, use a simpler command form.
- ((cdr terms)
- (list sym (car terms) `(list ,@(cdr terms))))
- (t
- (list sym (car terms))))))
+ (when terms
+ (setq terms (cons (car terms)
+ ;; Convert arguments to take their values.
+ (mapcar #'eshell-term-as-value (cdr terms))))
+ (let ((sym (if eshell-in-pipeline-p
+ 'eshell-named-command*
+ 'eshell-named-command))
+ (grouped-terms (eshell-prepare-splice terms)))
+ (cond
+ (grouped-terms
+ `(let ((new-terms (nconc ,@grouped-terms)))
+ (,sym (car new-terms) (cdr new-terms))))
+ ;; If no terms are spliced, use a simpler command form.
+ ((cdr terms)
+ (list sym (car terms) `(list ,@(cdr terms))))
+ (t
+ (list sym (car terms)))))))
(defvar eshell--command-body)
(defvar eshell--test-body)
,@(mapcar
(lambda (elem)
(if (listp elem)
- elem
+ (eshell-term-as-value elem)
`(list ,elem)))
(nthcdr 3 terms)))))
(while ,for-items
function."
;; If the test form is a subcommand, wrap it in `eshell-commands' to
;; silence the output.
- (when (eq (car test) 'eshell-as-subcommand)
+ (when (memq (car test) '(eshell-as-subcommand eshell-lisp-command))
(setq test `(eshell-commands ,test t)))
;; If the test form begins with `eshell-convert' or
(end-of-file
(throw 'eshell-incomplete "(")))))
(if (eshell-arg-delimiter)
- `(eshell-command-to-value
- (eshell-lisp-command (quote ,obj)))
+ `(eshell-lisp-command (quote ,obj))
(ignore (goto-char here))))))
(defun eshell-split-commands (terms separator &optional
,command
,value))))
+(defun eshell-term-as-value (term)
+ "Convert an Eshell TERM to take its value."
+ (cond
+ ((eq (car-safe term) 'eshell-as-subcommand) ; {x} -> ${x}
+ `(eshell-convert (eshell-command-to-value ,term)))
+ ((eq (car-safe term) 'eshell-lisp-command) ; (x) -> $(x)
+ `(eshell-command-to-value ,term))
+ (t term)))
+
;;;_* Iterative evaluation
;;
;; Eshell runs all of its external commands asynchronously, so that
(require 'cl-lib))
(declare-function eshell-interactive-print "esh-mode" (string))
+(declare-function eshell-term-as-value "esh-cmd" (term))
(defgroup eshell-io nil
"Eshell's I/O management code provides a scheme for treating many
(unless (cdr tt)
(error "Missing redirection target"))
(nconc eshell-current-redirections
- (list (list 'ignore
- (append (car tt) (list (cadr tt))))))
+ `((ignore ,(append (car tt)
+ (list (eshell-term-as-value (cadr tt)))))))
(setcdr tl (cddr tt))
(setq tt (cddr tt)))
(t
(defun eshell-prepare-indices (indices)
"Prepare INDICES to be evaluated by Eshell.
INDICES is a list of index-lists generated by `eshell-parse-indices'."
- `(list ,@(mapcar (lambda (idx-list) (cons 'list idx-list)) indices)))
+ `(list ,@(mapcar (lambda (idx-list)
+ (cons 'list (mapcar #'eshell-term-as-value idx-list)))
+ indices)))
(defun eshell-get-variable (name &optional indices quoted)
"Get the value for the variable NAME.
"setq eshell-test-value #<marker 1 #<buffer (buffer-name)>>")
(should (equal eshell-test-value marker)))))
+(ert-deftest esh-arg-test/special-reference/command-form ()
+ "Test that command forms inside special references work."
+ (with-temp-eshell
+ (let ((marker (make-marker))
+ eshell-test-value)
+ (set-marker marker 1 (current-buffer))
+ (eshell-insert-command
+ "setq eshell-test-value #<marker 1 {current-buffer}>")
+ (should (equal eshell-test-value marker))
+ (eshell-insert-command
+ "setq eshell-test-value #<marker 1 #<buffer {buffer-name}>>")
+ (should (equal eshell-test-value marker)))))
+
(ert-deftest esh-arg-test/special-reference/special-characters ()
"Test that \"#<...>\" works correctly when escaping special characters."
(with-temp-buffer
(eshell-match-command-output "for i in 1 { echo $for-items }"
"hello\n")))
+(ert-deftest esh-cmd-test/for-loop-lisp-body ()
+ "Test invocation of a for loop with a Lisp body form."
+ (with-temp-eshell
+ (eshell-match-command-output "for i in 1 2 3 (format \"%s\" i)"
+ "1\n2\n3\n")))
+
(ert-deftest esh-cmd-test/for-loop-pipe ()
"Test invocation of a for loop piped to another command."
(skip-unless (executable-find "rev"))
"{ setq eshell-test-value (1+ eshell-test-value) }")
"1\n2\n3\n"))))
+(ert-deftest esh-cmd-test/while-loop-lisp-body ()
+ "Test invocation of a while loop using a Lisp form for the body."
+ (with-temp-eshell
+ (let ((eshell-test-value 0))
+ (eshell-match-command-output
+ (concat "while (/= eshell-test-value 3) "
+ "(setq eshell-test-value (1+ eshell-test-value))")
+ "1\n2\n3\n"))))
+
(ert-deftest esh-cmd-test/while-loop-ext-cmd ()
"Test invocation of a while loop using an external command."
(skip-unless (executable-find "["))
(eshell-command-result-equal "if (zerop \"foo\") {echo yes} {echo no}"
"no"))))
+(ert-deftest esh-cmd-test/if-else-statement-lisp-body ()
+ "Test invocation of an if/else statement using Lisp forms for the bodies."
+ (eshell-command-result-equal "if (zerop 0) (format \"yes\") (format \"no\")"
+ "yes")
+ (eshell-command-result-equal "if (zerop 1) (format \"yes\") (format \"no\")"
+ "no")
+ (let ((debug-on-error nil))
+ (eshell-command-result-equal
+ "if (zerop \"foo\") (format \"yes\") (format \"no\")"
+ "no")))
+
(ert-deftest esh-cmd-test/if-else-statement-ext-cmd ()
"Test invocation of an if/else statement using an external command."
(skip-unless (executable-find "["))
"zero")
(eshell-command-result-equal
"echo $eshell-test-value[${*echo 0} ${*echo 2}]"
+ '("zero" "two"))
+ (eshell-command-result-equal
+ "echo $eshell-test-value[{*echo 0} {*echo 2}]"
'("zero" "two"))))
(ert-deftest esh-var-test/interp-var-length-list ()