最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

fish shell: Glob function? - Stack Overflow

programmeradmin2浏览0评论

When I migrated to fish from bash, I found glob patterns like "tmp[0-9][0-9].pdf" aren't supported. Then, recently fish has stopped to support the ? wildcard:

$ fish --version
fish, version 4.0.0
$ ls tmp00.pdf
tmp00.pdf
$ ls tmp??.pdf
ls: tmp??.pdf: No such file or directory
$

.0/fish_for_bash_users.html says, "Fish only supports the * and ** glob (and the deprecated ? glob) as syntax."

So, has somebody written a fish function that can simulate the good-old glob? which might be used like

for file in (fmatch 'img..\.jpg') # to get img00.jpg, img01.jpg, . . .
   # do something on $file

With some help from ChatGPT, I've been able to write something that filters the output from '*' with string match -r -- $regex -- $file, but the handling of paths is incredibly hard to me:

# bash
for file in ../../*/img??.jpg # How to implement this?

and also the constant need of escaping . in regex is tedious, especially when your path includes ../.

I tried fd -g <glob pattern> but this glob doesn't seem to allow for paths.

I need the good old Glob. Perhaps I should admit the defeat :-) and do this

function glob --description 'Good old glob'
    if test (count $argv) -ne 1
        echo "Usage: glob <glob_pattern>"
        return 1
    end
    bash -c 'printf "%s\n" $*' _ $argv[1] # call bash!
end

Edit: Initially, I wrote

bash -c "ls $argv[1]"

But a better version was proposed by @CharlesDuffy, which I've replaced mine with, in the above fish function. [Please refer to @CharlesDuffy's discussion below for why printf and what problems exist in $*. But, given that the above solution still has some problems, it's probably better to install this glob command instead of writing a fish function like the above.]

I'm already using the glob.fish function defined as above. So far so good. glob tmp[0-9][0-9].pdf works!

When I migrated to fish from bash, I found glob patterns like "tmp[0-9][0-9].pdf" aren't supported. Then, recently fish has stopped to support the ? wildcard:

$ fish --version
fish, version 4.0.0
$ ls tmp00.pdf
tmp00.pdf
$ ls tmp??.pdf
ls: tmp??.pdf: No such file or directory
$

https://fishshell/docs/4.0/fish_for_bash_users.html says, "Fish only supports the * and ** glob (and the deprecated ? glob) as syntax."

So, has somebody written a fish function that can simulate the good-old glob? which might be used like

for file in (fmatch 'img..\.jpg') # to get img00.jpg, img01.jpg, . . .
   # do something on $file

With some help from ChatGPT, I've been able to write something that filters the output from '*' with string match -r -- $regex -- $file, but the handling of paths is incredibly hard to me:

# bash
for file in ../../*/img??.jpg # How to implement this?

and also the constant need of escaping . in regex is tedious, especially when your path includes ../.

I tried fd -g <glob pattern> but this glob doesn't seem to allow for paths.

I need the good old Glob. Perhaps I should admit the defeat :-) and do this

function glob --description 'Good old glob'
    if test (count $argv) -ne 1
        echo "Usage: glob <glob_pattern>"
        return 1
    end
    bash -c 'printf "%s\n" $*' _ $argv[1] # call bash!
end

Edit: Initially, I wrote

bash -c "ls $argv[1]"

But a better version was proposed by @CharlesDuffy, which I've replaced mine with, in the above fish function. [Please refer to @CharlesDuffy's discussion below for why printf and what problems exist in $*. But, given that the above solution still has some problems, it's probably better to install this glob command instead of writing a fish function like the above.]

I'm already using the glob.fish function defined as above. So far so good. glob tmp[0-9][0-9].pdf works!

Share Improve this question edited Mar 11 at 5:40 Ryo asked Mar 5 at 15:46 RyoRyo 4123 silver badges8 bronze badges 22
  • 1 fish supports ? just fine. It's not clear why you are conflating regular expressions and glob patterns, though. . has no special meaning in a glob. Neither * nor ? will match the path separator, though, so to search in an arbitrarily deep path, you need to use **, which matches zero or more path components: **/foo would match /foo, /bar/foo, /xyz/bar/foo, etc. – chepner Commented Mar 5 at 15:56
  • 1 @chepner "fish supports ? just fine" . . . It seems to have been removed recently. I've edited my question above. I understand the rest of what you say. – Ryo Commented Mar 6 at 18:40
  • 1 BTW, the recommendation to use printf instead of ls was very deliberate. (Alternate source, written in light of objections to that FAQ). Admittedly, a better solution would use printf '%s\0' instead of printf '%s\n', but I don't know how to make the fish end of that work off the top of my head (specifically, I don't know how to tell fish to expect NUL delimiters when reading a subprocess's output into an array). – Charles Duffy Commented Mar 6 at 23:02
  • 1 @xpusostomos, $* unquoted in bash concatenates all your arguments into a single string, then word-splits that string and expands each element in it as a separate glob -- so if your original argv has items with spaces, those spaces are instead treated as separating individual glob expressions. – Charles Duffy Commented Mar 8 at 17:17
  • 1 @xpusostomos, ...additionally, with echo $* you can't tell if a space in the output is intended to separate two distinct items, or is a case where a glob matched a filename with a space as part of its literal content (where that entire filename needs to be understood by the caller as a single object). – Charles Duffy Commented Mar 8 at 17:18
 |  Show 17 more comments

2 Answers 2

Reset to default 2

I happened to be looking for a solution to this exact problem today...

Standing on the shoulders of everyone in the discussion above (special thanks to Ryo and Charles Duffy), I came up with this improved but somewhat "uglified" and more complex version of OP’s function...

function bash_glob --description 'Expand bash-style glob string into an array'
    [ (count $argv) -eq 0 ] && echo 'Usage: bash_glob <pattern>' && return 1

    bash -c '
        shopt -s nullglob;
        IFS=;
        eval "printf \"%s\\\\\\0\" $*"
    ' _ $argv | string split0
end

Example:

me@my-computer ~> bash_glob '.config/[e-n]??[cm]*/README.{md,txt}'

.config/emacs/README.md
.config/nvim/README.md

me@my-computer ~> ▇

Main Changes:

  • \0 as an array separator: Avoids wrong processing of paths containing whitespaces.
  • IFS= as a security measure: Prevents word splitting and potential code injection in Bash.
  • eval: Ensures Bash fully expands $*, and that the glob patterns are processed entirely within Bash.
  • shopt -s nullglob: Prevents unexpanded glob patterns from appearing as extra arguments - this way, only matched files are included in the result.

Only with these changes was I able to get the results I needed, since I did have to handle filenames with spaces, glob patterns that wouldn’t always expand, and other edge cases. This approach avoids most issues, at least.

And I'm not too happy with \0 escaped as \\\\\\0, but this is due to the multiple layers of indirection - each shell's handling of escape sequences and string parsing.

I myself use the solution provided by @rsenna (Thank you!) et al, but I post this just to suggest an alternative, which people might be interested in.

This project

https://github/p-ranav/glob

provides a glob library. As an example, it also provides a very simple stand-alone command for globbing. Out of the box, it doesn't work like glob <pattern> but it's a very short simple C++ program that can be easily modified.

So, here I report I just modified the source code in a trivial way, compiled everything using the provided make script, and successfully obtained a command line tool that can be an alternative to the fish-shell function shown in other messages.

If I'm very much bored (which wouldn't happen in the near future), I might turn this glob command into a Homebrew package . . .

发布评论

评论列表(0)

  1. 暂无评论