From 3d93eba3ebe8396c0d349cad15f40e2c387f4e91 Mon Sep 17 00:00:00 2001 From: Jim Porter Date: Tue, 1 Mar 2022 18:36:08 -0800 Subject: [PATCH 4/5] Fix parsing of indices in Eshell expansions Previously, more-complex index expansions, like '$var[":" 0]' or '$var[$(expr) 0]' failed to parse correctly. * lisp/eshell/esh-var.el (Commentary): Clarify indexing and length expansions. (eshell-parse-indices): Expand docstring and support parsing inside double-quotes. (eshell-eval-indices): New function. (eshell-parse-variable): Use it. * test/lisp/eshell/esh-var-tests.el (eshell-test-value): New defvar. (esh-var-test/interp-var-indices, (esh-var-test/interp-var-split-indices) (esh-var-test/interp-var-string-split-indices) (esh-var-test/interp-var-regexp-split-indices) (esh-var-test/interp-var-assoc, esh-var-test/interp-var-length-list) (esh-var-test/interp-var-length-string) (esh-var-test/interp-var-length-alist) (esh-var-test/quoted-interp-var-indices) (esh-var-test/quoted-interp-var-split-indices) (esh-var-test/quoted-interp-var-string-split-indices) (esh-var-test/quoted-interp-var-regexp-split-indices) (esh-var-test/quoted-interp-var-assoc) (esh-var-test/quoted-interp-var-length-list) (esh-var-test/quoted-interp-var-length-string) (esh-var-test/quoted-interp-var-length-alist): New tests. * doc/misc/eshell.texi (Dollars Expansion): Expand and reword documentation for indexing and length expansions. --- doc/misc/eshell.texi | 62 ++++++------ lisp/eshell/esh-var.el | 59 ++++++------ test/lisp/eshell/esh-var-tests.el | 152 ++++++++++++++++++++++++++++++ 3 files changed, 212 insertions(+), 61 deletions(-) diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi index bbf8ca6b8b..3301a854eb 100644 --- a/doc/misc/eshell.texi +++ b/doc/misc/eshell.texi @@ -1022,11 +1022,6 @@ Dollars Expansion disambiguate the variable name when concatenating it with another value, such as @samp{$"@var{var}"-suffix}. -@item $#@var{var} -Expands to the length of the value bound to @var{var}. Raises an error -if the value is not a sequence -(@pxref{Sequences Arrays Vectors, Sequences, , elisp, The Emacs Lisp Reference Manual}). - @item $(@var{lisp}) Expands to the result of evaluating the S-expression @code{(@var{lisp})}. On its own, this is identical to just @code{(@var{lisp})}, but with the @code{$}, @@ -1041,34 +1036,35 @@ Dollars Expansion @command{@var{command}}, but writes the output to a temporary file and returns the file name. -@item $@var{var}[i] -Expands to the @code{i}th element of the value bound to @var{var}. If -the value is a string, it will be split at whitespace to make it a list. -Again, raises an error if the value is not a sequence. - -@item $@var{var}[: i] -As above, but now splitting occurs at the colon character. - -@item $@var{var}[: i j] -As above, but instead of returning just a string, it now returns a list -of two strings. If the result is being interpolated into a larger -string, this list will be flattened into one big string, with each -element separated by a space. - -@item $@var{var}["\\\\" i] -Separate on backslash characters. Actually, the first argument -- if it -doesn't have the form of a number, or a plain variable name -- can be -any regular expression. So to split on numbers, use -@samp{$@var{var}["[0-9]+" 10 20]}. - -@item $@var{var}[hello] -Calls @code{assoc} on @var{var} with @code{"hello"}, expecting it to be -an alist (@pxref{Association List Type, Association Lists, , elisp, -The Emacs Lisp Reference Manual}). - -@item $#@var{var}[hello] -Returns the length of the @code{cdr} of the element of @var{var} whose -car is equal to @code{"hello"}. +@item $@var{expr}[@var{i...}] +Expands to the @var{i}th element of the result of @var{expr}, an +expression in one of the above forms listed here. If multiple indices +are supplied, this will return a list containing the elements for each +index. If @var{expr}'s value is a string, it will first be split at +whitespace to make it a list. If @var{expr}'s value is an alist +(@pxref{Association List Type, Association Lists, , elisp, The Emacs +Lisp Reference Manual}), this will call @code{assoc} on the result of +@var{expr}, returning the @code{cdr} of the element of the result +whose car is equal to @code{"i"}. Raises an error if the value is not +a sequence (@pxref{Sequences Arrays Vectors, Sequences, , elisp, The +Emacs Lisp Reference Manual}). + +Multiple sets of indices can also be specified. For example, if +@var{var} is a list of lists, @samp{$@var{var}[0][0]} is equivalent to +@samp{(caar @var{var})}. + +@item $@var{expr}[@var{regexp} @var{i...}] +As above (when @var{expr} expands to a string), but use @var{regexp} +to split the string. @var{regexp} can be any form other than a number +or a plain variable name. For example, @samp{$@var{var}[: 0]} will +return the first element of a colon-delimited string. + +@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, raises an error if +the result of @var{expr} is not a sequence. @end table diff --git a/lisp/eshell/esh-var.el b/lisp/eshell/esh-var.el index 24fdbde3cf..6f08a3fbc4 100644 --- a/lisp/eshell/esh-var.el +++ b/lisp/eshell/esh-var.el @@ -39,11 +39,6 @@ ;; ;; Only "MYVAR" is part of the variable name in this case. ;; -;; $#VARIABLE -;; -;; Returns the length of the value of VARIABLE. This could also be -;; done using the `length' Lisp function. -;; ;; $(lisp) ;; ;; Returns result of Lisp evaluation. Note: Used alone like this, it @@ -61,38 +56,36 @@ ;; Evaluates an eshell subcommand, redirecting the output to a ;; temporary file, and returning the file name. ;; -;; $ANYVAR[10] +;; $EXPR[10] ;; -;; Return the 10th element of ANYVAR. If ANYVAR's value is a string, -;; it will be split in order to make it a list. The splitting will -;; occur at whitespace. +;; Return the 10th element of $EXPR, which can be any dollar +;; expression. If $EXPR's value is a string, it will be split in +;; order to make it a list. The splitting will occur at whitespace. ;; -;; $ANYVAR[: 10] +;; $EXPR[10 20] ;; -;; As above, except that splitting occurs at the colon now. +;; As above, but instead of returning a single element, it now returns a +;; list of two elements. ;; -;; $ANYVAR[: 10 20] +;; $EXPR[: 10] ;; -;; As above, but instead of returning just a string, it now returns a -;; list of two strings. If the result is being interpolated into a -;; larger string, this list will be flattened into one big string, -;; with each element separated by a space. +;; Like $EXPR[10], except that splitting occurs at the colon now. ;; -;; $ANYVAR["\\\\" 10] +;; $EXPR["\\\\" 10] ;; ;; Separate on backslash characters. Actually, the first argument -- ;; if it doesn't have the form of a number, or a plain variable name ;; -- can be any regular expression. So to split on numbers, use -;; '$ANYVAR["[0-9]+" 10 20]'. +;; '$EXPR["[0-9]+" 10 20]'. ;; -;; $ANYVAR[hello] +;; $EXPR[hello] ;; -;; Calls `assoc' on ANYVAR with 'hello', expecting it to be an alist. +;; Calls `assoc' on $EXPR with 'hello', expecting it to be an alist. ;; -;; $#ANYVAR[hello] +;; $#EXPR ;; -;; Returns the length of the cdr of the element of ANYVAR who car is -;; equal to "hello". +;; Returns the length of the value of $EXPR. This could also be +;; done using the `length' Lisp function. ;; ;; There are also a few special variables defined by Eshell. '$$' is ;; the value of the last command (t or nil, in the case of an external @@ -416,7 +409,7 @@ eshell-parse-variable (eshell-parse-indices)) ;; This is an expression that will be evaluated by `eshell-do-eval', ;; which only support let-binding of dynamically-scoped vars - value `(let ((indices ',indices)) ,value)) + value `(let ((indices (eshell-eval-indices ',indices))) ,value)) (if get-len `(length ,value) value))) @@ -504,19 +497,29 @@ eshell-parse-variable-ref (defvar eshell-glob-function) (defun eshell-parse-indices () - "Parse and return a list of list of indices." + "Parse and return a list of index-lists. + +For example, \"[0 1][2]\" becomes: + ((\"0\" \"1\") (\"2\")." (let (indices) (while (eq (char-after) ?\[) (let ((end (eshell-find-delimiter ?\[ ?\]))) (if (not end) (throw 'eshell-incomplete ?\[) (forward-char) - (let (eshell-glob-function) - (setq indices (cons (eshell-parse-arguments (point) end) - indices))) + (eshell-with-temp-command (or (eshell-parse-inner-double-quote end) + (cons (point) end)) + (let (eshell-glob-function (eshell-current-quoted nil)) + (setq indices (cons (eshell-parse-arguments + (point-min) (point-max)) + indices)))) (goto-char (1+ end))))) (nreverse indices))) +(defun eshell-eval-indices (indices) + "Evaluate INDICES, a list of index-lists generated by `eshell-parse-indices'." + (mapcar (lambda (i) (mapcar #'eval i)) indices)) + (defun eshell-get-variable (name &optional indices) "Get the value for the variable NAME." (let* ((alias (assoc name eshell-variable-aliases-list)) diff --git a/test/lisp/eshell/esh-var-tests.el b/test/lisp/eshell/esh-var-tests.el index 7ec6a97519..e679174939 100644 --- a/test/lisp/eshell/esh-var-tests.el +++ b/test/lisp/eshell/esh-var-tests.el @@ -32,6 +32,8 @@ (file-name-directory (or load-file-name default-directory)))) +(defvar eshell-test-value nil) + ;;; Tests: @@ -56,6 +58,76 @@ esh-var-test/interp-quoted-var-concat (should (equal (eshell-test-command-result "echo $\"user-login-name\"-foo") (concat user-login-name "-foo")))) +(ert-deftest esh-var-test/interp-var-indices () + "Interpolate list variable with indices" + (let ((eshell-test-value '("zero" "one" "two" "three" "four"))) + (should (equal (eshell-test-command-result "echo $eshell-test-value[0]") + "zero")) + (should (equal (eshell-test-command-result "echo $eshell-test-value[0 2]") + '("zero" "two"))) + (should (equal (eshell-test-command-result "echo $eshell-test-value[0 2 4]") + '("zero" "two" "four"))))) + +(ert-deftest esh-var-test/interp-var-split-indices () + "Interpolate string variable with indices" + (let ((eshell-test-value "zero one two three four")) + (should (equal (eshell-test-command-result "echo $eshell-test-value[0]") + "zero")) + (should (equal (eshell-test-command-result "echo $eshell-test-value[0 2]") + '("zero" "two"))) + (should (equal (eshell-test-command-result "echo $eshell-test-value[0 2 4]") + '("zero" "two" "four"))))) + +(ert-deftest esh-var-test/interp-var-string-split-indices () + "Interpolate string variable with string splitter and indices" + (let ((eshell-test-value "zero:one:two:three:four")) + (should (equal (eshell-test-command-result "echo $eshell-test-value[: 0]") + "zero")) + (should (equal (eshell-test-command-result "echo $eshell-test-value[: 0 2]") + '("zero" "two"))))) + +(ert-deftest esh-var-test/interp-var-regexp-split-indices () + "Interpolate string variable with regexp splitter and indices" + (let ((eshell-test-value "zero:one!two:three!four")) + (should (equal (eshell-test-command-result + "echo $eshell-test-value['[:!]' 0]") + "zero")) + (should (equal (eshell-test-command-result + "echo $eshell-test-value['[:!]' 0 2]") + '("zero" "two"))) + (should (equal (eshell-test-command-result + "echo $eshell-test-value[\"[:!]\" 0]") + "zero")) + (should (equal (eshell-test-command-result + "echo $eshell-test-value[\"[:!]\" 0 2]") + '("zero" "two"))))) + +(ert-deftest esh-var-test/interp-var-assoc () + "Interpolate alist variable with index" + (let ((eshell-test-value '(("foo" . 1)))) + (should (eq (eshell-test-command-result "echo $eshell-test-value[foo]") + 1)))) + +(ert-deftest esh-var-test/interp-var-length-list () + "Interpolate length of list variable" + (let ((eshell-test-value '((1 2) (3) (5 (6 7 8 9))))) + (should (eq (eshell-test-command-result "echo $#eshell-test-value") 3)) + (should (eq (eshell-test-command-result "echo $#eshell-test-value[1]") 1)) + (should (eq (eshell-test-command-result "echo $#eshell-test-value[2][1]") + 4)))) + +(ert-deftest esh-var-test/interp-var-length-string () + "Interpolate length of string variable" + (let ((eshell-test-value "foobar")) + (should (eq (eshell-test-command-result "echo $#eshell-test-value") 6)))) + +(ert-deftest esh-var-test/interp-var-length-alist () + "Interpolate length of alist variable" + (let ((eshell-test-value '(("foo" . (1 2 3))))) + (should (eq (eshell-test-command-result "echo $#eshell-test-value") 1)) + (should (eq (eshell-test-command-result "echo $#eshell-test-value[foo]") + 3)))) + (ert-deftest esh-var-test/interp-lisp () "Interpolate Lisp form evaluation" (should (equal (eshell-test-command-result "+ $(+ 1 2) 3") 6))) @@ -112,6 +184,86 @@ esh-var-test/quoted-interp-quoted-var "echo \"hi, $\\\"user-login-name\\\"\"") (concat "hi, " user-login-name)))) +(ert-deftest esh-var-test/quoted-interp-var-indices () + "Interpolate string variable with indices inside double-quotes" + (let ((eshell-test-value '("zero" "one" "two" "three" "four"))) + (should (equal (eshell-test-command-result + "echo \"$eshell-test-value[0]\"") + "zero")) + (should (equal (eshell-test-command-result + "echo \"$eshell-test-value[0 2]\"") + '("zero" "two"))) + (should (equal (eshell-test-command-result + "echo \"$eshell-test-value[0 2 4]\"") + '("zero" "two" "four"))))) + +(ert-deftest esh-var-test/quoted-interp-var-split-indices () + "Interpolate string variable with indices inside double-quotes" + (let ((eshell-test-value "zero one two three four")) + (should (equal (eshell-test-command-result + "echo \"$eshell-test-value[0]\"") + "zero")) + (should (equal (eshell-test-command-result + "echo \"$eshell-test-value[0 2]\"") + '("zero" "two"))))) + +(ert-deftest esh-var-test/quoted-interp-var-string-split-indices () + "Interpolate string variable with string splitter and indices +inside double-quotes" + (let ((eshell-test-value "zero:one:two:three:four")) + (should (equal (eshell-test-command-result + "echo \"$eshell-test-value[: 0]\"") + "zero")) + (should (equal (eshell-test-command-result + "echo \"$eshell-test-value[: 0 2]\"") + '("zero" "two"))))) + +(ert-deftest esh-var-test/quoted-interp-var-regexp-split-indices () + "Interpolate string variable with regexp splitter and indices" + (let ((eshell-test-value "zero:one!two:three!four")) + (should (equal (eshell-test-command-result + "echo \"$eshell-test-value['[:!]' 0]\"") + "zero")) + (should (equal (eshell-test-command-result + "echo \"$eshell-test-value['[:!]' 0 2]\"") + '("zero" "two"))) + (should (equal (eshell-test-command-result + "echo \"$eshell-test-value[\\\"[:!]\\\" 0]\"") + "zero")) + (should (equal (eshell-test-command-result + "echo \"$eshell-test-value[\\\"[:!]\\\" 0 2]\"") + '("zero" "two"))))) + +(ert-deftest esh-var-test/quoted-interp-var-assoc () + "Interpolate alist variable with index inside double-quotes" + (let ((eshell-test-value '(("foo" . 1)))) + (should (equal (eshell-test-command-result + "echo \"$eshell-test-value[foo]\"") + 1)))) + +(ert-deftest esh-var-test/quoted-interp-var-length-list () + "Interpolate length of list variable inside double-quotes" + (let ((eshell-test-value '((1 2) (3) (5 (6 7 8 9))))) + (should (eq (eshell-test-command-result "echo \"$#eshell-test-value\"") 3)) + (should (eq (eshell-test-command-result "echo \"$#eshell-test-value[1]\"") + 1)) + (should (eq (eshell-test-command-result + "echo \"$#eshell-test-value[2][1]\"") + 4)))) + +(ert-deftest esh-var-test/quoted-interp-var-length-string () + "Interpolate length of string variable inside double-quotes" + (let ((eshell-test-value "foobar")) + (should (eq (eshell-test-command-result "echo \"$#eshell-test-value\"") + 6)))) + +(ert-deftest esh-var-test/quoted-interp-var-length-alist () + "Interpolate length of alist variable inside double-quotes" + (let ((eshell-test-value '(("foo" . (1 2 3))))) + (should (eq (eshell-test-command-result "echo \"$#eshell-test-value\"") 1)) + (should (eq (eshell-test-command-result "echo \"$#eshell-test-value[foo]\"") + 3)))) + (ert-deftest esh-var-test/quoted-interp-lisp () "Interpolate Lisp form evaluation inside double-quotes" (should (equal (eshell-test-command-result -- 2.25.1