* 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)
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.
(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)
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)))
"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
(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
(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
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'."
(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)))
(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
(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)))))))
default-directory))))
(defvar eshell-test-value nil)
+(defvar eshell-test-begin nil)
+(defvar eshell-test-end nil)
;;; Tests:
(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."