I am writing a Bash script and need to check to see if a file exists that looks like *.$1.*.ext
I can do this really easily with POSIX test as [ -f *.$1.*.ext ]
returns true, but using the double bracket [[ -f *.$1.*.ext ]]
fails.
This is just to satisfy curiosity as I can't believe the extended testing just can't pick out whether the file exists. I know that I can use [[ `ls *.$1.*.ext` ]]
but that will match if there's more than one match. I could probably pipe it to wc
or something but that seems clunky.
Is there a simple way to use double brackets to check for the existence of a file using wildcards?
EDIT: I see that [[ -f `ls -U *.$1.*.ext` ]]
works, but I'd still prefer to not have to call ls.
Neither [ -f ... ]
nor [[ -f ... ]]
(nor other file-test operators) are designed to work with patterns (a.k.a. globs, wildcard expressions) - they always interpret their operand as a literal filename.[1]
A simple trick to test if a pattern (glob) matches exactly one file is to use a helper function:
existsExactlyOne() { [[ $# -eq 1 && -f $1 ]]; }
if existsExactlyOne *."$1".*.ext; then # ....
If you're just interested in whether there are any matches - i.e., one or more - the function is even simpler:
exists() { [[ -f $1 ]]; }
If you want to avoid a function, it gets trickier:
Caveat: This solution does not distinguish between regular files directories, for instance (though that could be fixed.)
if [[ $(shopt -s nullglob; set -- *."$1".*.ext; echo $#) -eq 1 ]]; then # ...
$(...)
) does the following:
shopt -s nullglob
instructs bash to expand the pattern to an empty string, if there are no matchesset -- ...
assigns the results of the pattern expansion to the positional parameters ($1
, $2
, ...) of the subshell in which the command substitution runs.echo $#
simply echoes the count of positional parameters, which then corresponds to the count of matching files; -eq
operator, which (numerically) compares it to 1
.Again, if you're just interested in whether there are any matches - i.e., one or more - simply replace -eq
with -ge
.
[1]
As @Etan Reisinger points out in a comment, in the case of the [ ... ]
(single-bracket syntax), the shell expands the pattern before the -f
operator even sees it (normal command-line parsing rules apply).
By contrast, different rules apply to bash's [[ ... ]]
, which is parsed differently, and in this case simply treats the pattern as a literal (i.e., doesn't expand it).
Either way, it won't work (robustly and predictably) with patterns:
[[ ... ]]
it never works: the pattern is always seen as a literal by the file-test operator.[ ... ]
it only works properly if there happens to be exactly ONE match.
nullglob
is OFF (the default), or, if nullglob
is ON, the conditional always returns true, because it is reduced to -f
, which, due to the missing operand, is no longer interpreted as a file test, but as a nonempty string (and a nonempty string evaluates to true)).[ ... ]
command breaks as a whole, because the pattern then expands to multiple words, whereas file-test operators only take one argument.as your question is bash tagged, you can take advantage of bash specific facilities, such as an array:
file=(*.ext)
[[ -f "$file" ]] && echo "yes, ${#file[@]} matching files"
this first populates an array with one item for each matching file name, then tests the first item only: Referring to the array by name without specifying an index addresses its first element. As this represents only one single file, -f behaves nicely.
An added bonus is that the number of populated array items corresponds with the number of matching files, should you need the file count, and can thereby be determined easily, as shown in the echoed output above. You may find it an advantage that no extra function needs to be defined.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With