]> git.eshelyaron.com Git - emacs.git/commitdiff
Make '$?' and '$$' variables more consistent in Eshell
authorJim Porter <jporterbugs@gmail.com>
Wed, 10 Aug 2022 03:09:57 +0000 (20:09 -0700)
committerJim Porter <jporterbugs@gmail.com>
Sat, 13 Aug 2022 05:07:13 +0000 (22:07 -0700)
Previously, '$?' (last exit code) was only useful for external
commands, and '$$' (last result) was only useful for Lisp commands.

* lisp/eshell/esh-cmd.el (eshell-lisp-form-nil-is-failure): New
option.
(eshell-lisp-command): Set last exit code to 1 when the command
signals an error, and 2 if it returns nil (for Lisp forms only).

* lisp/eshell/esh-proc.el (eshell-sentinel): Set last result to t if
the command succeeded.

* test/lisp/eshell/esh-cmd-tests.el
(esh-cmd-test/while-loop-lisp-form, esh-cmd-test/until-loop-lisp-form)
(esh-cmd-test/if-else-statement-lisp-form)
(esh-cmd-test/if-else-statement-lisp-form-2)
(esh-cmd-test/unless-else-statement-lisp-form): New tests.

* test/lisp/eshell/esh-var-tests.el
(esh-var-test/last-status-var-lisp-command)
(esh-var-test/last-status-var-lisp-form)
(esh-var-test/last-status-var-lisp-form-2)
(esh-var-test/last-status-var-ext-cmd)
(esh-var-test/last-status-var-ext-cmd): New tests.
(esh-var-test/last-result-var2): Rename from this...
( esh-var-test/last-result-var-twice): ... to this.

* doc/misc/eshell.texi (Variables): Update documentation about '$?'
and '$$'.
(Control Flow): Mention that '(lisp forms)' can be used as
conditionals.

* etc/NEWS: Announce this change (bug#57129).

doc/misc/eshell.texi
etc/NEWS
lisp/eshell/esh-cmd.el
lisp/eshell/esh-proc.el
test/lisp/eshell/esh-cmd-tests.el
test/lisp/eshell/esh-var-tests.el

index 141c30ae9b94d4a9575291a79c356564ca6abae8..aae779575d554a9c959a039c35ec7bc672feb29c 100644 (file)
@@ -890,14 +890,18 @@ command (excluding the command name itself).
 
 @vindex $$
 @item $$
-This is the result of the last command.  In case of an external
-command, it is @code{t} or @code{nil}.
+This is the result of the last command.  For external commands, it is
+@code{t} if the exit code was 0 or @code{nil} otherwise.
 
+@vindex eshell-lisp-form-nil-is-failure
 @vindex $?
 @item $?
 This variable contains the exit code of the last command.  If the last
 command was a Lisp function, it is 0 for successful completion or 1
-otherwise.
+otherwise.  If @code{eshell-lisp-form-nil-is-failure} is
+non-@code{nil}, then a command with a Lisp form, like
+@samp{(@var{command} @var{args}@dots{})}, that returns @code{nil} will
+set this variable to 2.
 
 @vindex $COLUMNS
 @vindex $LINES
@@ -1024,8 +1028,8 @@ 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} @}}, the condition is satisfied if the
-@var{subcommand}'s exit status is 0.
+@var{subcommand} @}} or @samp{(@var{lisp form})}, the condition is
+satisfied if the command's exit status is 0.
 
 @table @code
 
index be647f6bbbc234992a0d301fe92f2b9101fa4ff3..f876916bb6f68cc7dfbce73b6e0a955c8853f1da 100644 (file)
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -2162,6 +2162,13 @@ Additionally, globs ending with '**/' or '***/' no longer raise an
 error, and now expand to all directories recursively (following
 symlinks in the latter case).
 
