Bash – Test if any files that do not match a specific pattern exist in a directory

bashfileswildcards

I am having much difficulty with this. I am trying to test if there exists files in a directory that DO NOT match a given pattern, returning true or false.

In this case, files in $dir that don't start with an underscore, _.
I figured I would try if [ -f $dir/!(_*) ], if [ $dir -f -name ! _* ]

or
if ls $dir/!(_*) 1> /dev/null 2>&1

but it would always say Too many arguments
or syntax error near unexpected token "("

Best Answer

names=( "$dir"/[!_]* )

if [ -e "${names[0]}" ]; then
    echo 'there are filenames that do not start with underscore'
    printf '%d of them\n' "${#names[@]}"
fi

Or, for /bin/sh (and bash for that matter):

set -- "$dir"/[!_]*

if [ -e "$1" ]; then
    echo 'there are filenames that do not start with underscore'
    printf '%d of them\n' "$#"
fi

In short, expand the appropriate globbing pattern and test whether it matched something that exists.

The [!_] pattern will match any character that is not an underscore. It is similar to the regular expression [^_], but filename globbing patterns use ! instead of ^ to negate a character class.

When the pattern doesn't match, by default, it will remain unexpanded, so that's why we use the -e test to make sure the first thing in the match list exists. We can't really test the length of the returned list because if the length is 1, it still may have matched nothing (unless you have the nullglob shell option set in bash).

It becomes a tiny bit trickier if you want to test for regular files specifically, since globbing patterns matches any name (directories, regular files, and all other types of files). But this would do it:

names=( "$dir"/[!_]* )

while [ "${#names[@]}" -gt 0 ] && [ ! -f "${names[0]}" ]; do
    names=( "${names[@]:1}" )
done

if [ -f "${names[0]}" ]; then
    echo 'there are at least one regular file here (or a symlink to one)'
    echo 'whose filename does not start with underscore'
fi

Or, for /bin/sh:

set -- "$dir"/[!_]*

while [ "$#" -gt 0 ] && [ ! -f "$1" ]; do
    shift
done

if [ -f "$1" ]; then
    echo 'there are at least one regular file here (or a symlink to one)'
    echo 'whose filename does not start with underscore'
fi

This approach would also detect a symbolic link to a regular file whose name does not start with an underscore.

The looping is necessary to shift off any names of files that are not regular files (e.g. directory names) that we may have matched.

In the zsh shell, you could use the pattern "$dir"/[^_]*(.) which is guaranteed to only match regular files (if it matches anything).


For more complex patterns, you could naively compare the number of items matched with the number of items matched by *. If they differ, there are names that does not match the complex pattern.

In bash, you may use the extended globbing pattern !(PATTERN) after enabling the extglob shell option with shopt -s extglob. The general form is !(pattern1|pattern2|pattern3|etc). You would still have to examine the result of the expansion as above to see whether it expanded to anything.

The example with names not starting with an underscore may use the !(_*) extended globbing pattern, but note that !(_)* would not work as that would match every visible name, just like * would.