Improve the behavior of concatenating parts of Eshell arguments

Previously, concatenating a list to a string would first convert the
list to a string.  Now, the string is concatenated with the last
element of the list.

* lisp/eshell/esh-util.el (eshell-to-flat-string): Make obsolete.

* lisp/eshell/esh-arg.el (eshell-concat, eshell-concat-1): New
functions.
(eshell-resolve-current-argument): Use 'eshell-concat'.

* test/lisp/eshell/esh-var-tests.el (esh-var-test/interp-concat-cmd):
Add check for concatenation of multiline output of subcommands.
(esh-var-test/quoted-interp-concat-cmd): New test.

* test/lisp/eshell/em-extpipe-tests.el (em-extpipe-test-13): Use
'eshell-concat'.

* doc/misc/eshell.texi (Expansion): Document this behavior.

* etc/NEWS: Announce the change (bug#55236).
This commit is contained in:
Jim Porter 2022-05-02 16:56:49 -07:00 committed by Lars Ingebrigtsen
parent 06423b5d1e
commit a3a7279a4a
6 changed files with 109 additions and 16 deletions

View file

@ -1017,11 +1017,37 @@ parsers (such as @command{cpp} and @command{m4}), but in a command
shell, they are less often used for constants, and usually for using
variables and string manipulation.@footnote{Eshell has no
string-manipulation expansions because the Elisp library already
provides many functions for this.} For example, @code{$var} on a line
expands to the value of the variable @code{var} when the line is
provides many functions for this.} For example, @code{$@var{var}} on
a line expands to the value of the variable @var{var} when the line is
executed. Expansions are usually passed as arguments, but may also be
used as commands.@footnote{E.g., entering just @samp{$var} at the prompt
is equivalent to entering the value of @code{var} at the prompt.}
used as commands.@footnote{E.g., entering just @samp{$@var{var}} at
the prompt is equivalent to entering the value of @var{var} at the
prompt.}
You can concatenate expansions with regular string arguments or even
other expansions. In the simplest case, when the expansion returns a
string value, this is equivalent to ordinary string concatenation; for
example, @samp{$@{echo "foo"@}bar} returns @samp{foobar}. The exact
behavior depends on the types of each value being concatenated:
@table @asis
@item both strings
Concatenate both values together.
@item one or both numbers
Concatenate the string representation of each value, converting back to
a number if possible.
@item one or both (non-@code{nil}) lists
Concatenate ``adjacent'' elements of each value (possibly converting
back to a number as above). For example, @samp{$list("a" "b")c}
returns @samp{("a" "bc")}.
@item anything else
Concatenate the string represenation of each value.
@end table
@menu
* Dollars Expansion::

View file

@ -1396,6 +1396,13 @@ If an Eshell expansion like '$FOO' is surrounded by double quotes, the
result will always be a single string, no matter the type that would
otherwise be returned.
+++
*** Concatenating Eshell expansions now works more similarly to other shells.
When concatenating an Eshell expansion that returns a list, "adjacent"
elements of each operand are now concatenated together,
e.g. '$list("a" "b")c' returns '("a" "bc")'. See the "(eshell)
Expansion" node in the Eshell manual for more details.
+++
*** Eshell subcommands with multiline numeric output return lists of numbers.
If every line of the output of an Eshell subcommand like '${COMMAND}'

View file

