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!
2 Answers
Reset to default 2I 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 . . .
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:56printf
instead ofls
was very deliberate. (Alternate source, written in light of objections to that FAQ). Admittedly, a better solution would useprintf '%s\0'
instead ofprintf '%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$*
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:17echo $*
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