]> git.eshelyaron.com Git - emacs.git/commitdiff
Move more of Eshell range handling to the parser phase
authorJim Porter <jporterbugs@gmail.com>
Mon, 21 Oct 2024 01:01:10 +0000 (18:01 -0700)
committerEshel Yaron <me@eshelyaron.com>
Fri, 8 Nov 2024 13:30:37 +0000 (14:30 +0100)
* lisp/eshell/esh-util.el (eshell-range): New struct.
(eshell--range-string-p, eshell--string-to-range): New functions.

* lisp/eshell/esh-arg.el (eshell-parse-integer)
(eshell-parse-range-token): New functions...
(eshell-parse-argument-hook): ... add them.
(eshell--after-range-token-regexp): New defsubst.
(eshell-concat-1): Don't remove the 'number' property; we use that when
handling range arguments.
(eshell--range-token): New constant.
(eshell-unmark-range-token): New function.

* lisp/eshell/esh-var.el (eshell-parse-index): Update implementation to
use parsed range argument.

* test/lisp/eshell/esh-var-tests.el (esh-var-test/interp-var-indices):
Test range index using variables.

(cherry picked from commit ed9ea57e57a915e743100591d7a71d44a4b4c0e9)

lisp/eshell/esh-arg.el
lisp/eshell/esh-util.el
lisp/eshell/esh-var.el
test/lisp/eshell/esh-var-tests.el

index 4f8119670d2a38dfebbb8a1611d2dda52afd529d..7e8d6444a7a4c0e8e6155670f22f3e364ede776c 100644 (file)
@@ -92,6 +92,11 @@ If POS is nil, the location of point is checked."
     eshell-parse-special-reference
     ;; Numbers convert to numbers if they stand alone.
     eshell-parse-number
+    ;; Integers convert to numbers if they stand alone or are part of a
+    ;; range expression.
+    eshell-parse-integer
+    ;; Range tokens go between integers and denote a half-open range.
+    eshell-parse-range-token
     ;; Parse any non-special characters, based on the current context.
     eshell-parse-non-special
     ;; Whitespace is an argument delimiter.
