Add support for chaining conditionals in Eshell

* lisp/eshell/esh-cmd.el (eshell-structure-basic-command): Check for the
presence of the conditional.  Allow any number of BODY forms.
(eshell-rewrite-if-command): Add support for 'else' keyword and chained
conditionals.

* test/lisp/eshell/esh-cmd-tests.el (esh-cmd-test/if-else-statement):
Test 'else' keyword.
(esh-cmd-test/if-else-statement-chain): New test.

* doc/misc/eshell.texi (Control Flow): Document this change.

* etc/NEWS: Announce this change.
This commit is contained in:
Jim Porter 2024-10-04 22:26:01 -07:00
parent 40ffacb34b
commit fada04cfc7
4 changed files with 73 additions and 24 deletions

View file

@ -1698,16 +1698,29 @@ satisfied if the subcommand's exit status is 0.
@table @code
@item if @var{conditional} @var{true-subcommand}
@itemx if @var{conditional} @var{true-subcommand} @var{false-subcommand}
@itemx if @var{conditional} @var{true-subcommand} else @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}.
You can also chain together @code{if}/@code{else} forms, for example:
@example
if @{[ -f file.txt ]@} @{
echo found file
@} else if @{[ -f alternate.txt ]@} @{
echo found alternate
@} else @{
echo not found!
@}
@end example
@item unless @var{conditional} @var{false-subcommand}
@itemx unless @var{conditional} @var{false-subcommand} @var{true-subcommand}
@itemx unless @var{conditional} @var{false-subcommand} else @var{true-subcommand}
Evaluate @var{false-subcommand} if @var{conditional} is not satisfied;
otherwise, evaluate @var{true-subcommand}.
otherwise, evaluate @var{true-subcommand}. Like above, you can also
chain together @code{unless}/@code{else} forms.
@item while @var{conditional} @var{subcommand}
Repeatedly evaluate @var{subcommand} so long as @var{conditional} is

View file

@ -257,6 +257,20 @@ These functions now take an optional ERROR-TARGET argument to control
where to send the standard error output. See the "(eshell) Entry
Points" node in the Eshell manual for more details.
+++
*** Conditional statements in Eshell now use an 'else' keyword.
Eshell now prefers the following form when writing conditionals:
if {conditional} {true-subcommand} else {false-subcommand}
The old form (without the 'else' keyword) is retained for compatibility.
+++
*** You can now chain conditional statements in Eshell.
When using the newly-preferred conditional form in Eshell, you can now
chain together multiple 'if'/'else' statements. For more information,
see "(eshell) Control Flow" in the Eshell manual.
+++
*** Eshell's built-in 'wait' command now accepts a timeout.
By passing '-t' or '--timeout', you can specify a maximum time to wait

View file

@ -551,12 +551,14 @@ implemented via rewriting, rather than as a function."
,body)
(setq ,for-items (cdr ,for-items)))))))
(defun eshell-structure-basic-command (func names keyword test body
&optional else)
(defun eshell-structure-basic-command (func names keyword test &rest body)
"With TERMS, KEYWORD, and two NAMES, structure a basic command.
The first of NAMES should be the positive form, and the second the
negative. It's not likely that users should ever need to call this
function."
(unless test
(error "Missing test for `%s' command" keyword))
;; If the test form is a subcommand, wrap it in `eshell-commands' to
;; silence the output.
(when (memq (car test) '(eshell-as-subcommand eshell-lisp-command))
@ -582,33 +584,39 @@ function."
(setq test `(not ,test)))
;; Finally, create the form that represents this structured command.
`(,func ,test ,body ,else))
`(,func ,test ,@body))
(defun eshell-rewrite-while-command (terms)
"Rewrite a `while' command into its equivalent Eshell command form.
Because the implementation of `while' relies upon conditional
evaluation of its argument (i.e., use of a Lisp special form), it
must be implemented via rewriting, rather than as a function."
(if (and (stringp (car terms))
(member (car terms) '("while" "until")))
(eshell-structure-basic-command
'while '("while" "until") (car terms)
(cadr terms)
(car (last terms)))))
(when (and (stringp (car terms))
(member (car terms) '("while" "until")))
(eshell-structure-basic-command
'while '("while" "until") (car terms)
(cadr terms)
(caddr terms))))
(defun eshell-rewrite-if-command (terms)
"Rewrite an `if' command into its equivalent Eshell command form.
Because the implementation of `if' relies upon conditional
evaluation of its argument (i.e., use of a Lisp special form), it
must be implemented via rewriting, rather than as a function."
(if (and (stringp (car terms))
(member (car terms) '("if" "unless")))
(eshell-structure-basic-command
'if '("if" "unless") (car terms)
(cadr terms)
(car (last terms (if (= (length terms) 4) 2)))
(when (= (length terms) 4)
(car (last terms))))))
(when (and (stringp (car terms))
(member (car terms) '("if" "unless")))
(eshell-structure-basic-command
'if '("if" "unless") (car terms)
(cadr terms)
(caddr terms)
(if (equal (nth 3 terms) "else")
;; If there's an "else" keyword, allow chaining together
;; multiple "if" forms...
(or (eshell-rewrite-if-command (nthcdr 4 terms))
(nth 4 terms))
;; ... otherwise, only allow a single "else" block (without the
;; keyword) as before for compatibility.
(nth 3 terms)))))
(defun eshell-set-exit-info (status &optional result)
"Set the exit status and result for the last command.

View file

@ -427,11 +427,15 @@ processes correctly."
(ert-deftest esh-cmd-test/if-else-statement ()
"Test invocation of an if/else statement."
(let ((eshell-test-value t))
(eshell-command-result-equal "if $eshell-test-value {echo yes} {echo no}"
"yes"))
(eshell-command-result-equal
"if $eshell-test-value {echo yes} {echo no}" "yes")
(eshell-command-result-equal
"if $eshell-test-value {echo yes} else {echo no}" "yes"))
(let ((eshell-test-value nil))
(eshell-command-result-equal "if $eshell-test-value {echo yes} {echo no}"
"no")))
(eshell-command-result-equal
"if $eshell-test-value {echo yes} {echo no}" "no")
(eshell-command-result-equal
"if $eshell-test-value {echo yes} else {echo no}" "no")))
(ert-deftest esh-cmd-test/if-else-statement-lisp-form ()
"Test invocation of an if/else statement using a Lisp form."
@ -474,6 +478,16 @@ This tests when `eshell-lisp-form-nil-is-failure' is nil."
(eshell-command-result-equal "if {[ foo = bar ]} {echo yes} {echo no}"
"no"))
(ert-deftest esh-cmd-test/if-else-statement-chain ()
"Test invocation of a chained if/else statement."
(dolist (case '((1 . "one") (2 . "two") (3 . "other")))
(let ((eshell-test-value (car case)))
(eshell-command-result-equal
(concat "if (= eshell-test-value 1) {echo one} "
"else if (= eshell-test-value 2) {echo two} "
"else {echo other}")
(cdr case)))))
(ert-deftest esh-cmd-test/if-statement-pipe ()
"Test invocation of an if statement piped to another command."
(skip-unless (executable-find "rev"))