@ -180,19 +180,63 @@ treated as a literal character."
(add-text-properties 0 (length string) '(escaped t) string))
string)
(defun eshell-concat (quoted &rest rest)
"Concatenate all the arguments in REST and return the result.
If QUOTED is nil, the resulting value(s) may be converted to
numbers (see `eshell-concat-1').
If each argument in REST is a non-list value, the result will be
a single value, as if (mapconcat #'eshell-stringify REST) had been
called, possibly converted to a number.
If there is at least one (non-nil) list argument, the result will
be a list, with \"adjacent\" elements of consecutive arguments
concatenated as strings (again, possibly converted to numbers).
For example, concatenating \"a\", (\"b\"), and (\"c\" \"d\")
would produce (\"abc\" \"d\")."
(let (result)
(dolist (i rest result)
(when i
(cond
((null result)
(setq result i))
((listp result)
(let (curr-head curr-tail)
(if (listp i)
(setq curr-head (car i)
curr-tail (cdr i))
(setq curr-head i
curr-tail nil))
(setq result
(append
(butlast result 1)
(list (eshell-concat-1 quoted (car (last result))
curr-head))
curr-tail))))
((listp i)
(setq result
(cons (eshell-concat-1 quoted result (car i))
(cdr i))))
(t
(setq result (eshell-concat-1 quoted result i))))))))
(defun eshell-concat-1 (quoted first second)
"Concatenate FIRST and SECOND.
If QUOTED is nil and either FIRST or SECOND are numbers, try to
convert the result to a number as well."
(let ((result (concat (eshell-stringify first) (eshell-stringify second))))
(if (and (not quoted)
(or (numberp first) (numberp second)))
(eshell-convert-to-number result)
result)))
(defun eshell-resolve-current-argument ()
"If there are pending modifications to be made, make them now."
(when eshell-current-argument
(when eshell-arg-listified
(let ((parts eshell-current-argument))
(while parts
(unless (stringp (car parts))
(setcar parts
(list 'eshell-to-flat-string (car parts))))
(setq parts (cdr parts)))
(setq eshell-current-argument
(list 'eshell-convert
(append (list 'concat) eshell-current-argument))))
(setq eshell-current-argument
(append (list 'eshell-concat eshell-current-quoted)
eshell-current-argument))
(setq eshell-arg-listified nil))
(while eshell-current-modifiers
(setq eshell-current-argument

View file

@ -293,6 +293,7 @@ Prepend remote identification of `default-directory', if any."
(defun eshell-to-flat-string (value)
"Make value a string. If separated by newlines change them to spaces."
(declare (obsolete nil "29.1"))
(let ((text (eshell-stringify value)))
(if (string-match "\n+\\'" text)
(setq text (replace-match "" t t text)))

View file

@ -170,7 +170,7 @@
(em-extpipe-tests--deftest em-extpipe-test-13 "foo*|bar"
(should-parse '(eshell-execute-pipeline
'((eshell-named-command (concat "foo" "*"))
'((eshell-named-command (eshell-concat nil "foo" "*"))
(eshell-named-command "bar")))))
(em-extpipe-tests--deftest em-extpipe-test-14 "tac *<temp"

View file

@ -176,8 +176,17 @@
(should (equal (eshell-test-command-result "+ $(+ 1 2)$(+ 1 2) 3") 36)))
(ert-deftest esh-var-test/interp-concat-cmd ()
"Interpolate and concat command"
(should (equal (eshell-test-command-result "+ ${+ 1 2}3 3") 36)))
"Interpolate and concat command with literal"
(should (equal (eshell-test-command-result "+ ${+ 1 2}3 3") 36))
(should (equal (eshell-test-command-result "echo ${*echo \"foo\nbar\"}-baz")
'("foo" "bar-baz")))
;; Concatenating to a number in a list should produce a number...
(should (equal (eshell-test-command-result "echo ${*echo \"1\n2\"}3")
'(1 23)))
;; ... but concatenating to a string that looks like a number in a list
;; should produce a string.
(should (equal (eshell-test-command-result "echo ${*echo \"hi\n2\"}3")
'("hi" "23"))))
(ert-deftest esh-var-test/interp-concat-cmd2 ()
"Interpolate and concat two commands"
@ -326,6 +335,12 @@ inside double-quotes"
"Interpolate command result redirected to temp file inside double-quotes"
(should (equal (eshell-test-command-result "cat \"$<echo hi>\"") "hi")))
(ert-deftest esh-var-test/quoted-interp-concat-cmd ()
"Interpolate and concat command with literal"
(should (equal (eshell-test-command-result
"echo \"${echo \\\"foo\nbar\\\"} baz\"")
"foo\nbar baz")))
;; Interpolated variable conversion