Fix documented Eshell behavior of ignoring leading nils in commands

* lisp/eshell/esh-var.el (eshell-handle-local-variables): Simplify,
and move leading-nil handling to...
* lisp/eshell/esh-cmd.el (eshell-named-command): ... here.

* test/lisp/eshell/esh-cmd-tests.el (esh-cmd-test/skip-leading-nils):
* test/lisp/eshell/esh-var-tests.el
(esh-var-test/local-variables/skip-nil): New tests.

* doc/misc/eshell.texi (Expansion): Document this behavior.
This commit is contained in:
Jim Porter 2023-09-15 13:40:37 -07:00
parent e7e925f062
commit bc25d76650
5 changed files with 61 additions and 46 deletions

View file

@ -1385,9 +1385,15 @@ Concatenate the string representation of each value.
@node Dollars Expansion
@section Dollars Expansion
Eshell has different @code{$} expansion syntax from other shells. There
are some similarities, but don't let these lull you into a false sense
of familiarity.
Like in many other shells, you can use @code{$} expansions to insert
various values into your Eshell invocations. While Eshell's @code{$}
expansion syntax has some similarities to the syntax from other
shells, there are also many differences. Don't let these similarities
lull you into a false sense of familiarity.
When using command form (@pxref{Invocation}), Eshell will ignore any
leading nil values, so if @var{foo} is @code{nil}, @samp{$@var{foo}
echo hello} is equivalent to @samp{echo hello}.
@table @code

View file

@ -1286,16 +1286,24 @@ have been replaced by constants."
COMMAND may result in an alias being executed, or a plain command."
(unless eshell-allow-commands
(signal 'eshell-commands-forbidden '(named)))
;; Strip off any leading nil values. This can only happen if a
;; variable evaluates to nil, such as "$var x", where `var' is nil.
;; In that case, the command name becomes `x', for compatibility
;; with most regular shells (the difference is that they do an
;; interpolation pass before the argument parsing pass, but Eshell
;; does both at the same time).
(while (and (not command) args)
(setq command (pop args)))
(setq eshell-last-arguments args
eshell-last-command-name (eshell-stringify command))
eshell-last-command-name (eshell-stringify command))
(run-hook-with-args 'eshell-prepare-command-hook)
(cl-assert (stringp eshell-last-command-name))
(if eshell-last-command-name
(or (run-hook-with-args-until-success
'eshell-named-command-hook eshell-last-command-name
eshell-last-arguments)
(eshell-plain-command eshell-last-command-name
eshell-last-arguments))))
(when eshell-last-command-name
(or (run-hook-with-args-until-success
'eshell-named-command-hook eshell-last-command-name
eshell-last-arguments)
(eshell-plain-command eshell-last-command-name
eshell-last-arguments))))
(defalias 'eshell-named-command* 'eshell-named-command)

View file

@ -296,43 +296,30 @@ copied (a.k.a. \"exported\") to the environment of created subprocesses."
(defun eshell-handle-local-variables ()
"Allow for the syntax `VAR=val <command> <args>'."
;; strip off any null commands, which can only happen if a variable
;; evaluates to nil, such as "$var x", where `var' is nil. The
;; command name in that case becomes `x', for compatibility with
;; most regular shells (the difference is that they do an
;; interpolation pass before the argument parsing pass, but Eshell
;; does both at the same time).
(while (and (not eshell-last-command-name)
eshell-last-arguments)
(setq eshell-last-command-name (car eshell-last-arguments)
eshell-last-arguments (cdr eshell-last-arguments)))
;; Eshell handles local variable settings (e.g. 'CFLAGS=-O2 make')
;; by making the whole command into a subcommand, and calling
;; `eshell-set-variable' immediately before the command is invoked.
;; This means that 'FOO=x cd bar' won't work exactly as expected,
;; but that is by no means a typical use of local environment
;; variables.
(let ((setvar "\\`\\([A-Za-z_][A-Za-z0-9_]*\\)=\\(.*\\)\\'")
(command (eshell-stringify eshell-last-command-name))
(args eshell-last-arguments))
;; local variable settings (such as 'CFLAGS=-O2 make') are handled
;; by making the whole command into a subcommand, and calling
;; setenv immediately before the command is invoked. This means
;; that 'BLAH=x cd blah' won't work exactly as expected, but that
;; is by no means a typical use of local environment variables.
(if (and command (string-match setvar command))
(throw
'eshell-replace-command
(list
'eshell-as-subcommand
(append
(list 'progn)
(let ((l (list t)))
(while (string-match setvar command)
(nconc
l (list
(list 'eshell-set-variable
(match-string 1 command)
(match-string 2 command))))
(setq command (eshell-stringify (car args))
args (cdr args)))
(cdr l))
(list (list 'eshell-named-command
command (list 'quote args)))))))))
(command eshell-last-command-name)
(args eshell-last-arguments))
(when (and (stringp command) (string-match setvar command))
(throw 'eshell-replace-command
`(eshell-as-subcommand
(progn
,@(let (locals)
(while (and (stringp command)
(string-match setvar command))
(push `(eshell-set-variable
,(match-string 1 command)
,(match-string 2 command))
locals)
(setq command (pop args)))
(nreverse locals))
(eshell-named-command ,command ,(list 'quote args)))
)))))
(defun eshell-interpolate-variable ()
"Parse a variable interpolation.

View file

@ -80,6 +80,12 @@ e.g. \"{(+ 1 2)} 3\" => 3"
(eshell-match-command-output "echo ${echo $value}"
"hello\n")))
(ert-deftest esh-cmd-test/skip-leading-nils ()
"Test that Eshell skips leading nil arguments for named commands."
(eshell-command-result-equal "$eshell-test-value echo hello" "hello")
(eshell-command-result-equal
"$eshell-test-value $eshell-test-value echo hello" "hello"))
(ert-deftest esh-cmd-test/let-rebinds-after-defer ()
"Test that let-bound values are properly updated after `eshell-defer'.
When inside a `let' block in an Eshell command form, we need to

View file

@ -645,6 +645,14 @@ nil, use FUNCTION instead."
(eshell-match-command-output "VAR=hello env" "VAR=hello\n")
(should (equal (getenv "VAR") "value"))))
(ert-deftest esh-var-test/local-variables/skip-nil ()
"Test that Eshell skips leading nil arguments after local variable setting."
(with-temp-eshell
(push "VAR=value" process-environment)
(eshell-match-command-output "VAR=hello $eshell-test-value env"
"VAR=hello\n")
(should (equal (getenv "VAR") "value"))))
;; Variable aliases