Test whether a glob has any matches in Bash

BashGlob

Bash Problem Overview


If I want to check for the existence of a single file, I can test for it using test -e filename or [ -e filename ].

Supposing I have a glob and I want to know whether any files exist whose names match the glob. The glob can match 0 files (in which case I need to do nothing), or it can match 1 or more files (in which case I need to do something). How can I test whether a glob has any matches? (I don't care how many matches there are, and it would be best if I could do this with one if statement and no loops (simply because I find that most readable).

(test -e glob* fails if the glob matches more than one file.)

Bash Solutions


Solution 1 - Bash

Bash-specific solution:
compgen -G "<glob-pattern>"

Escape the pattern or it'll get pre-expanded into matches.

Exit status is:

  • 1 for no-match,
  • 0 for 'one or more matches'

stdout is a list of files matching the glob. I think this is the best option in terms of conciseness and minimizing potential side effects.

Example:

if compgen -G "/tmp/someFiles*" > /dev/null; then
    echo "Some files exist."
fi

Solution 2 - Bash

The nullglob shell option is indeed a bashism.

To avoid the need for a tedious save and restore of the nullglob state, I'd only set it inside the subshell that expands the glob:

if test -n "$(shopt -s nullglob; echo glob*)"
then
    echo found
else
    echo not found
fi

For better portability and more flexible globbing, use find:

if test -n "$(find . -maxdepth 1 -name 'glob*' -print -quit)"
then
    echo found
else
    echo not found
fi

Explicit -print -quit actions are used for find instead of the default implicit -print action so that find will quit as soon as it finds the first file matching the search criteria. Where lots of files match, this should run much faster than echo glob* or ls glob* and it also avoids the possibility of overstuffing the expanded command line (some shells have a 4K length limit).

If find feels like overkill and the number of files likely to match is small, use stat:

if stat -t glob* >/dev/null 2>&1
then
    echo found
else
    echo not found
fi

Solution 3 - Bash

I like

exists() {
    [ -e "$1" ]
}

if exists glob*; then
    echo found
else
    echo not found
fi

This is both readable and efficient (unless there are a huge number of files).
The main drawback is that it's much more subtle than it looks, and I sometimes feel compelled to add a long comment.
If there's a match, "glob*" is expanded by the shell and all the matches are passed to exists(), which checks the first one and ignores the rest.
If there's no match, "glob*" is passed to exists() and found not to exist there either.

Edit: there may be a false positive, see comment

Solution 4 - Bash

#!/usr/bin/env bash

# If it is set, then an unmatched glob is swept away entirely -- 
# replaced with a set of zero words -- 
# instead of remaining in place as a single word.
shopt -s nullglob

M=(*px)

if [ "${#M[*]}" -ge 1 ]; then
    echo "${#M[*]} matches."
else
    echo "No such files."
fi

Solution 5 - Bash

If you have globfail set you can use this crazy (which you really should not)

shopt -s failglob # exit if * does not match 
( : * ) && echo 0 || echo 1

or

q=( * ) && echo 0 || echo 1

Solution 6 - Bash

test -e has the unfortunate caveat that it considers broken symbolic links to not exist. So you may want to check for those, too.

function globexists {
  test -e "$1" -o -L "$1"
}

if globexists glob*; then
    echo found
else
    echo not found
fi

Solution 7 - Bash

I have yet another solution:

if [ "$(echo glob*)" != 'glob*' ]

This works nicely for me. There may be some corner cases I missed.

Solution 8 - Bash

To simplify miku's answer somewhat, based on his idea:

M=(*py)
if [ -e ${M[0]} ]; then
  echo Found
else
  echo Not Found
fi

Solution 9 - Bash

Based on flabdablet's answer, for me it looks like easiest (not necessarily fastest) is just to use find itself, while leaving glob expansion on shell, like:

find /some/{p,long-p}ath/with/*globs* -quit &> /dev/null && echo "MATCH"

Or in if like:

if find $yourGlob -quit &> /dev/null; then
    echo "MATCH"
else
    echo "NOT-FOUND"
fi

Solution 10 - Bash

In Bash, you can glob to an array; if the glob didn't match, your array will contain a single entry that doesn't correspond to an existing file:

#!/bin/bash

shellglob='*.sh'

scripts=($shellglob)

if [ -e "${scripts[0]}" ]
then stat "${scripts[@]}"
fi

Note: if you have nullglob set, scripts will be an empty array, and you should test with [ "${scripts[*]}" ] or with [ "${#scripts[*]}" != 0 ] instead. If you're writing a library that must work with or without nullglob, you'll want

if [ "${scripts[*]}" ] && [ -e "${scripts[0]}" ]

An advantage of this approach is that you then have the list of files you want to work with, rather than having to repeat the glob operation.

Solution 11 - Bash

#!/bin/bash
set nullglob
touch /tmp/foo1 /tmp/foo2 /tmp/foo3
FOUND=0
for FILE in /tmp/foo*
do
    FOUND=$((${FOUND} + 1))
done
if [ ${FOUND} -gt 0 ]; then
    echo "I found ${FOUND} matches"
else
    echo "No matches found"
fi

Solution 12 - Bash

set -- glob*
if [ -f "$1" ]; then
  echo "It matched"
fi
Explanation

When there isn't a match for glob*, then $1 will contain 'glob*'. The test -f "$1" won't be true because the glob* file doesn't exist.

Why this is better than alternatives

This works with sh and derivates: KornShell and Bash. It doesn't create any sub-shell. $(..) and `...` commands create a sub-shell; they fork a process, and therefore are slower than this solution.

Solution 13 - Bash

Like this in Bash (test files containing pattern):

shopt -s nullglob
compgen -W *pattern* &>/dev/null
case $? in
    0) echo "only one file match" ;;
    1) echo "more than one file match" ;;
    2) echo "no file match" ;;
esac

It's far better than compgen -G: because we can discriminates more cases and more precisely.

It can work with only one wildcard *.

Solution 14 - Bash

This abomination seems to work:

#!/usr/bin/env bash
shopt -s nullglob
if [ "`echo *py`" != "" ]; then
    echo "Glob matched"
else
    echo "Glob did not match"
fi

It probably requires bash, not sh.

This works because the nullglob option causes the glob to evaluate to an empty string if there are no matches. Thus any non-empty output from the echo command indicates that the glob matched something.

Solution 15 - Bash

A solution for extended globs (extglob) in Bash:
bash -c $'shopt -s extglob \n /bin/ls -1U <ext-glob-pattern>'

Exit status is 0 if there is at least one match, and non-zero (2) when there is no match. Standard output contains a newline-separated list of matching files (and file names containing spaces they are quoted).

Or, slightly different:

bash -c $'shopt -s extglob \n compgen -G <ext-glob-pattern>'

Differences to the ls-based solution: probably faster (not measured), file names with spaces not quoted in output, exit code 1 when there is no match (not 2 :shrug:).

Example usage:

No match:

$ bash -c $'shopt -s extglob \n /bin/ls -1U @(*.foo|*.bar)'; echo "exit status: $?"
/bin/ls: cannot access '@(*.foo|*.bar)': No such file or directory
exit status: 2

At least one match:

$ bash -c $'shopt -s extglob \n /bin/ls -1U @(*.ts|*.mp4)'; echo "exit status: $?"
'video1 with spaces.mp4'
video2.mp4
video3.mp4
exit status: 0
Concepts used:
  • ls' exit code behavior (adds -U for efficiency, and -1 for output control).
  • Does not enable extglob in current shell (often not desired).
  • Makes use of the $ prefix so that the \n is interpreted, so that the extended glob pattern is on a different line than the shopt -s extglob -- otherwise the extended glob pattern would be a syntax error!

Note 1: I worked towards this solution because the compgen -G "<glob-pattern>" approach suggested in other answers does not seem to work smoothly with brace expansion; and yet I needed some more advanced globbing features.

Note 2: lovely resource for the extended glob syntax: extglob

Solution 16 - Bash

Popular belief is that [ -f file* ] doesn't work. The fact it, it does work, and I personally find it very useful on certain occasions -- when I want to catch the name of one and only one file in a particular location. Like, for example, a file that has a version number in its name. Consider this code:

if [ -f "$ROOT"/lib64/libc-*.so ] ;then
  LIBC=$(basename -- "$ROOT"/lib64/libc-*.so .so)
else
  echo "libc ??" ; exit 1
fi

BTW, ShellCheck cries foul when it sees such use. :-) I wish they fix that!

SC2144. -e doesn't work with globs. Use a for loop.

Solution 17 - Bash

Both nullglob and compgen are useful only on some bash shells.

A (non-recursive) solution that works on most shells is:

set -- ./glob*                  # or /path/dir/glob*
[ -f "$1" ] || shift            # remove the glob if present.
if    [ "$#" -lt 1 ]
then  echo "at least one file found"
fi

Solution 18 - Bash

(ls glob* &>/dev/null && echo Files found) || echo No file found

Solution 19 - Bash

if ls -d $glob > /dev/null 2>&1; then
  echo Found.
else
  echo Not found.
fi

Note that this can be very time cosuming if there are a lot of matches or file access is slow.

Solution 20 - Bash

ls | grep -q "glob.*"

Not the most efficient solution (if there's a ton of files in the directory it might be slowish), but it's simple, easy to read and also has the advantage that regexes are more powerful than plain Bash glob patterns.

Solution 21 - Bash

[ `ls glob* 2>/dev/null | head -n 1` ] && echo true

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionKen BloomView Question on Stackoverflow
Solution 1 - BashBrian ChrismanView Answer on Stackoverflow
Solution 2 - BashflabdabletView Answer on Stackoverflow
Solution 3 - BashDan BlochView Answer on Stackoverflow
Solution 4 - BashmikuView Answer on Stackoverflow
Solution 5 - BashJames AndinoView Answer on Stackoverflow
Solution 6 - BashNerdMachineView Answer on Stackoverflow
Solution 7 - BashSaschaZornView Answer on Stackoverflow
Solution 8 - BashKen BloomView Answer on Stackoverflow
Solution 9 - BashqueriaView Answer on Stackoverflow
Solution 10 - BashToby SpeightView Answer on Stackoverflow
Solution 11 - BashPeter LyonsView Answer on Stackoverflow
Solution 12 - BashjoseyluisView Answer on Stackoverflow
Solution 13 - BashGilles QuenotView Answer on Stackoverflow
Solution 14 - BashRyan C. ThompsonView Answer on Stackoverflow
Solution 15 - BashDr. Jan-Philip GehrckeView Answer on Stackoverflow
Solution 16 - BashPourkoView Answer on Stackoverflow
Solution 17 - BashIsaaCView Answer on Stackoverflow
Solution 18 - BashDamodharan RView Answer on Stackoverflow
Solution 19 - BashFlorian DieschView Answer on Stackoverflow
Solution 20 - BashjesjimherView Answer on Stackoverflow
Solution 21 - BashotocanView Answer on Stackoverflow