Test whether a glob has any matches in bash

2019-01-01 14:33发布

问题:

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

回答1:

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.

UPDATE: Example usage requested.

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


回答2:

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


回答3:

#!/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


回答4:

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



回答5:

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


回答6:

I have yet another solution:

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

This works nicely for me. Are there some corner cases I miss?



回答7:

To simplify The MYYN\'s answer somewhat, based on his idea:

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


回答8:

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


回答9:

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


回答10:

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.



回答11:

I did not see this answer, so I thought I\'d put it out there:

set -- glob*
[ -f \"$1\" ] && echo \"found $@\"


回答12:

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.



回答13:

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


回答14:

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.



回答15:

#!/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


回答16:

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



回答17:

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: ksh 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.



回答18:

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.



标签: bash glob