Test if there are files matching a pattern in order to execute a script

[ -f /*.txt ]

would return true only if there's one (and only one) non-hidden file in / whose name ends in .txt and if that file is a regular file or a symlink to a regular file.

That's because wildcards are expanded by the shell prior to being passed to the command (here [).

So if there's a /a.txt and /b.txt, [ will be passed 5 arguments: [, -f, /a.txt, /b.txt and ]. [ would then complain that -f is given too many arguments.

If you want to check that the *.txt pattern expands to at least one non-hidden file (regular or not), in the bash shell:

shopt -s nullglob
set -- *.txt
if [ "$#" -gt 0 ]; then
  ./script "$@" # call script with that list of files.
fi
# Or with bash arrays so you can keep the arguments:
files=( *.txt )
# apply C-style boolean on member count
(( ${#files[@]} )) && ./script "${files[@]}"

shopt -s nullglob is bash specific (shopt is, nullglob actually comes from zsh), but shells like ksh93, zsh, yash, tcsh have equivalent statements.

With zsh, the test for are there files matching a pattern can be written using an anonymous function and the N (for nullglob) and Y1 (to stop after the first find) glob qualifier:

if ()(($#)) *.txt(NY1); then
  do-something
fi

Note that those find those files by reading the contents of the directory, it doesn't try and access those files at all which makes it more efficient than solutions that call commands like ls or stat on that list of files computed by the shell.

The standard sh equivalent would be:

set -- [*].txt *.txt
case "$1$2" in
  ('[*].txt*.txt') ;;
  (*) shift; script "$@"
esac

The problem is that with Bourne or POSIX shells, if a pattern doesn't match, it expands to itself. So if *.txt expands to *.txt, you don't know whether it's because there's no .txt file in the directory or because there's one file called *.txt. Using [*].txt *.txt allows to discriminate between the two.

Now, if you want to check that the *.txt matches at least one regular file or symlink to regular file (like your [ -f *.txt ] suggests you want to do), or that all the files that match *.txt are regular files (after symlink resolution), that's yet another matter.

With zsh:

if ()(($#)) *.txt(NY1-.); then
  echo "there is at least one regular .txt file"
fi
if ()(($#)) *.txt(NY1^-.); then
  echo "there is at least one non-regular .txt files"
fi

(remove the - if you want to do the test prior to symlink resolution, that is consider symlinks as non-regular files whether they point to regular files or not).


You could always use find:

find . -maxdepth 1 -type f -name "*.txt" 2>/dev/null | grep -q . && ./script

Explanation:

  • find . : search the current directory
  • -maxdepth 1: do not search subdirectories
  • -type f : search only regular files
  • name "*.txt" : search for files ending in .txt
  • 2>/dev/null : redirect error messages to /dev/null
  • | grep -q . : grep for any character, will return false if no characters found.
  • && ./script : Execute ./script only if the previous command was successful (&&)

A possible solution is also Bash builtin compgen. That command returns all possible matches for a globbing pattern and has an exit code indicating whether any files matched.

compgen -G "/*.text" > /dev/null && ./script

I found this question while looking for solutions that are faster though.