293

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,030
  • 21
  • 100
  • 124
Ken Bloom
  • 55,158
  • 13
  • 108
  • 167
  • 3
    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
  • 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

22 Answers22

254

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,030
  • 21
  • 100
  • 124
Brian Chrisman
  • 3,204
  • 1
  • 14
  • 15
  • 14
    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
  • 8
    @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
  • 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
  • 1
    @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
194

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,335
  • 3
  • 21
  • 15
  • 11
    `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
  • Since when is `stat` "Bash only"? – Jesin Oct 19 '17 at 00:01
  • 1
    Since somebody else decided to edit my answer to make it say that, apparently. – flabdablet Oct 20 '17 at 16:58
  • 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
27

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,233
  • 1
  • 16
  • 25
Dan Bloch
  • 421
  • 5
  • 4
  • 15
    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
  • 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
26
#!/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
  • 172,072
  • 46
  • 300
  • 307
  • 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
11

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
  • 158,107
  • 27
  • 234
  • 292
James Andino
  • 22,903
  • 15
  • 52
  • 75
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
6

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,030
  • 21
  • 100
  • 124
SaschaZorn
  • 144
  • 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
  • 51
  • 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,030
  • 21
  • 100
  • 124
Ken Bloom
  • 55,158
  • 13
  • 108
  • 167
  • 4
    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
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
  • 25,191
  • 47
  • 61
  • 93
  • 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
  • 137,901
  • 29
  • 269
  • 269
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,030
  • 21
  • 100
  • 124
joseyluis
  • 517
  • 4
  • 9
  • 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
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,030
  • 21
  • 100
  • 124
Gilles Quenot
  • 154,891
  • 35
  • 213
  • 206
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
  • 38,976
  • 28
  • 92
  • 152
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,030
  • 21
  • 100
  • 124
Dr. Jan-Philip Gehrcke
  • 29,627
  • 13
  • 79
  • 123
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
done
  • 6,370
  • 27
  • 49
0

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
  • 193
  • 5
  • 13
-1
(ls glob* &>/dev/null && echo Files found) || echo No file found
Peter Mortensen
  • 30,030
  • 21
  • 100
  • 124
Damodharan R
  • 1,473
  • 7
  • 10
  • 5
    Would also return false if there are directories matching `glob*` and for instance you don't have the write to list those directories. – Stephane Chazelas Jun 13 '13 at 20:42
-1

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.

Peter Mortensen
  • 30,030
  • 21
  • 100
  • 124
Pourko
  • 109
  • 3
  • This most certainly doesn't work for a useful, reliable and robust value of "work". `[ -f foo* ]` if you have files `foo1.txt` and `foo2.txt` generates `[ -f foo1.txt foo2.txt ]`, which is invalid syntax for the `[` command. And then in the zero-match case, if `nullglob` is enabled it just runs `[ -f ]`, which returns true (because it's equivalent to `[ -n -f ]`) even though there are no files! – Charles Duffy May 25 '22 at 23:08
  • Now, if `nullglob` _isn't_ enabled, then yes, it'll distinguish between the 0-match case and the 1-match case reliably; but writing code that fails in the 2-or-more-matches case is foolish when nothing compels you to do so. – Charles Duffy May 25 '22 at 23:10
-2
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.

Florian Diesch
  • 1,009
  • 10
  • 16
  • 2
    This will give the wrong answer if a pattern like `[a]` is used when the file `[a]` is present and the file `a` is absent. It will say “found” even though the only file it should match, `a`, is not actually present. – Chris Johnsen May 30 '10 at 08:25
  • 1
    This version should work in an ordinary POSIX /bin/sh (without bashisms), and in the case that I'm need it for, the glob doesn't have brackets anyway, and I don't need to worry about cases that are terribly pathological. But I guess that there is no one good way to do test whether any files match a glob. – Ken Bloom May 30 '10 at 16:11
-2

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,030
  • 21
  • 100
  • 124
jesjimher
  • 1,117
  • 1
  • 11
  • 17
-4
[ `ls glob* 2>/dev/null | head -n 1` ] && echo true
Toby Speight
  • 25,191
  • 47
  • 61
  • 93
otocan
  • 581
  • 8
  • 14