emacs-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

master dabe0b7d407 1/2: Add support for negative indices and index range


From: Jim Porter
Subject: master dabe0b7d407 1/2: Add support for negative indices and index ranges in Eshell
Date: Fri, 27 Jan 2023 21:14:36 -0500 (EST)

branch: master
commit dabe0b7d40778496ecb308f54999248ea286d89b
Author: Jim Porter <jporterbugs@gmail.com>
Commit: Jim Porter <jporterbugs@gmail.com>

    Add support for negative indices and index ranges in Eshell
    
    * lisp/eshell/esh-util.el (eshell-integer-regexp): New defvar.
    
    * lisp/eshell/esh-var.el (eshell-parse-indices): Expand docstring.
    (eshell-parse-index): New function.
    (eshell-apply-indices): Use 'eshell-parse-index' to determine whether
    to treat the first index as a regexp.  Simplify the implementation a
    bit.
    (eshell-index-range): New pcase macro...
    (eshell-index-value): ... use it, and restructure the implementation.
    
    * test/lisp/eshell/esh-var-tests.el (esh-var-test/interp-var-indices):
    New function...
    (esh-var-test/interp-var-indices/list)
    (esh-var-test/interp-var-indices/vector)
    (esh-var-test/interp-var-indices/ring)
    (esh-var-test/interp-var-indices/split): ... use it.
    (esh-var-test/interp-var-string-split-indices)
    (esh-var-test/interp-var-regexp-split-indices)
    (esh-var-test/interp-var-assoc): Expand tests to cover things that
    look like numbers or ranges, but aren't.
    
    * doc/misc/eshell.texi (Variables): Describe how to get all arguments
    of the last command.
    (Dollars Expansion): Explain negative indices and index ranges.
    (Bugs and ideas): Remove now-implemented ideas.
    
    * etc/NEWS: Announce this change.
---
 doc/misc/eshell.texi              |  28 +++++---
 etc/NEWS                          |   7 ++
 lisp/eshell/esh-util.el           |   3 +
 lisp/eshell/esh-var.el            | 136 +++++++++++++++++++++++++-------------
 test/lisp/eshell/esh-var-tests.el | 102 +++++++++++++++++++++-------
 5 files changed, 196 insertions(+), 80 deletions(-)

diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi
index 57a2020fdca..e51e2cf799b 100644
--- a/doc/misc/eshell.texi
+++ b/doc/misc/eshell.texi
@@ -1059,7 +1059,9 @@ remote connection.
 This refers to the last argument of the last command.  With a
 subscript, you can access any argument of the last command.  For
 example, @samp{$_[1]} refers to the second argument of the last
-command (excluding the command name itself).
+command (excluding the command name itself).  To get all arguments of
+the last command, you can use an index range like @samp{$_[..]}
+(@pxref{Dollars Expansion}).
 
 @vindex $$
 @item $$
@@ -1370,11 +1372,24 @@ index.  The exact behavior depends on the type of 
@var{expr}'s value:
 @item a sequence
 Expands to the element at the (zero-based) index @var{i} of the
 sequence (@pxref{Sequences Arrays Vectors, Sequences, , elisp, The
-Emacs Lisp Reference Manual}).
+Emacs Lisp Reference Manual}).  If @var{i} is negative, @var{i} counts
+from the end, so -1 refers to the last element of the sequence.
+
+If @var{i} is a range like @code{@var{start}..@var{end}}, this expands
+to a subsequence from the indices @var{start} to @var{end}, where
+@var{end} is excluded@footnote{This behavior is different from ranges
+in Bash (where both the start and end are included in the range), but
+matches the behavior of similar Emacs Lisp functions, like
+@code{substring} (@pxref{Creating Strings, , , elisp, The Emacs Lisp
+Reference Manual}).}.  @var{start} and/or @var{end} can also be
+omitted, which is equivalent to the start and/or end of the entire
+list.  For example, @samp{$@var{expr}[-2..]} expands to the last two
+values of @var{expr}.
 
 @item a string
 Split the string at whitespace, and then expand to the @var{i}th
