Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bash check if file exists with double bracket test and wildcards

Tags:

bash

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.

like image 583
William Everett Avatar asked Jul 07 '14 16:07

William Everett


2 Answers

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 # ...
  • The code inside the command substitution ($(...)) does the following:
    • shopt -s nullglob instructs bash to expand the pattern to an empty string, if there are no matches
    • set -- ... 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;
  • That echoed number (the command substitution's stdout output) becomes the left-hand side to the -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:

  • With [[ ... ]] it never works: the pattern is always seen as a literal by the file-test operator.
  • With [ ... ] it only works properly if there happens to be exactly ONE match.
    • If there's NO match:
      • The file-test operator sees the pattern as a literal, if 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)).
    • If there are MULTIPLE matches: the [ ... ] command breaks as a whole, because the pattern then expands to multiple words, whereas file-test operators only take one argument.
like image 107
mklement0 Avatar answered Nov 12 '22 00:11

mklement0


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.

like image 21
Deleted User Avatar answered Nov 12 '22 01:11

Deleted User