Allow evaluating Eshell forms using lexical binding

* lisp/eshell/esh-cmd.el (eshell-lexical-binding): New option.
(eshell--eval): New defsubst...
(eshell-do-eval, eshell-exec-lisp):
* lisp/eshell/esh-var.el (eshell-parse-variable-ref):
* lisp/eshell/em-banner.el (eshell-banner-initialize): ... use it.

* lisp/eshell/em-ls.el (eshell-ls-applicable): Evaluate form using
lexical binding.

* test/lisp/eshell/esh-cmd-tests.el (esh-cmd-test--binding-check): New
function.
(esh-cmd-test/lexical-binding, esh-cmd-test/dynamic-binding): New tests.

* etc/NEWS: Announce this change.
This commit is contained in:
Jim Porter 2026-08-19 18:10:33 -07:00
parent 779a37c076
commit f856da36cd
6 changed files with 51 additions and 11 deletions

View file

@ -286,6 +286,11 @@ some other command:
See the "(eshell) Lisp Pipelines" node in the Eshell manual for more
details.
---
*** New user option 'eshell-lexical-binding'.
When enabled, this causes Eshell to evaluate commands in Lisp syntax
using lexical binding.
* New Modes and Packages in Emacs 32.1

View file

@ -41,6 +41,7 @@
(eval-when-compile
(require 'cl-lib))
(require 'esh-cmd)
(require 'esh-util)
(require 'esh-mode)
@ -77,7 +78,7 @@ This can be any sexp, and should end with at least two newlines."
(unless eshell-non-interactive-p
(cl-assert eshell-mode)
(cl-assert eshell-banner-message)
(let ((msg (eval eshell-banner-message)))
(let ((msg (eshell--eval eshell-banner-message)))
(cl-assert msg)
(eshell-interactive-print msg))))

View file

@ -212,7 +212,7 @@ calling FUNC with FILE as an argument."
(not (eq (aref modes (+ ,index 3)) ?-)))
(t
;; Otherwise call FUNC.
(,(eval func) ,file)))))
(,(eval func t) ,file)))))
(defcustom eshell-ls-highlight-alist nil
"This alist correlates test functions to color.

View file

@ -126,6 +126,10 @@ There are several different kinds of commands, however."
"If non-nil, prefer Lisp functions to external commands."
:type 'boolean)
(defcustom eshell-lexical-binding lexical-binding
"If non-nil, use lexical binding when evaluating Eshell forms."
:type 'boolean)
(defcustom eshell-lisp-regexp "\\([(`]\\|#'\\)"
"A regexp which, if matched at beginning of an argument, means Lisp.
Such arguments will be passed to `read', and then evaluated."
@ -1141,6 +1145,10 @@ the form (:eshell-background . PROCESSES)."
(eshell-always-debug-command 'form
"done %s\n\n%s" ,tag-symbol (eshell-stringify ,form)))))))
(defsubst eshell--eval (form)
"Evaluate FORM, respecting `eshell-lexical-binding'."
(eval form eshell-lexical-binding))
(defun eshell-do-eval (form &optional synchronous-p)
"Evaluate FORM, simplifying it as we go.
Unless SYNCHRONOUS-P is non-nil, throws `eshell-defer' if it needs to
@ -1155,7 +1163,7 @@ again. Any forms preceding one that throw `eshell-defer' will
have been replaced by constants."
(cond
((not (listp form))
(list 'quote (eval form)))
(list 'quote (eshell--eval form)))
((memq (car form) '(quote function))
form)
(t
@ -1232,10 +1240,10 @@ have been replaced by constants."
(eshell-do-eval form synchronous-p)))))
((eq (car form) 'setcar)
(setcar (cdr args) (eshell-do-eval (cadr args) synchronous-p))
(eval form))
(eshell--eval form))
((eq (car form) 'setcdr)
(setcar (cdr args) (eshell-do-eval (cadr args) synchronous-p))
(eval form))
(eshell--eval form))
((eq (car form) 'let)
(unless (eq (car-safe (cadr args)) 'eshell-do-eval)
(eshell-manipulate form "evaluating let args"
@ -1251,7 +1259,7 @@ have been replaced by constants."
(car args))
;; These expressions should all be constants now.
(mapcar (lambda (binding)
(when (consp binding) (eval (cadr binding))))
(when (consp binding) (eshell--eval (cadr binding))))
(car args))
(let (deferred result)
;; Evaluate the `let' body, catching `eshell-defer' so we
@ -1291,7 +1299,7 @@ have been replaced by constants."
(unless (eq (caar args) 'eshell-do-eval)
(eshell-manipulate form "handling special form"
(setcar args `(eshell-do-eval ',(car args) ,synchronous-p))))
(eval form))
(eshell--eval form))
((eq (car form) 'unwind-protect)
;; `unwind-protect' has to be handled specially, because we
;; only want to call `eshell-do-eval' on its first form, and
@ -1317,7 +1325,7 @@ have been replaced by constants."
(if (cddr args) (error "Unsupported form (setq X1 E1 X2 E2..)"))
(eshell-manipulate form "evaluating arguments to setq"
(setcar (cdr args) (eshell-do-eval (cadr args) synchronous-p)))
(list 'quote (eval form)))
(list 'quote (eshell--eval form)))
(t
(if (and args (not (memq (car form) '(run-hooks))))
(eshell-manipulate form
@ -1358,7 +1366,7 @@ have been replaced by constants."
(new-form
(catch 'eshell-replace-command
(ignore
(setq result (eval form))))))
(setq result (eshell--eval form))))))
(if new-form
(progn
(eshell-manipulate form "substituting replacement form"
@ -1484,7 +1492,7 @@ case."
(let ((result
(save-current-buffer
(if form-p
(eval func-or-form)
(eshell--eval func-or-form)
(apply func-or-form args)))))
(and result (funcall printer result))
result)

View file

@ -593,7 +593,8 @@ Possible variable references are:
(eshell-parse-double-quote)))))))
(throw 'eshell-incomplete (concat "$" delim)))
(when name
`(eshell-get-variable ,(eval name) indices ,eshell-current-quoted)))))
`(eshell-get-variable ,(eshell--eval name) indices
,eshell-current-quoted)))))
((assoc (char-to-string (char-after))
eshell-variable-aliases-list)
(forward-char)

View file

@ -627,6 +627,31 @@ NAME is the name of the test case."
(esh-cmd-test--deftest-invoke-directly complex "ls ." nil)
(esh-cmd-test--deftest-invoke-directly complex-subcmd "echo {ls .}" nil)
;; Lexical/dynamic binding
(defun esh-cmd-test--binding-check ()
"Run a sequence of Eshell commands that depend on the binding type."
(unwind-protect
(with-temp-eshell
(eshell-insert-command "(defun test-function () test-value)")
(eshell-insert-command "(setq test-value 1)")
(eshell-insert-command "(let ((test-value 2)) (test-function))")
(eshell-last-output))
(with-no-warnings
(fmakunbound #'test-function)
(makunbound 'test-value))))
(ert-deftest esh-cmd-test/lexical-binding ()
"Test that enabling `eshell-lexical-binding' works."
(let ((eshell-lexical-binding t))
(should (string-match-p "\\`1\n\\'" (esh-cmd-test--binding-check)))))
(ert-deftest esh-cmd-test/dynamic-binding ()
"Test that disabling `eshell-lexical-binding' works."
(let ((eshell-lexical-binding nil))
(should (string-match-p "\\`2\n\\'" (esh-cmd-test--binding-check)))))
;; Error handling