From 597b41576907d5b6233e02cff166a83644b9f9a5 Mon Sep 17 00:00:00 2001 From: Jim Porter Date: Tue, 8 Nov 2022 22:49:23 -0800 Subject: [PATCH 2/2] Add support for the "splice operator" in Eshell This allows splicing lists in-place in argument lists, which is particularly important when defining aliases using the '$*' special variable. * lisp/eshell/esh-var.el (eshell-parse-variable): Add support for the splice operator. (eshell-interpolate-variable): Let 'eshell-parse-variable' handle adding 'eshell-escape-arg'. (eshell-complete-variable-reference): Handle the splice operator. * lisp/eshell/esh-cmd.el (eshell-rewrite-named-command): Handle 'eshell-splice-args'. * lisp/eshell/esh-arg.el (eshell-splice-args): New function. * lisp/eshell/em-cmpl.el (eshell-complete-parse-arguments): Remove 'eshell-splice-args' sigils in Eshell command forms so that we can perform completion on splice-expansions. * lisp/eshell/em-unix.el (eshell-complete-host-reference): Don't try to complete arguments containing "$@". * test/lisp/eshell/esh-var-tets.el (esh-var-test/interp-var-splice) (esh-var-test/interp-var-splice-concat): New tests. * doc/misc/eshell.texi (Aliases): Expand documentation and use '$@*'. (Dollars Expansion): Explain the splice operator. * etc/NEWS: Announce this change. --- doc/misc/eshell.texi | 62 ++++++++++++++++++++++--------- etc/NEWS | 10 +++++ lisp/eshell/em-cmpl.el | 26 ++++++++----- lisp/eshell/em-unix.el | 12 ++++-- lisp/eshell/esh-arg.el | 5 +++ lisp/eshell/esh-cmd.el | 31 ++++++++++++---- lisp/eshell/esh-var.el | 12 ++++-- test/lisp/eshell/esh-var-tests.el | 30 +++++++++++++++ 8 files changed, 144 insertions(+), 44 deletions(-) diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi index 1383b412ce7..fa1d16f7913 100644 --- a/doc/misc/eshell.texi +++ b/doc/misc/eshell.texi @@ -1028,23 +1028,25 @@ Aliases @section Aliases @vindex $* -@findex eshell-expand-history-references +@findex eshell-read-aliases-list Aliases are commands that expand to a longer input line. For example, -@command{ll} is a common alias for @code{ls -l}, and would be defined -with the command invocation @kbd{alias ll 'ls -l $*'}; with this defined, -running @samp{ll foo} in Eshell will actually run @samp{ls -l foo}. -Aliases defined (or deleted) by the @command{alias} command are -automatically written to the file named by @code{eshell-aliases-file}, -which you can also edit directly. After doing so, use @w{@kbd{M-x -eshell-read-aliases-list}} to load the edited aliases. +@command{ll} is a common alias for @code{ls -l}. To define this alias +in Eshell, you can use the command invocation @kbd{alias ll 'ls -l +$@@*'}; with this defined, running @samp{ll foo} in Eshell will +actually run @samp{ls -l foo}. Aliases defined (or deleted) by the +@command{alias} command are automatically written to the file named by +@code{eshell-aliases-file}, which you can also edit directly. After +doing so, use @w{@kbd{M-x eshell-read-aliases-list}} to load the +edited aliases. @vindex $1, $2, @dots{} Note that unlike aliases in Bash, arguments must be handled -explicitly. Typically the alias definition would end in @samp{$*} to -pass all arguments along. More selective use of arguments via -@samp{$1}, @samp{$2}, etc., is also possible. For example, -@kbd{alias mcd 'mkdir $1 && cd $1'} would cause @kbd{mcd foo} to -create and switch to a directory called @samp{foo}. +explicitly. In most cases, you should end the alias definition with +@samp{$@@*} to pass all arguments along, splicing them into your +argument list (@pxref{Dollars Expansion}). More selective use of +arguments via @samp{$1}, @samp{$2}, etc., is also possible. For +example, @kbd{alias mcd 'mkdir $1 && cd $1'} would cause @kbd{mcd foo} +to create and switch to a directory called @samp{foo}. @node History @section History @@ -1307,12 +1309,36 @@ Dollars Expansion number. For example, @samp{$@var{var}[: 0]} will return the first element of a colon-delimited string. +@cindex length operator, in variable expansion @item $#@var{expr} -Expands to the length of the result of @var{expr}, an expression in -one of the above forms. For example, @samp{$#@var{var}} returns the -length of the variable @var{var} and @samp{$#@var{var}[0]} returns the -length of the first element of @var{var}. Again, signals an error if -the result of @var{expr} is not a string or a sequence. +This is the @dfn{length operator}. It expands to the length of the +result of @var{expr}, an expression in one of the above forms. For +example, @samp{$#@var{var}} returns the length of the variable +@var{var} and @samp{$#@var{var}[0]} returns the length of the first +element of @var{var}. Again, signals an error if the result of +@var{expr} is not a string or a sequence. + +@cindex splice operator, in variable expansion +@item $@@@var{expr} +This is the @dfn{splice operator}. It ``splices'' the elements of +@var{expr} (an expression of one of the above forms) into the +resulting list of arguments, much like the @samp{,@@} marker in Emacs +Lisp (@pxref{Backquote, , , elisp, The Emacs Lisp Reference Manual}). +The elements of @var{expr} become arguments at the same level as the +other arguments around it. For example, if @var{numbers} is the list +@code{(1 2 3)}, then: + +@example +@group +~ $ echo 0 $numbers +(0 + (1 2 3)) +@end group +@group +~ $ echo 0 $@@numbers +(0 1 2 3) +@end group +@end example @end table diff --git a/etc/NEWS b/etc/NEWS index 3338c06f037..ab1dcc59922 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -71,6 +71,16 @@ switches for shortlogs, such as the one produced by 'C-x v L'. You can now configure how to display the "*buffer-selection*" buffer using this new option. (Or set 'display-buffer-alist' directly.) +** Eshell + ++++ +*** New splice operator for Eshell dollar expansions. +Dollar expansions in Eshell now let you splice the elements of the +expansion in-place using '$@expr'. This makes it easier to fill lists +of arguments into a command, such as when defining alises. For more +information, see the "(eshell) Dollars Expansion" node in the Eshell +manual. + +++ *** 'eshell-read-aliases-list' is now an interactive command. After manually editing 'eshell-aliases-file', you can use diff --git a/lisp/eshell/em-cmpl.el b/lisp/eshell/em-cmpl.el index ac82e3f225c..2c721eb9e31 100644 --- a/lisp/eshell/em-cmpl.el +++ b/lisp/eshell/em-cmpl.el @@ -342,17 +342,23 @@ eshell-complete-parse-arguments (setq pos (1+ pos)))) (setq posns (cdr posns)) (cl-assert (= (length args) (length posns))) - (let ((a args) - (i 0) - l) + (let ((a args) (i 0) new-start) (while a - (if (and (consp (car a)) - (eq (caar a) 'eshell-operator)) - (setq l i)) - (setq a (cdr a) i (1+ i))) - (and l - (setq args (nthcdr (1+ l) args) - posns (nthcdr (1+ l) posns)))) + ;; Remove any top-level `eshell-splice-args' sigils. These + ;; are meant to be rewritten and can't actually be called. + (when (and (consp (car a)) + (eq (caar a) 'eshell-splice-args)) + (setcar a (cadar a))) + ;; If there's an unreplaced `eshell-operator' sigil, consider + ;; the token after it the new start of our arguments. + (when (and (consp (car a)) + (eq (caar a) 'eshell-operator)) + (setq new-start i)) + (setq a (cdr a) + i (1+ i))) + (when new-start + (setq args (nthcdr (1+ new-start) args) + posns (nthcdr (1+ new-start) posns)))) (cl-assert (= (length args) (length posns))) (when (and args (eq (char-syntax (char-before end)) ? ) (not (eq (char-before (1- end)) ?\\))) diff --git a/lisp/eshell/em-unix.el b/lisp/eshell/em-unix.el index 4b5e4dd53ed..3f7ec618a33 100644 --- a/lisp/eshell/em-unix.el +++ b/lisp/eshell/em-unix.el @@ -786,10 +786,14 @@ 'eshell-complete-hostname (defun eshell-complete-host-reference () "If there is a host reference, complete it." - (let ((arg (pcomplete-actual-arg)) - index) - (when (setq index (string-match "@[a-z.]*\\'" arg)) - (setq pcomplete-stub (substring arg (1+ index)) + (let ((arg (pcomplete-actual-arg))) + (when (string-match + (rx ;; Match an "@", but not immediately following a "$". + (or string-start (not "$")) "@" + (group (* (any "a-z."))) + string-end) + arg) + (setq pcomplete-stub (substring arg (match-beginning 1)) pcomplete-last-completion-raw t) (throw 'pcomplete-completions (pcomplete-read-host-names))))) diff --git a/lisp/eshell/esh-arg.el b/lisp/eshell/esh-arg.el index cfec04e183d..18dbaf4b14f 100644 --- a/lisp/eshell/esh-arg.el +++ b/lisp/eshell/esh-arg.el @@ -242,6 +242,7 @@ eshell-resolve-current-argument "If there are pending modifications to be made, make them now." (when eshell-current-argument (when eshell-arg-listified + (message "arg is %S" eshell-current-argument) (setq eshell-current-argument (append (list 'eshell-concat eshell-current-quoted) eshell-current-argument)) @@ -348,6 +349,10 @@ eshell-operator "A stub function that generates an error if a floating operator is found." (error "Unhandled operator in input text")) +(defsubst eshell-splice-args (&rest _args) + "A stub function that generates an error if a floating splice is found." + (error "Splice operator is not permitted in this context")) + (defsubst eshell-looking-at-backslash-return (pos) "Test whether a backslash-return sequence occurs at POS." (and (eq (char-after pos) ?\\) diff --git a/lisp/eshell/esh-cmd.el b/lisp/eshell/esh-cmd.el index 4a41bbe8fa1..a06d077ae5d 100644 --- a/lisp/eshell/esh-cmd.el +++ b/lisp/eshell/esh-cmd.el @@ -477,14 +477,29 @@ eshell-rewrite-initial-subcommand (defun eshell-rewrite-named-command (terms) "If no other rewriting rule transforms TERMS, assume a named command." - (let ((sym (if eshell-in-pipeline-p - 'eshell-named-command* - 'eshell-named-command)) - (cmd (car terms)) - (args (cdr terms))) - (if args - (list sym cmd `(list ,@(cdr terms))) - (list sym cmd)))) + (let* ((sym (if eshell-in-pipeline-p + 'eshell-named-command* + 'eshell-named-command)) + (splice-p nil) + ;; Group each term like ((list term-1) (list term-2) ...), + ;; splicing in `eshell-splice-args' terms. This lets us + ;; apply spliced terms correctly below. + (grouped-terms (mapcar (lambda (i) + (if (eq (car-safe i) 'eshell-splice-args) + (progn + (setq splice-p t) + (cadr i)) + `(list ,i))) + terms))) + (cond + (splice-p + `(let ((terms (nconc ,@grouped-terms))) + (,sym (car terms) (cdr terms)))) + ;; If no terms are spliced, use a simpler command form. + ((cdr terms) + (list sym (car terms) `(list ,@(cdr terms)))) + (t + (list sym (car terms)))))) (defvar eshell-command-body) (defvar eshell-test-body) diff --git a/lisp/eshell/esh-var.el b/lisp/eshell/esh-var.el index 5824da6dc0e..ffc6a5c9145 100644 --- a/lisp/eshell/esh-var.el +++ b/lisp/eshell/esh-var.el @@ -320,10 +320,9 @@ eshell-interpolate-variable "Parse a variable interpolation. This function is explicit for adding to `eshell-parse-argument-hook'." (when (and (eq (char-after) ?$) - (/= (1+ (point)) (point-max))) + (/= (1+ (point)) (point-max))) (forward-char) - (list 'eshell-escape-arg - (eshell-parse-variable)))) + (eshell-parse-variable))) (defun eshell/define (var-alias definition) "Define a VAR-ALIAS using DEFINITION." @@ -453,6 +452,8 @@ eshell-parse-variable process any indices that come after the variable reference." (let* ((get-len (when (eq (char-after) ?#) (forward-char) t)) + (splice (when (eq (char-after) ?@) + (forward-char) t)) value indices) (setq value (eshell-parse-variable-ref get-len) indices (and (not (eobp)) @@ -465,6 +466,9 @@ eshell-parse-variable (setq value `(length ,value))) (when eshell-current-quoted (setq value `(eshell-stringify ,value))) + (setq value `(eshell-escape-arg ,value)) + (when splice + (setq value `(eshell-splice-args ,value))) value)) (defun eshell-parse-variable-ref (&optional modifier-p) @@ -753,7 +757,7 @@ eshell-complete-variable-reference "If there is a variable reference, complete it." (let ((arg (pcomplete-actual-arg))) (when (string-match - (rx "$" (? "#") + (rx "$" (? (or "#" "@")) (? (group (regexp eshell-variable-name-regexp))) string-end) arg) diff --git a/test/lisp/eshell/esh-var-tests.el b/test/lisp/eshell/esh-var-tests.el index 96fde026a54..fead90bef4d 100644 --- a/test/lisp/eshell/esh-var-tests.el +++ b/test/lisp/eshell/esh-var-tests.el @@ -131,6 +131,18 @@ esh-var-test/interp-var-length-alist (eshell-command-result-equal "echo $#eshell-test-value" 1) (eshell-command-result-equal "echo $#eshell-test-value[foo]" 3))) +(ert-deftest esh-var-test/interp-var-splice () + "Splice-interpolate list variable" + (let ((eshell-test-value '(1 2 3))) + (eshell-command-result-equal "echo a $@eshell-test-value z" + '("a" 1 2 3 "z")))) + +(ert-deftest esh-var-test/interp-var-splice-concat () + "Splice-interpolate list variable" + (let ((eshell-test-value '(1 2 3))) + (eshell-command-result-equal "echo a$'eshell-test-value'z" + '("a1" 2 "3z")))) + (ert-deftest esh-var-test/interp-lisp () "Interpolate Lisp form evaluation" (eshell-command-result-equal "+ $(+ 1 2) 3" 6)) @@ -197,6 +209,9 @@ esh-var-test/interp-concat-cmd-external (eshell-match-command-output "echo ${echo hi}-${*echo there}" "hi-there\n"))) + +;; Quoted variable interpolation + (ert-deftest esh-var-test/quoted-interp-var () "Interpolate variable inside double-quotes" (eshell-command-result-equal "echo \"$user-login-name\"" @@ -324,6 +339,21 @@ esh-var-test/quoted-interp-concat-cmd (eshell-command-result-equal "echo \"${echo \\\"foo\nbar\\\"} baz\"" "foo\nbar baz")) + +;; Interpolating commands + +(ert-deftest esh-var-test/command-interp () + "Interpolate a variable as a command name" + (let ((eshell-test-value "printnl")) + (eshell-command-result-equal "$eshell-test-value hello there" + "hello\nthere\n"))) + +(ert-deftest esh-var-test/command-interp-splice () + "Interpolate a splice variable as a command name with arguments" + (let ((eshell-test-value '("printnl" "hello" "there"))) + (eshell-command-result-equal "$@eshell-test-value" + "hello\nthere\n"))) + ;; Interpolated variable conversion -- 2.25.1