-element of the resulting sequence.
+element of the resulting sequence.  As above, @var{i} can be a range
+like @code{@var{start}..@var{end}}.
 
 @item an alist
 If @var{i} is a non-numeric value, expand to the value associated with
@@ -2442,13 +2457,6 @@ current being used.
 
 This way, the user could change it to use rc syntax: @samp{>[2=1]}.
 
-@item Allow @samp{$_[-1]}, which would indicate the last element of the array
-
-@item Make @samp{$x[*]} equal to listing out the full contents of @samp{x}
-
-Return them as a list, so that @samp{$_[*]} is all the arguments of the
-last command.
-
 @item Copy ANSI code handling from @file{term.el} into @file{em-term.el}
 
 Make it possible for the user to send char-by-char to the underlying
diff --git a/etc/NEWS b/etc/NEWS
index 5b8ab06086c..e0175bacfdf 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -149,6 +149,13 @@ of arguments into a command, such as when defining 
aliases.  For more
 information, see the "(eshell) Dollars Expansion" node in the Eshell
 manual.
 
++++
+*** Eshell now supports negative numbers and ranges for indices.
+Now, you can retrieve the last element of a list with '$my-list[-1]'
+or get a sublist of elements 2 through 4 with '$my-list[2..5]'.  For
+more information, see the "(eshell) Dollars Expansion" node in the
+Eshell manual.
+
 ---
 *** Eshell now uses 'field' properties in its output.
 In particular, this means that pressing the '<home>' key moves the
diff --git a/lisp/eshell/esh-util.el b/lisp/eshell/esh-util.el
index 544a8a74039..8b522449762 100644
--- a/lisp/eshell/esh-util.el
+++ b/lisp/eshell/esh-util.el
@@ -111,6 +111,9 @@ function `string-to-number'."
 
 ;;; Internal Variables:
 
+(defvar eshell-integer-regexp (rx (? "-") (+ digit))
+  "Regular expression used to match integer arguments.")
+
 (defvar eshell-group-names nil
   "A cache to hold the names of groups.")
 
diff --git a/lisp/eshell/esh-var.el b/lisp/eshell/esh-var.el
index 83dd5cb50f5..60aab92b33e 100644
--- a/lisp/eshell/esh-var.el
+++ b/lisp/eshell/esh-var.el
@@ -587,6 +587,9 @@ Possible variable references are:
 
 (defun eshell-parse-indices ()
   "Parse and return a list of index-lists.
+This produces a series of Lisp forms to be processed by
+`eshell-prepare-indices' and ultimately evaluated by
+`eshell-do-eval'.
 
 For example, \"[0 1][2]\" becomes:
   ((\"0\" \"1\") (\"2\"))."
@@ -605,6 +608,36 @@ For example, \"[0 1][2]\" becomes:
          (goto-char (1+ end)))))
     (nreverse indices)))
 
+(defun eshell-parse-index (index)
+  "Parse a single INDEX in string form.
+If INDEX looks like a number, return that number.
+
+If INDEX looks like \"[BEGIN]..[END]\", where BEGIN and END look
+like integers, return a cons cell of BEGIN and END as numbers;
+BEGIN and/or END can be omitted here, in which case their value
+in the cons is nil.
+
+Otherwise (including if INDEX is not a string), return
+the original value of INDEX."
+  (save-match-data
+    (cond
+     ((and (stringp index) (get-text-property 0 'number index))
+      (string-to-number index))
+     ((and (stringp index)
+           (not (text-property-any 0 (length index) 'escaped t index))
+           (string-match (rx string-start
+                             (group-n 1 (? (regexp eshell-integer-regexp)))
+                             ".."
+                             (group-n 2 (? (regexp eshell-integer-regexp)))
+                             string-end)
+                         index))
+      (let ((begin (match-string 1 index))
+            (end (match-string 2 index)))
+        (cons (unless (string-empty-p begin) (string-to-number begin))
+              (unless (string-empty-p end) (string-to-number end)))))
+     (t
+      index))))
+
 (defun eshell-eval-indices (indices)
   "Evaluate INDICES, a list of index-lists generated by 
`eshell-parse-indices'."
   (declare (obsolete eshell-prepare-indices "30.1"))
