"Don't glob the command argument. Reflect this by modifying TERMS."
(ignore
(pcase (car terms)
- ((or `(eshell-extended-glob ,term)
- `(eshell-splice-args (eshell-extended-glob ,term)))
+ ((or `(eshell-expand-glob ,term)
+ `(eshell-splice-args (eshell-expand-glob ,term)))
(setcar terms term)))))
(defun eshell-add-glob-modifier ()
- "Add `eshell-extended-glob' to the argument modifier list."
+ "Add `eshell-expand-glob' to the argument modifier list."
(when eshell-glob-splice-results
(add-hook 'eshell-current-modifiers #'eshell-splice-args 99))
- (add-hook 'eshell-current-modifiers #'eshell-extended-glob))
+ (add-hook 'eshell-current-modifiers #'eshell-expand-glob))
(defun eshell-parse-glob-chars ()
- "Parse a globbing delimiter.
-The character is not advanced for ordinary globbing characters, so
-that other function may have a chance to override the globbing
-interpretation."
+ "Parse a globbing character."
(when (memq (char-after) eshell-glob-chars-list)
- (ignore (eshell-add-glob-modifier))))
+ (eshell-add-glob-modifier)
+ (prog1
+ (propertize (char-to-string (char-after)) 'eshell-glob-char t)
+ (forward-char))))
+
+(defvar eshell-glob-chars-regexp nil)
+(defsubst eshell-glob-chars-regexp ()
+ "Return the lazily-created value for `eshell-glob-chars-regexp'."
+ (or eshell-glob-chars-regexp
+ (setq-local eshell-glob-chars-regexp
+ (rx-to-string `(+ (any ,@eshell-glob-chars-list)) t))))
+
+(defun eshell-parse-glob-string (glob)
+ "Add text properties to glob characters in GLOB and return the result."
+ (let ((regexp (rx-to-string
+ `(or (seq (group-n 1 "\\") anychar)
+ (group-n 2 (regexp ,(eshell-glob-chars-regexp))))
+ t)))
+ (with-temp-buffer
+ (insert glob)
+ (goto-char (point-min))
+ (while (re-search-forward regexp nil t)
+ (cond
+ ((match-beginning 1) ; Remove backslash escape.
+ (delete-region (match-beginning 1) (match-end 1)))
+ ((match-beginning 2) ; Propertize globbing character.
+ (put-text-property (match-beginning 2) (match-end 2)
+ 'eshell-glob-char t))))
+ (buffer-string))))
(defvar eshell-glob-matches)
(defvar message-shown)
'(("**/" . recurse)
("***/" . recurse-symlink)))
-(defvar eshell-glob-chars-regexp nil)
-(defsubst eshell-glob-chars-regexp ()
- "Return the lazily-created value for `eshell-glob-chars-regexp'."
- (or eshell-glob-chars-regexp
- (setq-local eshell-glob-chars-regexp
- (rx-to-string `(+ (any ,@eshell-glob-chars-list)) t))))
+(defsubst eshell--glob-char-p (string index)
+ (get-text-property index 'eshell-glob-char string))
+
+(defsubst eshell--contains-glob-char-p (string)
+ (text-property-any 0 (length string) 'eshell-glob-char t string))
+
+(defun eshell--all-glob-chars-p (string)
+ (and (length> string 0)
+ (not (text-property-not-all
+ 0 (length string) 'eshell-glob-char t string))))
(defun eshell-glob-regexp (pattern)
"Convert glob-pattern PATTERN to a regular expression.
[a-b] [a-b] matches a character or range
[^a] [^a] excludes a character or range
-If any characters in PATTERN have the text property `escaped'
-set to true, then these characters will match themselves in the
-resulting regular expression."
+This function only considers in PATTERN that have the text property
+`eshell-glob-char' set to t for conversion from glob to regexp syntax.
+All other characters are treated as literals. See also
+`eshell-parse-glob-chars' and `eshell-parse-glob-string'."
(let ((matched-in-pattern 0) ; How much of PATTERN handled
regexp)
(while (string-match (eshell-glob-chars-regexp)
(concat regexp
(regexp-quote
(substring pattern matched-in-pattern op-begin))))
- (if (get-text-property op-begin 'escaped pattern)
+ (if (not (eshell--glob-char-p pattern op-begin))
(setq regexp (concat regexp
(regexp-quote (char-to-string op-char)))
matched-in-pattern (1+ op-begin))
(defun eshell-glob-p (pattern)
"Return non-nil if PATTERN has any special glob characters."
+ (declare (obsolete nil "31.1"))
;; "~" is an infix globbing character, so one at the start of a glob
;; must be a literal.
(let ((start (if (string-prefix-p "~" pattern) 1 0)))
;; Split the glob if it contains a negation like x~y.
(while (and (eq incl glob)
(setq index (string-search "~" glob index)))
- (if (or (get-text-property index 'escaped glob)
- (or (= (1+ index) len)))
+ (if (or (not (eshell--glob-char-p glob index))
+ (= (1+ index) len))
(setq index (1+ index))
(setq incl (substring glob 0 index)
excl (substring glob (1+ index)))))
(setq start-dir (pop globs))
(setq start-dir (file-name-as-directory ".")))
(while globs
- (if-let ((recurse (cdr (assoc (car globs)
- eshell-glob-recursive-alist))))
+ ;; "~" is an infix globbing character, so one at the start of a
+ ;; glob component must be a literal.
+ (when (eq (aref (car globs) 0) ?~)
+ (remove-text-properties 0 1 '(eshell-glob-char) (car globs)))
+ (if-let* ((recurse (cdr (assoc (car globs) eshell-glob-recursive-alist)))
+ ((eshell--all-glob-chars-p
+ (string-trim-right (car globs) "/"))))
(if last-saw-recursion
(setcar result recurse)
(push recurse result)
(setq last-saw-recursion t))
- (if (or result (eshell-glob-p (car globs)))
+ (if (or result (eshell--contains-glob-char-p (car globs)))
(push (eshell-glob-convert-1 (car globs) (null (cdr globs)))
result)
;; We haven't seen a glob yet, so instead append to the start
(nreverse result)
isdir)))
+(defun eshell-expand-glob (glob)
+ "Return a list of files matched by GLOB.
+Each globbing character in GLOB should have a non-nil value for the text
+property `eshell-glob-char' (e.g. by `eshell-parse-glob-chars') in order
+for it to have syntactic meaning; otherwise, this function treats the
+character literally.
+
+This function is primarily intended for use within Eshell command
+forms. If you want to use an ordinary string as a glob, use
+`eshell-extended-glob' instead."
+ (let ((globs (eshell-glob-convert glob))
+ eshell-glob-matches message-shown)
+ (unwind-protect
+ ;; After examining GLOB, make sure we actually got some globs
+ ;; before computing the results. We can get zero globs for
+ ;; remote file names using "~", like "/ssh:remote:~/file.txt".
+ ;; During Eshell argument parsing, we can't always be sure if
+ ;; the "~" is a home directory reference or part of a glob
+ ;; (e.g. if the argument was assembled from variables).
+ (when (cadr globs)
+ (apply #'eshell-glob-entries globs))
+ (when message-shown
+ (message nil)))
+ (cond
+ (eshell-glob-matches
+ (sort eshell-glob-matches #'string<))
+ ((and eshell-error-if-no-glob (cadr globs))
+ (error "No matches found: %s" glob))
+ (t
+ (let ((result (substring-no-properties glob)))
+ (if eshell-glob-splice-results (list result) result))))))
+
(defun eshell-extended-glob (glob)
"Return a list of files matched by GLOB.
If no files match, signal an error (if `eshell-error-if-no-glob'
Mainly they are not supported because file matching is done with Emacs
regular expressions, and these cannot support the above constructs."
- (let ((globs (eshell-glob-convert glob))
- eshell-glob-matches message-shown)
- (if (null (cadr globs))
- ;; If, after examining GLOB, there are no actual globs, just
- ;; bail out. This can happen for remote file names using "~",
- ;; like "/ssh:remote:~/file.txt". During parsing, we can't
- ;; always be sure if the "~" is a home directory reference or
- ;; part of a glob (e.g. if the argument was assembled from
- ;; variables).
- (if eshell-glob-splice-results (list glob) glob)
- (unwind-protect
- (apply #'eshell-glob-entries globs)
- (if message-shown
- (message nil)))
- (or (and eshell-glob-matches (sort eshell-glob-matches #'string<))
- (if eshell-error-if-no-glob
- (error "No matches found: %s" glob)
- (if eshell-glob-splice-results
- (list glob)
- glob))))))
+ (eshell-expand-glob (eshell-parse-glob-string glob)))
+
+(defconst eshell--glob-anything (eshell-parse-glob-string "*"))
;; FIXME does this really need to abuse eshell-glob-matches, message-shown?
(defun eshell-glob-entries (path globs only-dirs)
(if (rassq (car globs) eshell-glob-recursive-alist)
(setq recurse-p (car globs)
glob (or (cadr globs)
- (eshell-glob-convert-1 "*" t))
+ (eshell-glob-convert-1 eshell--glob-anything t))
glob-remainder (cddr globs))
(setq glob (car globs)
glob-remainder (cdr globs)))
(ert-deftest em-glob-test/convert/current-start-directory ()
"Test converting a glob starting in the current directory."
- (should (equal (eshell-glob-convert "*.el")
+ (should (equal (eshell-glob-convert (eshell-parse-glob-string "*.el"))
'("./" (("\\`.*\\.el\\'" . "\\`\\.")) nil))))
(ert-deftest em-glob-test/convert/relative-start-directory ()
"Test converting a glob starting in a relative directory."
- (should (equal (eshell-glob-convert "some/where/*.el")
+ (should (equal (eshell-glob-convert
+ (eshell-parse-glob-string "some/where/*.el"))
'("./some/where/" (("\\`.*\\.el\\'" . "\\`\\.")) nil))))
(ert-deftest em-glob-test/convert/absolute-start-directory ()
"Test converting a glob starting in an absolute directory."
- (should (equal (eshell-glob-convert "/some/where/*.el")
+ (should (equal (eshell-glob-convert
+ (eshell-parse-glob-string "/some/where/*.el"))
'("/some/where/" (("\\`.*\\.el\\'" . "\\`\\.")) nil))))
(ert-deftest em-glob-test/convert/remote-start-directory ()
(skip-unless (eshell-tests-remote-accessible-p))
(let* ((default-directory ert-remote-temporary-file-directory)
(remote (file-remote-p default-directory)))
- (should (equal (eshell-glob-convert (format "%s/some/where/*.el" remote))
+ (should (equal (eshell-glob-convert
+ (format (eshell-parse-glob-string "%s/some/where/*.el")
+ remote))
`(,(format "%s/some/where/" remote)
(("\\`.*\\.el\\'" . "\\`\\.")) nil)))))
-(ert-deftest em-glob-test/convert/quoted-start-directory ()
- "Test converting a glob starting in a quoted directory name."
+(ert-deftest em-glob-test/convert/start-directory-with-spaces ()
+ "Test converting a glob starting in a directory with spaces in its name."
(should (equal (eshell-glob-convert
- (concat (eshell-escape-arg "some where/") "*.el"))
+ (eshell-parse-glob-string "some where/*.el"))
'("./some where/" (("\\`.*\\.el\\'" . "\\`\\.")) nil))))
+(ert-deftest em-glob-test/convert/literal-characters ()
+ "Test converting a \"glob\" with only literal characters."
+ (should (equal (eshell-glob-convert "*.el") '("./*.el" nil nil)))
+ (should (equal (eshell-glob-convert "**/") '("./**/" nil t))))
+
+(ert-deftest em-glob-test/convert/mixed-literal-characters ()
+ "Test converting a glob with some literal characters."
+ (should (equal (eshell-glob-convert (eshell-parse-glob-string "\\*\\*/*.el"))
+ '("./**/" (("\\`.*\\.el\\'" . "\\`\\.")) nil)))
+ (should (equal (eshell-glob-convert (eshell-parse-glob-string "**/\\*.el"))
+ '("./" (recurse ("\\`\\*\\.el\\'" . "\\`\\.")) nil))))
+
\f
;; Glob matching
(ert-deftest em-glob-test/match-n-or-more-groups ()
"Test that \"(x)#\" and \"(x)#\" match zero or more instances of \"(x)\"."
- (with-fake-files '("h.el" "ha.el" "hi.el" "hii.el" "dir/hi.el")
- (should (equal (eshell-extended-glob "hi#.el")
- '("h.el" "hi.el" "hii.el")))
- (should (equal (eshell-extended-glob "hi##.el")
- '("hi.el" "hii.el")))))
+ (with-fake-files '("h.el" "ha.el" "hi.el" "hah.el" "hahah.el" "dir/hah.el")
+ (should (equal (eshell-extended-glob "h(ah)#.el")
+ '("h.el" "hah.el" "hahah.el")))
+ (should (equal (eshell-extended-glob "h(ah)##.el")
+ '("hah.el" "hahah.el")))))
(ert-deftest em-glob-test/match-n-or-more-character-sets ()
"Test that \"[x]#\" and \"[x]#\" match zero or more instances of \"[x]\"."
(ert-deftest em-glob-test/no-matches ()
"Test behavior when a glob fails to match any files."
(with-fake-files '("foo.el" "bar.el")
- (should (equal (eshell-extended-glob "*.txt")
- "*.txt"))
+ (should (equal-including-properties (eshell-extended-glob "*.txt")
+ "*.txt"))
(let ((eshell-glob-splice-results t))
- (should (equal (eshell-extended-glob "*.txt")
- '("*.txt"))))
+ (should (equal-including-properties (eshell-extended-glob "*.txt")
+ '("*.txt"))))
(let ((eshell-error-if-no-glob t))
(should-error (eshell-extended-glob "*.txt")))))