From b7ed8aa37dad9d32ab9738aa1a19a36c892f8017 Mon Sep 17 00:00:00 2001 From: Jim Porter Date: Fri, 4 Oct 2024 22:26:01 -0700 Subject: [PATCH 3/3] 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. --- doc/misc/eshell.texi | 19 +++++++++++--- etc/NEWS | 14 +++++++++++ lisp/eshell/esh-cmd.el | 42 ++++++++++++++++++------------- test/lisp/eshell/esh-cmd-tests.el | 22 +++++++++++++--- 4 files changed, 73 insertions(+), 24 deletions(-) diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi index bbb6b2e6aac..9a2714b14fb 100644 --- a/doc/misc/eshell.texi +++ b/doc/misc/eshell.texi @@ -1698,16 +1698,29 @@ Control Flow @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 diff --git a/etc/NEWS b/etc/NEWS index e4c1ef4eae0..adf47b61a5e 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -217,6 +217,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 diff --git a/lisp/eshell/esh-cmd.el b/lisp/eshell/esh-cmd.el index 65f997e5b88..c9096b0d159 100644 --- a/lisp/eshell/esh-cmd.el +++ b/lisp/eshell/esh-cmd.el @@ -551,12 +551,14 @@ eshell-rewrite-for-command ,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 @@ eshell-structure-basic-command (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. diff --git a/test/lisp/eshell/esh-cmd-tests.el b/test/lisp/eshell/esh-cmd-tests.el index 9e4cbc58201..0f388a9eba4 100644 --- a/test/lisp/eshell/esh-cmd-tests.el +++ b/test/lisp/eshell/esh-cmd-tests.el @@ -427,11 +427,15 @@ esh-cmd-test/if-statement (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 @@ esh-cmd-test/if-else-statement-ext-cmd (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")) -- 2.25.1