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.
This commit is contained in:
Jim Porter 2022-03-01 18:36:08 -08:00 committed by Lars Ingebrigtsen
parent cccee7e840
commit 990f36fa10
3 changed files with 209 additions and 58 deletions

View file

@ -1022,11 +1022,6 @@ Expands to the value bound to @var{var}. This is useful to
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 @@ As with @samp{$@{@var{command}@}}, evaluates the Eshell command invocation
@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{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}).
@item $@var{var}[: i]
As above, but now splitting occurs at the colon character.
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{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{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{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}
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

View file

@ -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 @@ process any indices that come after the variable reference."
(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 @@ Possible options are:
(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))

View file

@ -32,6 +32,8 @@
(file-name-directory (or load-file-name
default-directory))))
(defvar eshell-test-value nil)
;;; Tests:
@ -56,6 +58,76 @@
(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 @@
"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