Handle Lisp errors in Eshell workers

* lisp/eshell/esh-worker.el (eshell-worker-name): New generic function.
(eshell-worker--error): New defsubst.
(eshell-output-object-to-target, eshell-close-target): Wrap
'cl-call-next-method' in error handlers.

* test/lisp/eshell/esh-worker-tests.el
(esh-worker-test/pipe/error-handling)
(esh-worker-test/map-lines/error-handling)
(esh-worker-test/apply-lines/error-handling): New tests.
This commit is contained in:
Jim Porter 2026-05-27 18:35:14 -07:00
parent a1cbfe27b1
commit e9847f8263
2 changed files with 60 additions and 6 deletions

View file

@ -57,6 +57,8 @@
(require 'esh-io)
(declare-function eshell-set-exit-info "esh-cmd" (status &optional result))
(defgroup eshell-worker nil
"Eshell workers provide a way to construct process-like objects in Emacs
Lisp that can serve as pipe targets, allowing you to manipulating other
@ -106,6 +108,14 @@ task in Eshell."
This just returns RAW-TARGET."
raw-target)
(cl-defgeneric eshell-worker-name (_worker)
"Return the program name for WORKER."
nil)
(defsubst eshell-worker--error (worker error)
(eshell-errorn (format "%s: %s" (eshell-worker-name worker)
(error-message-string error))))
(cl-defmethod eshell-output-object-to-target :around
(_object (target eshell-worker))
"Output OBJECT to the Eshell worker TARGET.
@ -116,7 +126,11 @@ for let-binding the proper value for `eshell-current-handles'."
(let ((eshell-current-handles (eshell-worker-output-handles target))
(eshell-ensure-newline-p (eshell-worker-ensure-newline-p target)))
(with-current-buffer (eshell-worker-eshell-buffer target)
(cl-call-next-method))
(eshell-condition-case err
(cl-call-next-method)
(error
(eshell-worker--error target err)
(eshell-set-exit-info 1))))
(setf (eshell-worker-ensure-newline-p target) eshell-ensure-newline-p)))
(cl-defmethod eshell-close-target :around ((target eshell-worker) _status)
@ -129,11 +143,17 @@ closing the handles when done, and calling
(let ((eshell-current-handles (eshell-worker-output-handles target))
(eshell-ensure-newline-p (eshell-worker-ensure-newline-p target)))
(with-current-buffer (eshell-worker-eshell-buffer target)
(cl-call-next-method)
(setf (eshell-worker-status target) 'exit)
(eshell-close-handles)
(declare-function eshell-kill-process-function "esh-proc" (proc status))
(eshell-kill-process-function target "finished\n")))))
(unwind-protect
(eshell-condition-case err
(cl-call-next-method)
(error
(eshell-worker--error target err)
(eshell-set-exit-info 1)))
(setf (eshell-worker-status target) 'exit)
(eshell-close-handles)
(declare-function eshell-kill-process-function "esh-proc"
(proc status))
(eshell-kill-process-function target "finished\n"))))))
(defun eshell--apply-print (function args)
"Call FUNCTION with Eshell-converted ARGS and print the result."
@ -162,6 +182,9 @@ pass the value unaltered to FUNCTION."
(setf (eshell-accumulate-worker-buffer-or-value worker)
(generate-new-buffer " *eshell-worker*" t)))
(cl-defmethod eshell-worker-name ((worker eshell-accumulate-worker))
(symbol-name (eshell-accumulate-worker-function worker)))
(cl-defmethod eshell-output-object-to-target
(object (target eshell-accumulate-worker))
"Send OBJECT to the accumulate-worker TARGET.
@ -223,6 +246,9 @@ specified value."
(let ((function (eshell-map-lines-worker-function target)))
(eshell--apply-print function (list line))))
(cl-defmethod eshell-worker-name ((worker eshell-map-lines-worker))
(format "map-lines %s" (eshell-map-lines-worker-function worker)))
(cl-defmethod eshell-output-object-to-target
(object (target eshell-map-lines-worker))
"Send OBJECT to the map-lines worker TARGET.
@ -273,6 +299,9 @@ other data types to this worker (e.g. lists), each object is passed as a
single argument to FUNCTION."
args) ; Arguments stored in reverse order
(cl-defmethod eshell-worker-name ((worker eshell-apply-lines-worker))
(format "apply-lines %s" (eshell-map-lines-worker-function worker)))
(cl-defmethod eshell-map-lines-worker--apply
(line (target eshell-apply-lines-worker))
(push line (eshell-apply-lines-worker-args target)))

View file

@ -90,6 +90,14 @@
"echo hi | #'upcase | (lambda (i) (concat \"> \" i))"
"\\`> HI\n\\'")))
(ert-deftest esh-worker-test/pipe/error-handling ()
"Test that Eshell workers catch errors."
(with-temp-eshell
(eshell-match-command-output
"echo hi | #'1+"
"\\`1\\+: Wrong type argument: number-or-marker-p, \"hi\"\n\\'")
(should (= eshell-last-command-status 1))))
;; `map-lines' pipelines
@ -151,6 +159,14 @@ It should call the mapped function once per line."
"{echo '10\n1'; echo '5\n20'} | map-lines #'1+"
"\\`11\n16\n21\n\\'")))
(ert-deftest esh-worker-test/map-lines/error-handling ()
"Test that `map-lines' catches errors."
(with-temp-eshell
(eshell-match-command-output
"echo hi | map-lines #'1+"
"\\`map-lines 1\\+: Wrong type argument: number-or-marker-p, \"hi\"\n\\'")
(should (= eshell-last-command-status 1))))
;; `apply-lines' pipelines
@ -196,4 +212,13 @@ It should pass each line as an argument to the applied function."
"{echo '10\n1'; echo '5\n20'} | apply-lines #'+"
"\\`45\n\\'")))
(ert-deftest esh-worker-test/apply-lines/error-handling ()
"Test that `apply-lines' catches errors."
(with-temp-eshell
(eshell-match-command-output
"echo hi | apply-lines #'1+"
(concat "\\`apply-lines 1\\+: Wrong type argument: number-or-marker-p, "
"\"hi\"\n\\'"))
(should (= eshell-last-command-status 1))))
;;; esh-io-tests.el ends here