From e6da81c7fd8a667ba9231b6670dfac3684dd86f9 Mon Sep 17 00:00:00 2001 From: Jim Porter Date: Sun, 20 Oct 2024 18:01:10 -0700 Subject: [PATCH] Move more of Eshell range handling to the parser phase * 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 | 61 +++++++++++++++++++++++++++++-- lisp/eshell/esh-util.el | 29 +++++++++++++++ lisp/eshell/esh-var.el | 46 ++++++++--------------- test/lisp/eshell/esh-var-tests.el | 8 +++- 4 files changed, 109 insertions(+), 35 deletions(-) diff --git a/lisp/eshell/esh-arg.el b/lisp/eshell/esh-arg.el index 4f8119670d2..7e8d6444a7a 100644 --- a/lisp/eshell/esh-arg.el +++ b/lisp/eshell/esh-arg.el @@ -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 \"#\" 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 diff --git a/lisp/eshell/esh-util.el b/lisp/eshell/esh-util.el index de3f86ccae4..57dd1353aab 100644 --- a/lisp/eshell/esh-util.el +++ b/lisp/eshell/esh-util.el @@ -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 diff --git a/lisp/eshell/esh-var.el b/lisp/eshell/esh-var.el index bf474c5e279..eaa73290a83 100644 --- a/lisp/eshell/esh-var.el +++ b/lisp/eshell/esh-var.el @@ -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))))))) diff --git a/test/lisp/eshell/esh-var-tests.el b/test/lisp/eshell/esh-var-tests.el index 38f90e615a8..2f8ac32b0b5 100644 --- a/test/lisp/eshell/esh-var-tests.el +++ b/test/lisp/eshell/esh-var-tests.el @@ -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." -- 2.39.5