@@ -193,6 +198,15 @@ Eshell will expand special refs like \"#<ARG...>\" into
                   (rx-to-string
                    `(+ (not (any ,@eshell-special-chars-outside-quoting))) t))))
 
+(defvar eshell--after-range-token-regexp nil)
+(defsubst eshell--after-range-token-regexp ()
+  (or eshell--after-range-token-regexp
+      (setq-local eshell--after-range-token-regexp
+                  (rx-to-string
+                   `(or (any ,@eshell-special-chars-outside-quoting)
+                        (regexp ,eshell-integer-regexp))
+                   t))))
+
 (defsubst eshell-escape-arg (string)
   "Return STRING with the `escaped' property on it."
   (if (stringp string)
@@ -245,7 +259,6 @@ If QUOTED is nil and either FIRST or SECOND are numberlike, try to mark
 the result as a number as well."
   (let ((result (concat (eshell-stringify first quoted)
                         (eshell-stringify second quoted))))
-    (remove-text-properties 0 (length result) '(number) result)
     (when (and (not quoted)
                (or (numberp first)  (eshell--numeric-string-p first)
                    (numberp second) (eshell--numeric-string-p second)))
@@ -412,6 +425,8 @@ Point is left at the end of the arguments."
   "A stub function that generates an error if a floating splice is found."
   (error "Splice operator is not permitted in this context"))
 
+(defconst eshell--range-token (propertize ".." 'eshell-range t))
+
 (defun eshell-parse-number ()
   "Parse a numeric argument.
 Eshell can treat unquoted arguments matching `eshell-number-regexp' as
@@ -422,10 +437,50 @@ their numeric values."
              (eshell-arg-delimiter (match-end 0)))
     (goto-char (match-end 0))
     (let ((str (match-string 0)))
-      (when (> (length str) 0)
-        (add-text-properties 0 (length str) '(number t) str))
+      (add-text-properties 0 (length str) '(number t) str)
       str)))
 
+(defun eshell-parse-integer ()
+  "Parse an integer argument."
+  (unless eshell-current-quoted
+    (let ((prev-token (if eshell-arg-listified
+                          (car (last eshell-current-argument))
+                        eshell-current-argument)))
+      (when (and (memq prev-token `(nil ,eshell--range-token))
+                 (looking-at eshell-integer-regexp)
+                 (or (eshell-arg-delimiter (match-end 0))
+                     (save-excursion
+                       (goto-char (match-end 0))
+                       (looking-at-p (rx "..")))))
+        (goto-char (match-end 0))
+        (let ((str (match-string 0)))
+          (add-text-properties 0 (length str) '(number t) str)
+          str)))))
+
+(defun eshell-unmark-range-token (string)
+  (remove-text-properties 0 (length string) '(eshell-range) string))
+
+(defun eshell-parse-range-token ()
+  "Parse a range token.
+This separates two integers (possibly as dollar expansions) and denotes
+a half-open range."
+  (when (and (not eshell-current-quoted)
+             (looking-at (rx ".."))
+             (or (eshell-arg-delimiter (match-end 0))
+                 (save-excursion
+                   (goto-char (match-end 0))
+                   (looking-at (eshell--after-range-token-regexp)))))
+    ;; If we parse multiple range tokens for a single argument, then
+    ;; they can't actually be range tokens.  Unmark the result to
+    ;; indicate this.
+    (when (memq eshell--range-token
+                (if eshell-arg-listified
+                    eshell-current-argument
+                  (list eshell-current-argument)))
+      (add-hook 'eshell-current-modifiers #'eshell-unmark-range-token))
+    (forward-char 2)
+    eshell--range-token))
+
 (defun eshell-parse-non-special ()
   "Parse any non-special characters, depending on the current context."
   (when (looking-at (if eshell-current-quoted
index de3f86ccae4e3d1235ef661f56175468c7fe572f..57dd1353aabfade744a83adaae1cf5150ca1ebc2 100644 (file)
@@ -369,6 +369,35 @@ unchanged."
       (string-to-number string)
     string))
 
+(cl-defstruct (eshell-range
+               (:constructor nil)
+               (:constructor eshell-range-create (begin end)))
+  "A half-open range from BEGIN to END."
+  begin end)
+
+(defsubst eshell--range-string-p (string)
+  "Return non-nil if STRING has been marked as a range."
+  (and (stringp string)
+       (text-property-any 0 (length string) 'eshell-range t string)))
+
+(defun eshell--string-to-range (string)
+  "Convert STRING to an `eshell-range' object."
+  (let* ((startpos (text-property-any 0 (length string) 'eshell-range t string))
+         (endpos (next-single-property-change startpos 'eshell-range
+                                              string (length string)))
+         range-begin range-end)
+    (unless (= startpos 0)
+      (setq range-begin (substring string 0 startpos))
+      (unless (eshell--numeric-string-p range-begin)
+        (user-error "range begin `%s' is not a number" range-begin))
+      (setq range-begin (string-to-number range-begin)))
+    (unless (= endpos (length string))
+      (setq range-end (substring string endpos))
+      (unless (eshell--numeric-string-p range-end)
+        (user-error "range end `%s' is not a number" range-end))
+      (setq range-end (string-to-number range-end)))
+    (eshell-range-create range-begin range-end)))
+
 (defun eshell-convert (string &optional to-string)
   "Convert STRING into a more-native Lisp object.
 If TO-STRING is non-nil, always return a single string with
index bf474c5e279fdc3bd8c8724d834106265f32ec62..eaa73290a835ceb026219cb83e0ac22e1009192c 100644 (file)
@@ -641,24 +641,13 @@ 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))))
+  (cond
+   ((eshell--numeric-string-p index)
+    (string-to-number index))
+   ((eshell--range-string-p index)
+    (eshell--string-to-range index))
+   (t
+    index)))
 
 (defun eshell-eval-indices (indices)
   "Evaluate INDICES, a list of index-lists generated by `eshell-parse-indices'."
@@ -795,14 +784,6 @@ For example, to retrieve the second element of a user's record in
           (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."
   (let ((parsed-index (eshell-parse-index index)))
@@ -810,15 +791,17 @@ START and END."
         (pcase parsed-index
           ((pred integerp)
            (ring-ref value parsed-index))
-          ((eshell-index-range start end)
+          ((pred eshell-range-p)
            (let* ((len (ring-length value))
-                  (real-start (mod (or start 0) len))
+                  (begin (eshell-range-begin parsed-index))
+                  (end (eshell-range-end parsed-index))
+                  (real-begin (mod (or begin 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))))
+              (seq-subseq (ring-elements value) real-begin real-end))))
           (_
            (error "Invalid index for ring: %s" index)))
       (pcase parsed-index
@@ -826,8 +809,9 @@ START and END."
          (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))
+        ((pred eshell-range-p)
+         (seq-subseq value (or (eshell-range-begin parsed-index) 0)
+                     (eshell-range-end parsed-index)))
         (_
          ;; INDEX is some non-integer value, so treat VALUE as an alist.
          (cdr (assoc parsed-index value)))))))
index 38f90e615a89abdef80f75c66658ec74a648708e..2f8ac32b0b596c1d567fed75ae3e5bb5a81c0666 100644 (file)
@@ -35,6 +35,8 @@
                                                     default-directory))))
 
 (defvar eshell-test-value nil)
+(defvar eshell-test-begin nil)
+(defvar eshell-test-end nil)
 
 ;;; Tests:
 
@@ -111,7 +113,11 @@ nil, use FUNCTION instead."
     (eshell-command-result-equal
      "echo $eshell-test-value[1..4 -2..]"
      (list (funcall range-function '("one" "two" "three"))
-           (funcall range-function '("three" "four"))))))
+           (funcall range-function '("three" "four"))))
+    (let ((eshell-test-begin 1) (eshell-test-end 4))
+      (eshell-command-result-equal
+       "echo $eshell-test-value[$eshell-test-begin..$eshell-test-end]"
+       (funcall range-function '("one" "two" "three"))))))
 
 (ert-deftest esh-var-test/interp-var-indices/list ()
   "Interpolate list variable with indices."