aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--lisp/eshell/em-glob.el141
-rw-r--r--lisp/eshell/em-ls.el19
-rw-r--r--test/lisp/eshell/em-glob-tests.el48
3 files changed, 140 insertions, 68 deletions
diff --git a/lisp/eshell/em-glob.el b/lisp/eshell/em-glob.el
index 57bb0c53b57..b94c4e3ed46 100644
--- a/lisp/eshell/em-glob.el
+++ b/lisp/eshell/em-glob.el
@@ -149,23 +149,48 @@ This mimics the behavior of zsh if non-nil, but bash if nil."
149 "Don't glob the command argument. Reflect this by modifying TERMS." 149 "Don't glob the command argument. Reflect this by modifying TERMS."
150 (ignore 150 (ignore
151 (pcase (car terms) 151 (pcase (car terms)
152 ((or `(eshell-extended-glob ,term) 152 ((or `(eshell-expand-glob ,term)
153 `(eshell-splice-args (eshell-extended-glob ,term))) 153 `(eshell-splice-args (eshell-expand-glob ,term)))
154 (setcar terms term))))) 154 (setcar terms term)))))
155 155
156(defun eshell-add-glob-modifier () 156(defun eshell-add-glob-modifier ()
157 "Add `eshell-extended-glob' to the argument modifier list." 157 "Add `eshell-expand-glob' to the argument modifier list."
158 (when eshell-glob-splice-results 158 (when eshell-glob-splice-results
159 (add-hook 'eshell-current-modifiers #'eshell-splice-args 99)) 159 (add-hook 'eshell-current-modifiers #'eshell-splice-args 99))
160 (add-hook 'eshell-current-modifiers #'eshell-extended-glob)) 160 (add-hook 'eshell-current-modifiers #'eshell-expand-glob))
161 161
162(defun eshell-parse-glob-chars () 162(defun eshell-parse-glob-chars ()
163 "Parse a globbing delimiter. 163 "Parse a globbing character."
164The character is not advanced for ordinary globbing characters, so
165that other function may have a chance to override the globbing
166interpretation."
167 (when (memq (char-after) eshell-glob-chars-list) 164 (when (memq (char-after) eshell-glob-chars-list)
168 (ignore (eshell-add-glob-modifier)))) 165 (eshell-add-glob-modifier)
166 (prog1
167 (propertize (char-to-string (char-after)) 'eshell-glob-char t)
168 (forward-char))))
169
170(defvar eshell-glob-chars-regexp nil)
171(defsubst eshell-glob-chars-regexp ()
172 "Return the lazily-created value for `eshell-glob-chars-regexp'."
173 (or eshell-glob-chars-regexp
174 (setq-local eshell-glob-chars-regexp
175 (rx-to-string `(+ (any ,@eshell-glob-chars-list)) t))))
176
177(defun eshell-parse-glob-string (glob)
178 "Add text properties to glob characters in GLOB and return the result."
179 (let ((regexp (rx-to-string
180 `(or (seq (group-n 1 "\\") anychar)
181 (group-n 2 (regexp ,(eshell-glob-chars-regexp))))
182 t)))
183 (with-temp-buffer
184 (insert glob)
185 (goto-char (point-min))
186 (while (re-search-forward regexp nil t)
187 (cond
188 ((match-beginning 1) ; Remove backslash escape.
189 (delete-region (match-beginning 1) (match-end 1)))
190 ((match-beginning 2) ; Propertize globbing character.
191 (put-text-property (match-beginning 2) (match-end 2)
192 'eshell-glob-char t))))
193 (buffer-string))))
169 194
170(defvar eshell-glob-matches) 195(defvar eshell-glob-matches)
171(defvar message-shown) 196(defvar message-shown)
@@ -174,12 +199,16 @@ interpretation."
174 '(("**/" . recurse) 199 '(("**/" . recurse)
175 ("***/" . recurse-symlink))) 200 ("***/" . recurse-symlink)))
176 201
177(defvar eshell-glob-chars-regexp nil) 202(defsubst eshell--glob-char-p (string index)
178(defsubst eshell-glob-chars-regexp () 203 (get-text-property index 'eshell-glob-char string))
179 "Return the lazily-created value for `eshell-glob-chars-regexp'." 204
180 (or eshell-glob-chars-regexp 205(defsubst eshell--contains-glob-char-p (string)
181 (setq-local eshell-glob-chars-regexp 206 (text-property-any 0 (length string) 'eshell-glob-char t string))
182 (rx-to-string `(+ (any ,@eshell-glob-chars-list)) t)))) 207
208(defun eshell--all-glob-chars-p (string)
209 (and (length> string 0)
210 (not (text-property-not-all
211 0 (length string) 'eshell-glob-char t string))))
183 212
184(defun eshell-glob-regexp (pattern) 213(defun eshell-glob-regexp (pattern)
185 "Convert glob-pattern PATTERN to a regular expression. 214 "Convert glob-pattern PATTERN to a regular expression.
@@ -196,9 +225,10 @@ The basic syntax is:
196 [a-b] [a-b] matches a character or range 225 [a-b] [a-b] matches a character or range
197 [^a] [^a] excludes a character or range 226 [^a] [^a] excludes a character or range
198 227
199If any characters in PATTERN have the text property `escaped' 228This function only considers in PATTERN that have the text property
200set to true, then these characters will match themselves in the 229`eshell-glob-char' set to t for conversion from glob to regexp syntax.
201resulting regular expression." 230All other characters are treated as literals. See also
231`eshell-parse-glob-chars' and `eshell-parse-glob-string'."
202 (let ((matched-in-pattern 0) ; How much of PATTERN handled 232 (let ((matched-in-pattern 0) ; How much of PATTERN handled
203 regexp) 233 regexp)
204 (while (string-match (eshell-glob-chars-regexp) 234 (while (string-match (eshell-glob-chars-regexp)
@@ -209,7 +239,7 @@ resulting regular expression."
209 (concat regexp 239 (concat regexp
210 (regexp-quote 240 (regexp-quote
211 (substring pattern matched-in-pattern op-begin)))) 241 (substring pattern matched-in-pattern op-begin))))
212 (if (get-text-property op-begin 'escaped pattern) 242 (if (not (eshell--glob-char-p pattern op-begin))
213 (setq regexp (concat regexp 243 (setq regexp (concat regexp
214 (regexp-quote (char-to-string op-char))) 244 (regexp-quote (char-to-string op-char)))
215 matched-in-pattern (1+ op-begin)) 245 matched-in-pattern (1+ op-begin))
@@ -229,6 +259,7 @@ resulting regular expression."
229 259
230(defun eshell-glob-p (pattern) 260(defun eshell-glob-p (pattern)
231 "Return non-nil if PATTERN has any special glob characters." 261 "Return non-nil if PATTERN has any special glob characters."
262 (declare (obsolete nil "31.1"))
232 ;; "~" is an infix globbing character, so one at the start of a glob 263 ;; "~" is an infix globbing character, so one at the start of a glob
233 ;; must be a literal. 264 ;; must be a literal.
234 (let ((start (if (string-prefix-p "~" pattern) 1 0))) 265 (let ((start (if (string-prefix-p "~" pattern) 1 0)))
@@ -249,8 +280,8 @@ include, and the second for ones to exclude."
249 ;; Split the glob if it contains a negation like x~y. 280 ;; Split the glob if it contains a negation like x~y.
250 (while (and (eq incl glob) 281 (while (and (eq incl glob)
251 (setq index (string-search "~" glob index))) 282 (setq index (string-search "~" glob index)))
252 (if (or (get-text-property index 'escaped glob) 283 (if (or (not (eshell--glob-char-p glob index))
253 (or (= (1+ index) len))) 284 (= (1+ index) len))
254 (setq index (1+ index)) 285 (setq index (1+ index))
255 (setq incl (substring glob 0 index) 286 (setq incl (substring glob 0 index)
256 excl (substring glob (1+ index))))) 287 excl (substring glob (1+ index)))))
@@ -294,13 +325,18 @@ The result is a list of three elements:
294 (setq start-dir (pop globs)) 325 (setq start-dir (pop globs))
295 (setq start-dir (file-name-as-directory "."))) 326 (setq start-dir (file-name-as-directory ".")))
296 (while globs 327 (while globs
297 (if-let* ((recurse (cdr (assoc (car globs) 328 ;; "~" is an infix globbing character, so one at the start of a
298 eshell-glob-recursive-alist)))) 329 ;; glob component must be a literal.
330 (when (eq (aref (car globs) 0) ?~)
331 (remove-text-properties 0 1 '(eshell-glob-char) (car globs)))
332 (if-let* ((recurse (cdr (assoc (car globs) eshell-glob-recursive-alist)))
333 ((eshell--all-glob-chars-p
334 (string-trim-right (car globs) "/"))))
299 (if last-saw-recursion 335 (if last-saw-recursion
300 (setcar result recurse) 336 (setcar result recurse)
301 (push recurse result) 337 (push recurse result)
302 (setq last-saw-recursion t)) 338 (setq last-saw-recursion t))
303 (if (or result (eshell-glob-p (car globs))) 339 (if (or result (eshell--contains-glob-char-p (car globs)))
304 (push (eshell-glob-convert-1 (car globs) (null (cdr globs))) 340 (push (eshell-glob-convert-1 (car globs) (null (cdr globs)))
305 result) 341 result)
306 ;; We haven't seen a glob yet, so instead append to the start 342 ;; We haven't seen a glob yet, so instead append to the start
@@ -312,6 +348,38 @@ The result is a list of three elements:
312 (nreverse result) 348 (nreverse result)
313 isdir))) 349 isdir)))
314 350
351(defun eshell-expand-glob (glob)
352 "Return a list of files matched by GLOB.
353Each globbing character in GLOB should have a non-nil value for the text
354property `eshell-glob-char' (e.g. by `eshell-parse-glob-chars') in order
355for it to have syntactic meaning; otherwise, this function treats the
356character literally.
357
358This function is primarily intended for use within Eshell command
359forms. If you want to use an ordinary string as a glob, use
360`eshell-extended-glob' instead."
361 (let ((globs (eshell-glob-convert glob))
362 eshell-glob-matches message-shown)
363 (unwind-protect
364 ;; After examining GLOB, make sure we actually got some globs
365 ;; before computing the results. We can get zero globs for
366 ;; remote file names using "~", like "/ssh:remote:~/file.txt".
367 ;; During Eshell argument parsing, we can't always be sure if
368 ;; the "~" is a home directory reference or part of a glob
369 ;; (e.g. if the argument was assembled from variables).
370 (when (cadr globs)
371 (apply #'eshell-glob-entries globs))
372 (when message-shown
373 (message nil)))
374 (cond
375 (eshell-glob-matches
376 (sort eshell-glob-matches #'string<))
377 ((and eshell-error-if-no-glob (cadr globs))
378 (error "No matches found: %s" glob))
379 (t
380 (let ((result (substring-no-properties glob)))
381 (if eshell-glob-splice-results (list result) result))))))
382
315(defun eshell-extended-glob (glob) 383(defun eshell-extended-glob (glob)
316 "Return a list of files matched by GLOB. 384 "Return a list of files matched by GLOB.
317If no files match, signal an error (if `eshell-error-if-no-glob' 385If no files match, signal an error (if `eshell-error-if-no-glob'
@@ -327,26 +395,9 @@ syntax. Things that are not supported are:
327 395
328Mainly they are not supported because file matching is done with Emacs 396Mainly they are not supported because file matching is done with Emacs
329regular expressions, and these cannot support the above constructs." 397regular expressions, and these cannot support the above constructs."
330 (let ((globs (eshell-glob-convert glob)) 398 (eshell-expand-glob (eshell-parse-glob-string glob)))
331 eshell-glob-matches message-shown) 399
332 (if (null (cadr globs)) 400(defconst eshell--glob-anything (eshell-parse-glob-string "*"))
333 ;; If, after examining GLOB, there are no actual globs, just
334 ;; bail out. This can happen for remote file names using "~",
335 ;; like "/ssh:remote:~/file.txt". During parsing, we can't
336 ;; always be sure if the "~" is a home directory reference or
337 ;; part of a glob (e.g. if the argument was assembled from
338 ;; variables).
339 (if eshell-glob-splice-results (list glob) glob)
340 (unwind-protect
341 (apply #'eshell-glob-entries globs)
342 (if message-shown
343 (message nil)))
344 (or (and eshell-glob-matches (sort eshell-glob-matches #'string<))
345 (if eshell-error-if-no-glob
346 (error "No matches found: %s" glob)
347 (if eshell-glob-splice-results
348 (list glob)
349 glob))))))
350 401
351;; FIXME does this really need to abuse eshell-glob-matches, message-shown? 402;; FIXME does this really need to abuse eshell-glob-matches, message-shown?
352(defun eshell-glob-entries (path globs only-dirs) 403(defun eshell-glob-entries (path globs only-dirs)
@@ -363,7 +414,7 @@ directories and files."
363 (if (rassq (car globs) eshell-glob-recursive-alist) 414 (if (rassq (car globs) eshell-glob-recursive-alist)
364 (setq recurse-p (car globs) 415 (setq recurse-p (car globs)
365 glob (or (cadr globs) 416 glob (or (cadr globs)
366 (eshell-glob-convert-1 "*" t)) 417 (eshell-glob-convert-1 eshell--glob-anything t))
367 glob-remainder (cddr globs)) 418 glob-remainder (cddr globs))
368 (setq glob (car globs) 419 (setq glob (car globs)
369 glob-remainder (cdr globs))) 420 glob-remainder (cdr globs)))
diff --git a/lisp/eshell/em-ls.el b/lisp/eshell/em-ls.el
index 8bf2e20d320..e8cdb9c82c4 100644
--- a/lisp/eshell/em-ls.el
+++ b/lisp/eshell/em-ls.el
@@ -246,6 +246,17 @@ scope during the evaluation of TEST-SEXP."
246 246
247(declare-function eshell-extended-glob "em-glob" (glob)) 247(declare-function eshell-extended-glob "em-glob" (glob))
248(defvar eshell-error-if-no-glob) 248(defvar eshell-error-if-no-glob)
249(defvar eshell-glob-splice-results)
250
251(defun eshell-ls--expand-wildcards (file)
252 "Expand the shell wildcards in FILE if any."
253 (if (and (atom file)
254 (not (file-exists-p file)))
255 (let ((eshell-error-if-no-glob t)
256 ;; Ensure `eshell-extended-glob' returns a list.
257 (eshell-glob-splice-results t))
258 (mapcar #'file-relative-name (eshell-extended-glob file)))
259 (list (file-relative-name file))))
249 260
250(defun eshell-ls--insert-directory 261(defun eshell-ls--insert-directory
251 (orig-fun file switches &optional wildcard full-directory-p) 262 (orig-fun file switches &optional wildcard full-directory-p)
@@ -277,13 +288,7 @@ instead."
277 (require 'em-glob) 288 (require 'em-glob)
278 (let* ((insert-func 'insert) 289 (let* ((insert-func 'insert)
279 (error-func 'insert) 290 (error-func 'insert)
280 (eshell-error-if-no-glob t) 291 (target (eshell-ls--expand-wildcards file))
281 (target ; Expand the shell wildcards if any.
282 (if (and (atom file)
283 (string-match "[[?*]" file)
284 (not (file-exists-p file)))
285 (mapcar #'file-relative-name (eshell-extended-glob file))
286 (file-relative-name file)))
287 (switches 292 (switches
288 (append eshell-ls-dired-initial-args 293 (append eshell-ls-dired-initial-args
289 (and (or (consp dired-directory) wildcard) (list "-d")) 294 (and (or (consp dired-directory) wildcard) (list "-d"))
diff --git a/test/lisp/eshell/em-glob-tests.el b/test/lisp/eshell/em-glob-tests.el
index 16ae9be1bce..57343eced6b 100644
--- a/test/lisp/eshell/em-glob-tests.el
+++ b/test/lisp/eshell/em-glob-tests.el
@@ -134,17 +134,19 @@ value of `eshell-glob-splice-results'."
134 134
135(ert-deftest em-glob-test/convert/current-start-directory () 135(ert-deftest em-glob-test/convert/current-start-directory ()
136 "Test converting a glob starting in the current directory." 136 "Test converting a glob starting in the current directory."
137 (should (equal (eshell-glob-convert "*.el") 137 (should (equal (eshell-glob-convert (eshell-parse-glob-string "*.el"))
138 '("./" (("\\`.*\\.el\\'" . "\\`\\.")) nil)))) 138 '("./" (("\\`.*\\.el\\'" . "\\`\\.")) nil))))
139 139
140(ert-deftest em-glob-test/convert/relative-start-directory () 140(ert-deftest em-glob-test/convert/relative-start-directory ()
141 "Test converting a glob starting in a relative directory." 141 "Test converting a glob starting in a relative directory."
142 (should (equal (eshell-glob-convert "some/where/*.el") 142 (should (equal (eshell-glob-convert
143 (eshell-parse-glob-string "some/where/*.el"))
143 '("./some/where/" (("\\`.*\\.el\\'" . "\\`\\.")) nil)))) 144 '("./some/where/" (("\\`.*\\.el\\'" . "\\`\\.")) nil))))
144 145
145(ert-deftest em-glob-test/convert/absolute-start-directory () 146(ert-deftest em-glob-test/convert/absolute-start-directory ()
146 "Test converting a glob starting in an absolute directory." 147 "Test converting a glob starting in an absolute directory."
147 (should (equal (eshell-glob-convert "/some/where/*.el") 148 (should (equal (eshell-glob-convert
149 (eshell-parse-glob-string "/some/where/*.el"))
148 '("/some/where/" (("\\`.*\\.el\\'" . "\\`\\.")) nil)))) 150 '("/some/where/" (("\\`.*\\.el\\'" . "\\`\\.")) nil))))
149 151
150(ert-deftest em-glob-test/convert/remote-start-directory () 152(ert-deftest em-glob-test/convert/remote-start-directory ()
@@ -152,16 +154,30 @@ value of `eshell-glob-splice-results'."
152 (skip-unless (eshell-tests-remote-accessible-p)) 154 (skip-unless (eshell-tests-remote-accessible-p))
153 (let* ((default-directory ert-remote-temporary-file-directory) 155 (let* ((default-directory ert-remote-temporary-file-directory)
154 (remote (file-remote-p default-directory))) 156 (remote (file-remote-p default-directory)))
155 (should (equal (eshell-glob-convert (format "%s/some/where/*.el" remote)) 157 (should (equal (eshell-glob-convert
158 (format (eshell-parse-glob-string "%s/some/where/*.el")
159 remote))
156 `(,(format "%s/some/where/" remote) 160 `(,(format "%s/some/where/" remote)
157 (("\\`.*\\.el\\'" . "\\`\\.")) nil))))) 161 (("\\`.*\\.el\\'" . "\\`\\.")) nil)))))
158 162
159(ert-deftest em-glob-test/convert/quoted-start-directory () 163(ert-deftest em-glob-test/convert/start-directory-with-spaces ()
160 "Test converting a glob starting in a quoted directory name." 164 "Test converting a glob starting in a directory with spaces in its name."
161 (should (equal (eshell-glob-convert 165 (should (equal (eshell-glob-convert
162 (concat (eshell-escape-arg "some where/") "*.el")) 166 (eshell-parse-glob-string "some where/*.el"))
163 '("./some where/" (("\\`.*\\.el\\'" . "\\`\\.")) nil)))) 167 '("./some where/" (("\\`.*\\.el\\'" . "\\`\\.")) nil))))
164 168
169(ert-deftest em-glob-test/convert/literal-characters ()
170 "Test converting a \"glob\" with only literal characters."
171 (should (equal (eshell-glob-convert "*.el") '("./*.el" nil nil)))
172 (should (equal (eshell-glob-convert "**/") '("./**/" nil t))))
173
174(ert-deftest em-glob-test/convert/mixed-literal-characters ()
175 "Test converting a glob with some literal characters."
176 (should (equal (eshell-glob-convert (eshell-parse-glob-string "\\*\\*/*.el"))
177 '("./**/" (("\\`.*\\.el\\'" . "\\`\\.")) nil)))
178 (should (equal (eshell-glob-convert (eshell-parse-glob-string "**/\\*.el"))
179 '("./" (recurse ("\\`\\*\\.el\\'" . "\\`\\.")) nil))))
180
165 181
166;; Glob matching 182;; Glob matching
167 183
@@ -262,11 +278,11 @@ value of `eshell-glob-splice-results'."
262 278
263(ert-deftest em-glob-test/match-n-or-more-groups () 279(ert-deftest em-glob-test/match-n-or-more-groups ()
264 "Test that \"(x)#\" and \"(x)#\" match zero or more instances of \"(x)\"." 280 "Test that \"(x)#\" and \"(x)#\" match zero or more instances of \"(x)\"."
265 (with-fake-files '("h.el" "ha.el" "hi.el" "hii.el" "dir/hi.el") 281 (with-fake-files '("h.el" "ha.el" "hi.el" "hah.el" "hahah.el" "dir/hah.el")
266 (should (equal (eshell-extended-glob "hi#.el") 282 (should (equal (eshell-extended-glob "h(ah)#.el")
267 '("h.el" "hi.el" "hii.el"))) 283 '("h.el" "hah.el" "hahah.el")))
268 (should (equal (eshell-extended-glob "hi##.el") 284 (should (equal (eshell-extended-glob "h(ah)##.el")
269 '("hi.el" "hii.el"))))) 285 '("hah.el" "hahah.el")))))
270 286
271(ert-deftest em-glob-test/match-n-or-more-character-sets () 287(ert-deftest em-glob-test/match-n-or-more-character-sets ()
272 "Test that \"[x]#\" and \"[x]#\" match zero or more instances of \"[x]\"." 288 "Test that \"[x]#\" and \"[x]#\" match zero or more instances of \"[x]\"."
@@ -300,11 +316,11 @@ value of `eshell-glob-splice-results'."
300(ert-deftest em-glob-test/no-matches () 316(ert-deftest em-glob-test/no-matches ()
301 "Test behavior when a glob fails to match any files." 317 "Test behavior when a glob fails to match any files."
302 (with-fake-files '("foo.el" "bar.el") 318 (with-fake-files '("foo.el" "bar.el")
303 (should (equal (eshell-extended-glob "*.txt") 319 (should (equal-including-properties (eshell-extended-glob "*.txt")
304 "*.txt")) 320 "*.txt"))
305 (let ((eshell-glob-splice-results t)) 321 (let ((eshell-glob-splice-results t))
306 (should (equal (eshell-extended-glob "*.txt") 322 (should (equal-including-properties (eshell-extended-glob "*.txt")
307 '("*.txt")))) 323 '("*.txt"))))
308 (let ((eshell-error-if-no-glob t)) 324 (let ((eshell-error-if-no-glob t))
309 (should-error (eshell-extended-glob "*.txt"))))) 325 (should-error (eshell-extended-glob "*.txt")))))
310 326