324

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.)

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Ken Bloom
  • 57,498
  • 14
  • 111
  • 168
  • 4
    I suspect my answer below is 'clearly correct' in a way that all the others kind of hack-around. It's a one-line shell-builtin that's been around forever and appears to be 'the intended tool for this particular job'. I'm concerned that users will mistakenly reference the accepted answer here. Anybody please feel free to correct me and I'll withdraw my comment here, I'm more than happy to be wrong and learn from it. If the difference didn't appear so drastic, I wouldn't raise this issue. – Brian Chrisman Dec 11 '15 at 20:18
  • 1
    My favorite solutions to this question are [the find command](http://stackoverflow.com/a/4264351/197788) which works in any shell (even non-Bourne shells) but requires GNU find, and the [compgen command](http://stackoverflow.com/a/34195247/197788) which is clearly a Bashism. Too bad I can't accept both answers. – Ken Bloom Jan 04 '16 at 15:29
  • 1
    Note: This question has been edited since it was asked. The original title was "Test whether a glob has any matches in bash". The specific shell, 'bash', was dropped from the question after I published my answer. The editing of the question's title makes my answer appear to be in error. I hope someone can amend or at least address this change. – Brian Chrisman Sep 09 '18 at 16:16
  • Adding here a note that "glob" is a synonym for "wildcard", in case people are searching on the second term. – tripleee May 10 '21 at 16:42

20 Answers20

276

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
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Brian Chrisman
  • 3,482
  • 1
  • 15
  • 16
  • 20
    Note that `compgen` is a _bash_-specific built-in command and is not part of the POSIX standard Unix shell specified built-in commands. http://pubs.opengroup.org/onlinepubs/9699919799/ http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_14 Therefore, avoid using it in scripts where portability to other shells is a concern. – Diomidis Spinellis Aug 04 '16 at 08:30
  • 6
    It seems to me that a similar effect without bash builtins would be to use any other command which acts on a glob and fails if no files matched, such as ls: `if ls /tmp/*Files 2>&1 >/dev/null; then echo exists; fi` - maybe useful for code golf? Fails if there's a file named the same as the glob, which the glob shouldn't have matched, but if that's the case you probably have bigger problems. – Dewi Morgan May 15 '17 at 17:24
  • 9
    @DewiMorgan This is simpler: `if ls /tmp/*Files &> /dev/null; then echo exists; fi` – Clay Bridges Mar 29 '18 at 19:47
  • For details on `compgen`, see `man bash` or with `help compgen` – el-teedee Sep 08 '18 at 13:53
  • 1
    This just plain doesn't work for me. `ls dir/*.ext` shows output. `compgen dir/*.ext` shows no output and returns 1. Am I using it incorrectly? – jrwren Dec 07 '18 at 17:29
  • 3
    yeah, quote it or the filename wildcard will be pre-expanded. compgen "dir/*.ext" – Brian Chrisman Jan 03 '19 at 18:42
  • 1
    Doesn't work if the glob contains `{}`, for example `./dir/*.{ext1,ext2}`, while bash otherwise can expand it – z2s8 Aug 03 '20 at 09:41
  • might try extglob... ./dir/*.(ext1|ext2) – Brian Chrisman Aug 04 '20 at 04:18
  • Added a new answer for extended globs: https://stackoverflow.com/a/65217274/145400. About `Doesn't work if the glob contains {}`: Brace expansion is not working well with the `compgen` approach because brace expansion is not actually globbing! It's a separate mechanism. Maybe use extended globbing instead of brace expansion, as I've tried to show in my answer :-). – Dr. Jan-Philip Gehrcke Dec 09 '20 at 14:02
  • 3
    @DewiMorgan: your suggestion of: if ls /tmp/*Files 2>&1 >/dev/null; then echo exists; fi is bascially a good one, but the redirection won't work as written, you want: if ls /tmp/*Files >/dev/null 2>&1 ; then echo exists; fi – John Vincent Dec 13 '21 at 15:52
  • @JohnVincent Good catch! Though Clay Bridges' suggestion of &> is cleaner than my suggestion anyway, at least where supported. – Dewi Morgan Dec 14 '21 at 00:46
  • Might be more convenient to people as a single line: `compgen -G "/tmp/someFiles*" > /dev/null && echo "Some files exist."` – otocan Jun 14 '22 at 14:41
  • @BrianChrisman Your answer says, "Exit status is: 1 for no-match" etc., but `man bash` says "The return value is true unless an invalid option is supplied, or no matches were generated." Perhaps I'm using an newer version, but your answer doesn't work anymore. – lmat - Reinstate Monica Apr 12 '23 at 14:54
208

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
flabdablet
  • 3,565
  • 3
  • 22
  • 15
  • 12
    `find` seems to be exactly correct. It has no corner cases, since the shell isn't doing expansion (and passing an unexpanded glob to some other command), it's portable between shells (though apparently not all of the options you use are specified by POSIX), and it's faster than `ls -d glob*` (the previous accepted answer) becasue it stops when it reaches the first match. – Ken Bloom Nov 24 '10 at 14:54
  • 1
    Note that this answer may require a `shopt -u failglob` as these options seem to conflict somehow. – Calimo Jul 30 '14 at 13:42
  • The `find` solution will match a filename with no glob characters as well. In this case, that's what I wanted. Just something to be aware of though. – We Are All Monica Aug 19 '14 at 14:33
  • 1
    https://unix.stackexchange.com/questions/275637/limit-posix-find-to-specific-depth discusses how to replace the `-maxdepth` option for a POSIX find. – Ken Bloom Feb 21 '18 at 15:58
  • find is usually the resilient and portable answer when scripting, but for me, detecting a glob match is usually to prepare for *using* the glob, so I want all the existing shopt glob options and stick to the shell itself so I get the same results. – sondra.kinsey Oct 14 '18 at 09:11
  • The `find` solution did not work when there's a path in the pattern, but `stat` did. – not2savvy Jul 16 '21 at 08:45
  • The difference is because the `stat` solution has the shell in charge of expanding the glob, but the `find` solution passes it unexpanded to `find`, whose `-name` option is specifically designed to match against the filename (last pathname component) only. There are also options (`-path`, `-ipath`, `-regex`, others) for matching whole pathnames against a given pattern. None of these work quite the same way that shell globbing does; see the `find` manual for details. – flabdablet Jul 23 '21 at 11:07
  • 1
    The `-t` option of `stat` is not portable. On BSD it's used for formatting timestamp displays. – Mark Adler May 12 '23 at 17:11
33

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

el-teedee
  • 1,293
  • 1
  • 15
  • 27
Dan Bloch
  • 489
  • 5
  • 4
  • 17
    It may return a false positive if the glob is something like `*.[cC]` (there may be not `c` or `C` file, but a file called `*.[cC]`) or false negative if the first file expanded from that is for instance a symlink to an unexistent file or to a file in a directory you don't have access to (you way want to add a `|| [ -L "$1" ]`). – Stephane Chazelas Jun 13 '13 at 20:39
  • 1
    Interesting. Shellcheck reports that globbing only works with `-e`, when there are 0 or 1 matches. It doesn't work for multiple matches, because that would become `[ -e file1 file2 ]` and this would fail. Also see https://github.com/koalaman/shellcheck/wiki/SC2144 for the rationale and suggested solutions. – Thomas Praxl Feb 07 '20 at 05:58
  • Consider `[ -e "$1" ] || [ -L "$1" ]` -- as currently written this will be false if the glob _does_ have matches, but the first of those matches is a broken symlink. – Charles Duffy Aug 06 '22 at 13:50
27
#!/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
miku
  • 181,842
  • 47
  • 306
  • 310
  • 2
    To avoid a possible false “no matches” set `nullglob` instead of checking to see if a single result equals the pattern itself. Some patterns can match names that are exactly equal to the pattern itself (e.g. `a*b`; but not e.g. `a?b` or `[a]`). – Chris Johnsen May 30 '10 at 04:00
  • I suppose this fails on the *highly unlikely* chance that there's actually a file named like the glob. (e.g. somebody ran `touch '*py'`), but this does point me in another good direction. – Ken Bloom May 30 '10 at 04:01
  • I like this one as the most general version. – Ken Bloom May 30 '10 at 16:11
  • And also shortest. If you are only expecting one match, you can use `"$M"` as a shorthand for `"${M[0]}"`. Otherwise, well you already have the glob expansion in an array variable, so you're gtg for passing it to other things as a list, instead of making them re-expand the glob. – Peter Cordes Feb 27 '15 at 08:31
  • 1
    Nice. You can test M more quickly (less bytes and without spawning a `[` process) with `if [[ $M ]]; then ...` – Tobia Oct 06 '15 at 10:05
14

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
tripleee
  • 175,061
  • 34
  • 275
  • 318
Tegra Detra
  • 24,551
  • 17
  • 53
  • 78
8

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
NerdMachine
  • 155
  • 1
  • 3
  • 5
    That still doesn't fix the false-positive on filenames that have glob special-characters in them, like Stephane Chazelas points out for Dan Bloch's answer. (unless you monkey with nullglob). – Peter Cordes Feb 27 '15 at 05:47
  • 4
    You shoud avoid `-o` and `-a` in `test`/`[`. For instance, here, it fails if `$1` is `=` with most implementations. Use `[ -e "$1" ] || [ -L "$1" ]` instead. – Stephane Chazelas Aug 22 '15 at 18:22
5

I have yet another solution:

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

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

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
SaschaZorn
  • 142
  • 1
  • 2
  • 2
    Works except if the file is actually named 'glob*'. – Ian Kelling Sep 23 '16 at 02:23
  • does work for passing in glob as variable - gives "too many arguments" error when there is more than one match. "$(echo $GLOB)" is not returning a single string or at least it's not interpreted as single single thus the too many arguments error – DKebler Feb 01 '18 at 18:14
  • @DKebler : it should be interpreted as single string, because it is wrapped in double-quotes. – user1934428 May 24 '19 at 07:54
  • This will fail if the `nullglob` shell option is set, and it's _always_ unnecessarily slow (as `$(...)` involves forking off a new copy of the shell). – Charles Duffy Dec 08 '20 at 22:48
4

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
Community
  • 1
  • 1
queria
  • 66
  • 2
  • This works exactly like the version I already presented using stat; not sure how find is "easier" than stat. – flabdablet Aug 01 '14 at 00:41
  • 3
    Be aware that &> redirection is a bashism, and will quietly do the wrong thing in other shells. – flabdablet Aug 01 '14 at 00:42
  • This seems to be better than flabdablet's `find` answer because it accepts paths in the glob and it is more terse (doesn't require `-maxdepth` etc). It also seems better than his `stat` answer because it doesn't continue to do the extra `stat`ing on each additional glob match. I'd appreciate if anyone could contribute corner cases where this doesn't work. – drwatsoncode May 24 '16 at 18:29
  • 1
    After futher consideration, I would add `-maxdepth 0` because it allows more flexibility in adding conditions. e.g. assume I want to restrict the result to matching files only. I might try `find $glob -type f -quit` , but that would return true if the glob did NOT match a file, but did match a directory that *contained* a file (even recursively). In contrast `find $glob -maxdepth 0 -type f -quit` would only return true if the glob itself matched at least one file. Note that `maxdepth` does not prevent the glob from having a directory component. ( FYI `2>` is sufficient. no need for `&>`) – drwatsoncode May 24 '16 at 19:24
  • 2
    The point of using `find` in the first place is to avoid having the shell generate and sort a potentially huge list of glob matches; `find -name ... -quit` will match at most one filename. If a script relies on passing a shell-generated list of glob matches to `find`, invoking `find` achieves nothing but unnecessary process-startup overhead. Simply testing the resulting list directly for non-emptiness will be quicker and clearer. – flabdablet Feb 22 '18 at 03:54
4

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

M=(*py)
if [ -e ${M[0]} ]; then
  echo Found
else
  echo Not Found
fi
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Ken Bloom
  • 57,498
  • 14
  • 111
  • 168
  • 5
    Close, but what if you are matching `[a]`, have a file named `[a]`, but no file named `a`? I still like `nullglob` for this. Some might view this as pedantic, but we might as well be as fully correct as is reasonable. – Chris Johnsen May 30 '10 at 06:15
  • @sondra.kinsey That's wrong; the glob `[a]` should only match `a`, not the literal file name `[a]`. – tripleee Jun 10 '19 at 17:39
4

If you want to test if the files exist before iterating over them, you can use this pattern:

  for F in glob*; do
    if [[ ! -f $F ]]; then break; fi
    
    ...

  done

if the glob does not does not match anything, $F will be the non-expanded glob ('glob*' in this case) and if a file with the same name does not exist, it will skip the rest of the loop.

premek.v
  • 231
  • 4
  • 14
3

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.

Toby Speight
  • 27,591
  • 48
  • 66
  • 103
  • Why, with nullglob set, and the array possibly empty, can you not still test with `if [ -e "${scripts[0]}" ]...`? Are you also allowing for the possibility of shell option *nounset* set? – johnraff Dec 21 '18 at 06:18
  • @johnraff, yes, I normally assume `nounset` is active. Also, it might be (slightly) cheaper to test the string is non-empty than to check for a file's presence. Unlikely though, given that we've just performed a glob, meaning the directory contents should be fresh in the OS's cache. – Toby Speight Jan 09 '19 at 09:46
2
#!/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
Peter Lyons
  • 142,938
  • 30
  • 279
  • 274
2
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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
joseyluis
  • 537
  • 4
  • 10
  • 1
    The duplicate https://stackoverflow.com/questions/6363441/check-if-a-file-exists-with-wildcard-in-shell-script has a number of other non-Bash solutions, many of them horrible. – tripleee Jul 31 '20 at 07:45
  • You might want `[ -e "$1" ] || [ -L "$1" ]`; your current code treats a glob where the first match is a broken symlink, or a directory, or anything else except a file, as if it had no matches at all. – Charles Duffy Aug 06 '22 at 13:42
2

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 *.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Gilles Quénot
  • 173,512
  • 41
  • 224
  • 223
1

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.

Ryan C. Thompson
  • 40,856
  • 28
  • 97
  • 159
0

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

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Dr. Jan-Philip Gehrcke
  • 33,287
  • 14
  • 85
  • 130
0

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
0

Assuming you may want to do something with the files if they exist:

mapfile -t exists < <(find "$dirName" -type f -iname '*.zip'); [[ ${#exists} -ne 0 ]] && { echo "Zip files found" ; } || { echo "Zip files not found" ; }

You can then loop through the exists array if you need to do something with the files.

Isaac
  • 11
  • 6
  • In case it's not obvious, `find` checks for matches in all subdirectories. You can restrict it to only look in the current directory, but then using `find` is megadeath overkill. Just use one of the older answers which actually answer the question. – tripleee Feb 23 '23 at 13:23
-4

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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
jesjimher
  • 1,175
  • 1
  • 12
  • 17
  • Note that "plain" bash glob patterns aren't the only type bash supports -- it also has extglobs, which are comparable in expressiveness to regexes. The bigger problem is that [`ls` is unreliable for programmatic use](https://mywiki.wooledge.org/ParsingLs). – Charles Duffy Aug 06 '22 at 13:47
-5
[ `ls glob* 2>/dev/null | head -n 1` ] && echo true
Toby Speight
  • 27,591
  • 48
  • 66
  • 103
otocan
  • 824
  • 11
  • 16