++++
+*** Lisp forms in Eshell now treat a 'nil' result as a failed exit status.
+When executing a command that looks like '(lisp form)', Eshell will
+set the exit status (available in the '$?' variable) to 2.  This
+allows commands like that to be used as conditionals.  To change this
+behavior, customize the new 'eshell-lisp-form-nil-is-failure' option.
+
 ** Shell
 
 ---
index 454a90e91d3016c5742183417547e8244da542c7..62c95056fd255c2fc6a181c812680dc7a896bf07 100644 (file)
@@ -133,6 +133,10 @@ There are several different kinds of commands, however."
 Such arguments will be passed to `read', and then evaluated."
   :type 'regexp)
 
+(defcustom eshell-lisp-form-nil-is-failure t
+  "If non-nil, Lisp forms like (COMMAND ARGS) treat a nil result as failure."
+  :type 'boolean)
+
 (defcustom eshell-pre-command-hook nil
   "A hook run before each interactive command is invoked."
   :type 'hook)
@@ -1412,43 +1416,53 @@ via `eshell-errorn'."
 (defun eshell-lisp-command (object &optional args)
   "Insert Lisp OBJECT, using ARGS if a function."
   (catch 'eshell-external               ; deferred to an external command
+    (setq eshell-last-command-status 0
+          eshell-last-arguments args)
     (let* ((eshell-ensure-newline-p (eshell-interactive-output-p))
+           (command-form-p (functionp object))
            (result
-            (if (functionp object)
-                (progn
-                  (setq eshell-last-arguments args
-                        eshell-last-command-name
+            (if command-form-p
+                (let ((numeric (not (get object
+                                         'eshell-no-numeric-conversions)))
+                      (fname-args (get object 'eshell-filename-arguments)))
+                  (when (or numeric fname-args)
+                    (while args
+                      (let ((arg (car args)))
+                        (cond
+                         ((and numeric (stringp arg) (> (length arg) 0)
+                               (text-property-any 0 (length arg)
+                                                  'number t arg))
+                          ;; If any of the arguments are flagged as
+                          ;; numbers waiting for conversion, convert
+                          ;; them now.
+                          (setcar args (string-to-number arg)))
+                         ((and fname-args (stringp arg)
+                               (string-equal arg "~"))
+                          ;; If any of the arguments match "~",
+                          ;; prepend "./" to treat it as a regular
+                          ;; file name.
+                          (setcar args (concat "./" arg)))))
+                      (setq args (cdr args))))
+                  (setq eshell-last-command-name
                         (concat "#<function " (symbol-name object) ">"))
-                  (let ((numeric (not (get object
-                                           'eshell-no-numeric-conversions)))
-                        (fname-args (get object 'eshell-filename-arguments)))
-                    (when (or numeric fname-args)
-                      (while args
-                        (let ((arg (car args)))
-                          (cond ((and numeric (stringp arg) (> (length arg) 0)
-                                      (text-property-any 0 (length arg)
-                                                         'number t arg))
-                                 ;; If any of the arguments are
-                                 ;; flagged as numbers waiting for
-                                 ;; conversion, convert them now.
-                                 (setcar args (string-to-number arg)))
-                                ((and fname-args (stringp arg)
-                                      (string-equal arg "~"))
-                                 ;; If any of the arguments match "~",
-                                 ;; prepend "./" to treat it as a
-                                 ;; regular file name.
-                                 (setcar args (concat "./" arg)))))
-                        (setq args (cdr args)))))
                   (eshell-apply object eshell-last-arguments))
-              (setq eshell-last-arguments args
-                    eshell-last-command-name "#<Lisp object>")
+              (setq eshell-last-command-name "#<Lisp object>")
               (eshell-eval object))))
       (if (and eshell-ensure-newline-p
               (save-excursion
                 (goto-char eshell-last-output-end)
                 (not (bolp))))
          (eshell-print "\n"))
-      (eshell-close-handles 0 (list 'quote result)))))
+      (eshell-close-handles
+       ;; If `eshell-lisp-form-nil-is-failure' is non-nil, Lisp forms
+       ;; that succeeded but have a nil result should have an exit
+       ;; status of 2.
+       (when (and eshell-lisp-form-nil-is-failure
+                  (not command-form-p)
+                  (= eshell-last-command-status 0)
+                  (not result))
+         2)
+       (list 'quote result)))))
 
 (defalias 'eshell-lisp-command* #'eshell-lisp-command)
 
index 99b43661f2cbad8ee800e6b58b25b574ce335ba2..c367b5cd64386d3fcb522c544c479e2625e25ceb 100644 (file)
@@ -346,7 +346,9 @@ Used only on systems which do not support async subprocesses.")
         (defvar eshell-last-output-end)         ;Defined in esh-mode.el.
        (eshell-update-markers eshell-last-output-end)
        ;; Simulate the effect of eshell-sentinel.
-       (eshell-close-handles (if (numberp exit-status) exit-status -1))
+       (eshell-close-handles
+         (if (numberp exit-status) exit-status -1)
+         (list 'quote (and (numberp exit-status) (= exit-status 0))))
        (eshell-kill-process-function command exit-status)
        (or (bound-and-true-p eshell-in-pipeline-p)
            (setq eshell-last-sync-output-start nil))
@@ -398,40 +400,36 @@ PROC is the process that's exiting.  STRING is the exit message."
   (when (buffer-live-p (process-buffer proc))
     (with-current-buffer (process-buffer proc)
       (unwind-protect
-          (let ((entry (assq proc eshell-process-list)))
-;          (if (not entry)
-;              (error "Sentinel called for unowned process `%s'"
-;                     (process-name proc))
-           (when entry
-             (unwind-protect
-                 (progn
-                   (unless (string= string "run")
-                      ;; Write the exit message if the status is
-                      ;; abnormal and the process is already writing
-                      ;; to the terminal.
-                      (when (and (eq proc (eshell-tail-process))
-                                 (not (string-match "^\\(finished\\|exited\\)"
-                                                    string)))
-                        (funcall (process-filter proc) proc string))
-                      (let ((handles (nth 1 entry))
-                            (str (prog1 (nth 3 entry)
-                                   (setf (nth 3 entry) nil)))
-                            (status (process-exit-status proc)))
-                        ;; If we're in the middle of handling output
-                        ;; from this process then schedule the EOF for
-                        ;; later.
-                        (letrec ((finish-io
-                                  (lambda ()
-                                    (if (nth 4 entry)
-                                        (run-at-time 0 nil finish-io)
-                                      (when str
-                                        (ignore-error 'eshell-pipe-broken
-                                          (eshell-output-object
-                                           str nil handles)))
-                                      (eshell-close-handles
-                                       status 'nil handles)))))
-                          (funcall finish-io)))))
-               (eshell-remove-process-entry entry))))
+          (when-let ((entry (assq proc eshell-process-list)))
+           (unwind-protect
+               (unless (string= string "run")
+                  ;; Write the exit message if the status is
+                  ;; abnormal and the process is already writing
+                  ;; to the terminal.
+                  (when (and (eq proc (eshell-tail-process))
+                             (not (string-match "^\\(finished\\|exited\\)"
+                                                string)))
+                    (funcall (process-filter proc) proc string))
+                  (let ((handles (nth 1 entry))
+                        (str (prog1 (nth 3 entry)
+                               (setf (nth 3 entry) nil)))
+                        (status (process-exit-status proc)))
+                    ;; If we're in the middle of handling output
+                    ;; from this process then schedule the EOF for
+                    ;; later.
+                    (letrec ((finish-io
+                              (lambda ()
+                                (if (nth 4 entry)
+                                    (run-at-time 0 nil finish-io)
+                                  (when str
+                                    (ignore-error 'eshell-pipe-broken
+                                      (eshell-output-object
+                                       str nil handles)))
+                                  (eshell-close-handles
+                                   status (list 'quote (= status 0))
+                                   handles)))))
+                      (funcall finish-io))))
+             (eshell-remove-process-entry entry)))
        (eshell-kill-process-function proc string)))))
 
 (defun eshell-process-interact (func &optional all query)
index b31159a1a8ff46601ac2ec123a16365b49379d11..e86985ec7177e5cd520d4dc0adfc679335260620 100644 (file)
@@ -139,6 +139,15 @@ e.g. \"{(+ 1 2)} 3\" => 3"
               "{ setq eshell-test-value (cdr eshell-test-value) }")
       "(1 2)\n(2)\n"))))
 
+(ert-deftest esh-cmd-test/while-loop-lisp-form ()
+  "Test invocation of a while loop using a Lisp form."
+  (with-temp-eshell
+   (let ((eshell-test-value 0))
+     (eshell-command-result-p
+      (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 "["))
@@ -158,6 +167,16 @@ e.g. \"{(+ 1 2)} 3\" => 3"
               "{ setq eshell-test-value t }")
       "t\n"))))
 
+(ert-deftest esh-cmd-test/until-loop-lisp-form ()
+  "Test invocation of an until loop using a Lisp form."
+  (skip-unless (executable-find "["))
+  (with-temp-eshell
+   (let ((eshell-test-value 0))
+     (eshell-command-result-p
+      (concat "until (= eshell-test-value 3) "
+              "{ setq eshell-test-value (1+ eshell-test-value) }")
+      "1\n2\n3\n"))))
+
 (ert-deftest esh-cmd-test/until-loop-ext-cmd ()
   "Test invocation of an until loop using an external command."
   (skip-unless (executable-find "["))
@@ -188,6 +207,30 @@ e.g. \"{(+ 1 2)} 3\" => 3"
      (eshell-command-result-p "if $eshell-test-value {echo yes} {echo no}"
                               "no\n"))))
 
+(ert-deftest esh-cmd-test/if-else-statement-lisp-form ()
+  "Test invocation of an if/else statement using a Lisp form."
+  (with-temp-eshell
+   (eshell-command-result-p "if (zerop 0) {echo yes} {echo no}"
+                            "yes\n")
+   (eshell-command-result-p "if (zerop 1) {echo yes} {echo no}"
+                            "no\n")
+   (let ((debug-on-error nil))
+     (eshell-command-result-p "if (zerop \"foo\") {echo yes} {echo no}"
+                              "no\n"))))
+
+(ert-deftest esh-cmd-test/if-else-statement-lisp-form-2 ()
+  "Test invocation of an if/else statement using a Lisp form.
+This tests when `eshell-lisp-form-nil-is-failure' is nil."
+  (let ((eshell-lisp-form-nil-is-failure nil))
+    (with-temp-eshell
+     (eshell-command-result-p "if (zerop 0) {echo yes} {echo no}"
+                              "yes\n")
+     (eshell-command-result-p "if (zerop 1) {echo yes} {echo no}"
+                              "yes\n")
+     (let ((debug-on-error nil))
+       (eshell-command-result-p "if (zerop \"foo\") {echo yes} {echo no}"
+                                "no\n")))))
+
 (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 "["))
@@ -217,6 +260,17 @@ e.g. \"{(+ 1 2)} 3\" => 3"
      (eshell-command-result-p "unless $eshell-test-value {echo no} {echo yes}"
                               "no\n"))))
 
+(ert-deftest esh-cmd-test/unless-else-statement-lisp-form ()
+  "Test invocation of an unless/else statement using a Lisp form."
+  (with-temp-eshell
+   (eshell-command-result-p "unless (zerop 0) {echo no} {echo yes}"
+                            "yes\n")
+   (eshell-command-result-p "unless (zerop 1) {echo no} {echo yes}"
+                            "no\n")
+   (let ((debug-on-error nil))
+     (eshell-command-result-p "unless (zerop \"foo\") {echo no} {echo yes}"
+                              "no\n"))))
+
 (ert-deftest esh-cmd-test/unless-else-statement-ext-cmd ()
   "Test invocation of an unless/else statement using an external command."
   (skip-unless (executable-find "["))
index 54e701a6aabbb79d58ba426f320f6e8f0c31139e..66dabd424bd39607da1dc5d3ef444bbffbcea045 100644 (file)
@@ -500,18 +500,72 @@ inside double-quotes"
    (eshell-command-result-p "echo $INSIDE_EMACS[, 1]"
                             "eshell")))
 
+(ert-deftest esh-var-test/last-status-var-lisp-command ()
+  "Test using the \"last exit status\" ($?) variable with a Lisp command"
+  (with-temp-eshell
+   (eshell-command-result-p "zerop 0; echo $?"
+                            "t\n0\n")
+   (eshell-command-result-p "zerop 1; echo $?"
+                            "0\n")
+   (let ((debug-on-error nil))
+     (eshell-command-result-p "zerop foo; echo $?"
+                              "1\n"))))
+
+(ert-deftest esh-var-test/last-status-var-lisp-form ()
+  "Test using the \"last exit status\" ($?) variable with a Lisp form"
+  (let ((eshell-lisp-form-nil-is-failure t))
+  (with-temp-eshell
+   (eshell-command-result-p "(zerop 0); echo $?"
+                            "t\n0\n")
+   (eshell-command-result-p "(zerop 1); echo $?"
+                            "2\n")
+   (let ((debug-on-error nil))
+     (eshell-command-result-p "(zerop \"foo\"); echo $?"
+                              "1\n")))))
+
+(ert-deftest esh-var-test/last-status-var-lisp-form-2 ()
+  "Test using the \"last exit status\" ($?) variable with a Lisp form.
+This tests when `eshell-lisp-form-nil-is-failure' is nil."
+  (let ((eshell-lisp-form-nil-is-failure nil))
+    (with-temp-eshell
+     (eshell-command-result-p "(zerop 0); echo $?"
+                              "0\n")
+     (eshell-command-result-p "(zerop 0); echo $?"
+                              "0\n")
+     (let ((debug-on-error nil))
+       (eshell-command-result-p "(zerop \"foo\"); echo $?"
+                                "1\n")))))
+
+(ert-deftest esh-var-test/last-status-var-ext-cmd ()
+  "Test using the \"last exit status\" ($?) variable with an external command"
+  (skip-unless (executable-find "["))
+  (with-temp-eshell
+   (eshell-command-result-p "[ foo = foo ]; echo $?"
+                            "0\n")
+   (eshell-command-result-p "[ foo = bar ]; echo $?"
+                            "1\n")))
+
 (ert-deftest esh-var-test/last-result-var ()
   "Test using the \"last result\" ($$) variable"
   (with-temp-eshell
    (eshell-command-result-p "+ 1 2; + $$ 2"
                             "3\n5\n")))
 
-(ert-deftest esh-var-test/last-result-var2 ()
+(ert-deftest esh-var-test/last-result-var-twice ()
   "Test using the \"last result\" ($$) variable twice"
   (with-temp-eshell
    (eshell-command-result-p "+ 1 2; + $$ $$"
                             "3\n6\n")))
 
+(ert-deftest esh-var-test/last-result-var-ext-cmd ()
+  "Test using the \"last result\" ($$) variable with an external command"
+  (skip-unless (executable-find "["))
+  (with-temp-eshell
+   (eshell-command-result-p "[ foo = foo ]; format \"%s\" $$"
+                            "t\n")
+   (eshell-command-result-p "[ foo = bar ]; format \"%s\" $$"
+                            "nil\n")))
+
 (ert-deftest esh-var-test/last-result-var-split-indices ()
   "Test using the \"last result\" ($$) variable with split indices"
   (with-temp-eshell