@@ -716,56 +749,65 @@ For example, to retrieve the second element of a user's 
record in
 '/etc/passwd', the variable reference would look like:
 
   ${grep johnw /etc/passwd}[: 2]"
-  (while indices
-    (let ((refs (car indices)))
-      (when (stringp value)
-       (let (separator (index (caar indices)))
-          (when (and (stringp index)
-                     (not (get-text-property 0 'number index)))
-            (setq separator index
-                  refs (cdr refs)))
-         (setq value (split-string value separator))
-          (unless quoted
-            (setq value (mapcar #'eshell-convert-to-number value)))))
-      (cond
-       ((< (length refs) 0)
-       (error "Invalid array variable index: %s"
-              (eshell-stringify refs)))
-       ((= (length refs) 1)
-       (setq value (eshell-index-value value (car refs))))
-       (t
-       (let ((new-value (list t)))
-         (while refs
-           (nconc new-value
-                  (list (eshell-index-value value
-                                            (car refs))))
-           (setq refs (cdr refs)))
-         (setq value (cdr new-value))))))
-    (setq indices (cdr indices)))
-  value)
+  (dolist (refs indices value)
+    ;; For string values, check if the first index looks like a
+    ;; regexp, and if so, use that to split the string.
+    (when (stringp value)
+      (let (separator (first (car refs)))
+        (when (stringp (eshell-parse-index first))
+          (setq separator first
+                refs (cdr refs)))
+        (setq value (split-string value separator))
+        (unless quoted
+          (setq value (mapcar #'eshell-convert-to-number value)))))
+    (cond
+     ((< (length refs) 0)
+      (error "Invalid array variable index: %s"
+             (eshell-stringify refs)))
+     ((= (length refs) 1)
+      (setq value (eshell-index-value value (car refs))))
+     (t
+      (let (new-value)
+        (dolist (ref refs)
+          (push (eshell-index-value value ref) new-value))
+        (setq value (nreverse new-value)))))))
+
+(pcase-defmacro eshell-index-range (start end)
+  "A pattern that matches an Eshell index range.
+EXPVAL should be a cons cell, with each slot containing either an
+integer or nil.  If this matches, bind the values of the sltos to
+START and END."
+  (list '\` (cons (list '\, `(and (or (pred integerp) (pred null)) ,start))
+                  (list '\, `(and (or (pred integerp) (pred null)) ,end)))))
 
 (defun eshell-index-value (value index)
   "Reference VALUE using the given INDEX."
-  (when (and (stringp index) (get-text-property 0 'number index))
-    (setq index (string-to-number index)))
-  (if (integerp index)
-      (cond
-       ((ring-p value)
-        (if (> index (ring-length value))
-            (error "Index exceeds length of ring")
-          (ring-ref value index)))
-       ((listp value)
-        (if (> index (length value))
-            (error "Index exceeds length of list")
-          (nth index value)))
-       ((vectorp value)
-        (if (> index (length value))
-            (error "Index exceeds length of vector")
-          (aref value index)))
-       (t
-        (error "Invalid data type for indexing")))
-    ;; INDEX is some non-integer value, so treat VALUE as an alist.
-    (cdr (assoc index value))))
+  (let ((parsed-index (eshell-parse-index index)))
+    (if (ring-p value)
+        (pcase parsed-index
+          ((pred integerp)
+           (ring-ref value parsed-index))
+          ((eshell-index-range start end)
+           (let* ((len (ring-length value))
+                  (real-start (mod (or start 0) len))
+                  (real-end (mod (or end len) len)))
+             (when (and (eq real-end 0)
+                        (not (eq end 0)))
+               (setq real-end len))
+             (ring-convert-sequence-to-ring
+              (seq-subseq (ring-elements value) real-start real-end))))
+          (_
+           (error "Invalid index for ring: %s" index)))
+      (pcase parsed-index
+        ((pred integerp)
+         (when (< parsed-index 0)
+           (setq parsed-index (+ parsed-index (length value))))
+         (seq-elt value parsed-index))
+        ((eshell-index-range start end)
+         (seq-subseq value (or start 0) end))
+        (_
+         ;; INDEX is some non-integer value, so treat VALUE as an alist.
+         (cdr (assoc parsed-index value)))))))
 
 ;;;_* Variable name completion
 
diff --git a/test/lisp/eshell/esh-var-tests.el 
b/test/lisp/eshell/esh-var-tests.el
index 12412d13640..6767d9289f9 100644
--- a/test/lisp/eshell/esh-var-tests.el
+++ b/test/lisp/eshell/esh-var-tests.el
@@ -72,52 +72,89 @@
     (eshell-command-result-equal "echo a$'eshell-test-value'z"
                                  '("a1" 2 "3z"))))
 
-(ert-deftest esh-var-test/interp-var-indices ()
-  "Interpolate list variable with indices"
-  (let ((eshell-test-value '("zero" "one" "two" "three" "four")))
+(defun esh-var-test/interp-var-indices (function &optional range-function)
+  "Test interpolation of an indexable value with indices.
+FUNCTION is a function that takes a list of elements and returns
+the object to test.
+
+RANGE-FUNCTION is a function that takes a list of elements and
+returns the expected result of an index range for the object; if
+nil, use FUNCTION instead."
+  (let ((eshell-test-value
+         (funcall function '("zero" "one" "two" "three" "four")))
+        (range-function (or range-function function)))
+    ;; Positive indices
     (eshell-command-result-equal "echo $eshell-test-value[0]"
                                  "zero")
     (eshell-command-result-equal "echo $eshell-test-value[0 2]"
                                  '("zero" "two"))
     (eshell-command-result-equal "echo $eshell-test-value[0 2 4]"
-                                 '("zero" "two" "four"))))
-
-(ert-deftest esh-var-test/interp-var-indices-subcommand ()
-  "Interpolate list variable with subcommand expansion for indices."
-  (skip-unless (executable-find "echo"))
-  (let ((eshell-test-value '("zero" "one" "two" "three" "four")))
+                                 '("zero" "two" "four"))
+    ;; Negative indices
+    (eshell-command-result-equal "echo $eshell-test-value[-1]"
+                                 "four")
+    (eshell-command-result-equal "echo $eshell-test-value[-1 -3]"
+                                 '("four" "two"))
+    ;; Index ranges
     (eshell-command-result-equal
-     "echo $eshell-test-value[${*echo 0}]"
-     "zero")
+     "echo $eshell-test-value[1..4]"
+     (funcall range-function '("one" "two" "three")))
     (eshell-command-result-equal
-     "echo $eshell-test-value[${*echo 0} ${*echo 2}]"
-     '("zero" "two"))))
+     "echo $eshell-test-value[..2]"
+     (funcall range-function '("zero" "one")))
+    (eshell-command-result-equal
+     "echo $eshell-test-value[-2..]"
+     (funcall range-function '("three" "four")))
+    (eshell-command-result-equal
+     "echo $eshell-test-value[..]"
+     (funcall range-function '("zero" "one" "two" "three" "four")))
+    (eshell-command-result-equal
+     "echo $eshell-test-value[1..4 -2..]"
+     (list (funcall range-function '("one" "two" "three"))
+           (funcall range-function '("three" "four"))))))
+
+(ert-deftest esh-var-test/interp-var-indices/list ()
+  "Interpolate list variable with indices."
+  (esh-var-test/interp-var-indices #'identity))
+
+(ert-deftest esh-var-test/interp-var-indices/vector ()
+  "Interpolate vector variable with indices."
+  (esh-var-test/interp-var-indices #'vconcat))
 
-(ert-deftest esh-var-test/interp-var-split-indices ()
+(ert-deftest esh-var-test/interp-var-indices/ring ()
+  "Interpolate ring variable with indices."
+  (esh-var-test/interp-var-indices #'ring-convert-sequence-to-ring))
+
+(ert-deftest esh-var-test/interp-var-indices/split ()
   "Interpolate string variable with indices."
-  (let ((eshell-test-value "zero one two three four"))
-    (eshell-command-result-equal "echo $eshell-test-value[0]"
-                                 "zero")
-    (eshell-command-result-equal "echo $eshell-test-value[0 2]"
-                                 '("zero" "two"))
-    (eshell-command-result-equal "echo $eshell-test-value[0 2 4]"
-                                 '("zero" "two" "four"))))
+  (esh-var-test/interp-var-indices
+   (lambda (values) (string-join values " "))
+   #'identity))
 
 (ert-deftest esh-var-test/interp-var-string-split-indices ()
   "Interpolate string variable with string splitter and indices."
+  ;; Test using punctuation as a delimiter.
   (let ((eshell-test-value "zero:one:two:three:four"))
     (eshell-command-result-equal "echo $eshell-test-value[: 0]"
                                  "zero")
     (eshell-command-result-equal "echo $eshell-test-value[: 0 2]"
                                  '("zero" "two")))
+  ;; Test using a letter as a delimiter.
   (let ((eshell-test-value "zeroXoneXtwoXthreeXfour"))
     (eshell-command-result-equal "echo $eshell-test-value[X 0]"
                                  "zero")
     (eshell-command-result-equal "echo $eshell-test-value[X 0 2]"
+                                 '("zero" "two")))
+  ;; Test using a number as a delimiter.
+  (let ((eshell-test-value "zero0one0two0three0four"))
+    (eshell-command-result-equal "echo $eshell-test-value[\"0\" 0]"
+                                 "zero")
+    (eshell-command-result-equal "echo $eshell-test-value[\"0\" 0 2]"
                                  '("zero" "two"))))
 
 (ert-deftest esh-var-test/interp-var-regexp-split-indices ()
   "Interpolate string variable with regexp splitter and indices."
+  ;; Test using a regexp as a delimiter.
   (let ((eshell-test-value "zero:one!two:three!four"))
     (eshell-command-result-equal "echo $eshell-test-value['[:!]' 0]"
                                  "zero")
@@ -126,15 +163,34 @@
     (eshell-command-result-equal "echo $eshell-test-value[\"[:!]\" 0]"
                                  "zero")
     (eshell-command-result-equal "echo $eshell-test-value[\"[:!]\" 0 2]"
+                                 '("zero" "two")))
+  ;; Test using a regexp that looks like range syntax as a delimiter.
+  (let ((eshell-test-value "zero0..0one0..0two0..0three0..0four"))
+    (eshell-command-result-equal "echo $eshell-test-value[\"0..0\" 0]"
+                                 "zero")
+    (eshell-command-result-equal "echo $eshell-test-value[\"0..0\" 0 2]"
                                  '("zero" "two"))))
 
 (ert-deftest esh-var-test/interp-var-assoc ()
   "Interpolate alist variable with index."
-  (let ((eshell-test-value '(("foo" . 1) (bar . 2))))
+  (let ((eshell-test-value '(("foo" . 1) (bar . 2) ("3" . "three"))))
     (eshell-command-result-equal "echo $eshell-test-value[foo]"
                                  1)
     (eshell-command-result-equal "echo $eshell-test-value[#'bar]"
-                                 2)))
+                                 2)
+    (eshell-command-result-equal "echo $eshell-test-value[\"3\"]"
+                                 "three")))
+
+(ert-deftest esh-var-test/interp-var-indices-subcommand ()
+  "Interpolate list variable with subcommand expansion for indices."
+  (skip-unless (executable-find "echo"))
+  (let ((eshell-test-value '("zero" "one" "two" "three" "four")))
+    (eshell-command-result-equal
+     "echo $eshell-test-value[${*echo 0}]"
+     "zero")
+    (eshell-command-result-equal
+     "echo $eshell-test-value[${*echo 0} ${*echo 2}]"
+     '("zero" "two"))))
 
 (ert-deftest esh-var-test/interp-var-length-list ()
   "Interpolate length of list variable."



reply via email to

[Prev in Thread] Current Thread [Next in Thread]