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.
This commit is contained in:
Jim Porter 2024-10-20 18:01:10 -07:00
parent 4d69d3778a
commit ed9ea57e57
4 changed files with 109 additions and 35 deletions

View 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

View 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

View 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)))))))

View 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."