From 55959a5874f65c2dfd9e3ac5549577daceaabe6e Mon Sep 17 00:00:00 2001 From: Jim Porter Date: Mon, 2 May 2022 16:56:49 -0700 Subject: [PATCH 3/3] 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). --- doc/misc/eshell.texi | 34 +++++++++++++-- etc/NEWS | 7 ++++ lisp/eshell/esh-arg.el | 62 ++++++++++++++++++++++++---- lisp/eshell/esh-util.el | 1 + test/lisp/eshell/em-extpipe-tests.el | 2 +- test/lisp/eshell/esh-var-tests.el | 19 ++++++++- 6 files changed, 109 insertions(+), 16 deletions(-) diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi index f13f1cacfc..960f5d9335 100644 --- a/doc/misc/eshell.texi +++ b/doc/misc/eshell.texi @@ -1017,11 +1017,37 @@ Expansion 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:: diff --git a/etc/NEWS b/etc/NEWS index 0c77b0bf58..360540b02a 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -1376,6 +1376,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}' diff --git a/lisp/eshell/esh-arg.el b/lisp/eshell/esh-arg.el index 395aa87ff0..459487f435 100644 --- a/lisp/eshell/esh-arg.el +++ b/lisp/eshell/esh-arg.el @@ -180,19 +180,63 @@ eshell-escape-arg (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 diff --git a/lisp/eshell/esh-util.el b/lisp/eshell/esh-util.el index 9960912bce..b5a423f023 100644 --- a/lisp/eshell/esh-util.el +++ b/lisp/eshell/esh-util.el @@ -293,6 +293,7 @@ eshell-split-path (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))) diff --git a/test/lisp/eshell/em-extpipe-tests.el b/test/lisp/eshell/em-extpipe-tests.el index 91c2fba479..3b84d763ac 100644 --- a/test/lisp/eshell/em-extpipe-tests.el +++ b/test/lisp/eshell/em-extpipe-tests.el @@ -170,7 +170,7 @@ em-extpipe-test-12 (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 *\"") "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 -- 2.25.1