14

Stupidly, I had been using a condition like this as part of a script:

if [ $(ls FOO* 2> /dev/null) ] # if files named "FOO*" were downloaded 
then
    echo "Files found" 
    # ... process and email results
else
    echo "Not found"
    # ... email warning that no files were found (against expectations)
fi

That works for zero and one files named FOO*, but fails if there are more than one. From logs I found several different error messages stemming from this:

[: FOO_20131107_082920: unary operator expected
[: FOO_20131108_070203: binary operator expected
[: too many arguments

My question is: what is the correct way to check, in a Bash if condition, whether one or more files whose name begins with FOO exist?

GNU bash, version 4.2.25(1)-release (x86_64-pc-linux-gnu)

Jonik
  • 1,480

3 Answers3

24

This happens because your command substitution for ls outputs whitespace, and it ultimately undergoes word splitting before being passed to [. A less breakable way would be to put the files in an array, and then check that the array has at least one member.

shopt -s nullglob

files=( FOO* )
if (( ${#files[@]} )); then
    # there were files
fi

This works because (( by default returns true if the value does not equal 0, and ${#files[@]} gets the number of items in the array (which will be >0 if there are files matching the glob).

You could also do something like this, as long as nullglob is not set:

if ls FOO* >/dev/null 2>&1; then
    # there were files
fi

This just checks the exit code of ls, which will be 1 if you passed a filename that doesn't exist (the literal FOO*, if nothing is matched (unless, of course, you are evil and there is a file named FOO*, in which case it will return 0 :-) )).

Note that both of these also match directories. If you really only want to match regular files, you need to test that:

for file in FOO*; do
    if [[ -f $file ]]; then
        # file found, do some stuff and break
        break
    fi
done
Chris Down
  • 125,559
  • 25
  • 270
  • 266
  • Thanks, this works. I've never seen shopt -s nullglob before but it makes the difference here. I just hope it doesn't trigger any problems in the remainder of the script (at least it does weird things to normal Bash shell). Can you turn that mode off later in the script? – Jonik Nov 12 '13 at 10:07
  • 1
    @Jonik Sure, just call shopt -u nullglob. It could cause problems if your script does some things that rely on POSIX null globbing behaviour. help shopt may also give you some useful information about it. – Chris Down Nov 12 '13 at 10:10
  • if you have files FOO_1.out and FOO_1.arc and you only want to match arc files then this doesn't work and files=( FOO*.arc ) will return 1 even you only have a FOO_1.out which is not desired – Brian Wiley Jul 24 '22 at 12:31
8

The multi-line answers above didn't suit my desire for a one-line solution and not modifying the environment. Here is a generic one-liner that may work for you:

echo $(ls FOO* 2>/dev/null | wc -w)

the /dev/null is because ls throws an error if there's no file. This just ignores ls and counts the number of files found based on the number of "words" (really filenames). You can the use it in a simple if statement:

if [ 0 -lt $(ls FOO* 2>/dev/null | wc -w) ]; then
 echo "Some FOO is there."
fi

This approach has the flexibility of not just checking for whether any matching files exist but also optionally checking for how many matching files exist (just change 0 and/or -lt) and then you proceed further on that basis.

I haven't checked against files/folders that have wildcards or spaces in them; I suspect the above method would give wrong counts on spaces in particular, which is only an issue if an accurate count is required beyond 0. One fix I tried for this is to use $(ls -l FOO* 2>/dev/null | wc -l), which will break the list down by lines and therefore should be more accepting of white space.

  • I realise that my approach is logically identical to the Chris Down's answer. it's just a different way to create the array and check its length. My approach allows factoring in any condition that ls can use to filter return results besides filename matching. – intech_guy Oct 31 '14 at 03:39
  • 2
    Your answer is different from @ChrisDown's because you parse ls output whereas he does not (he only checks its exit status). Due to the complexities involved with parsing ls output, the fact that ChrisDown avoids it entirely makes his answer more robust and correct than yours, even if yours will work in the many cases where "reasonable" file names (i.e. not containing whitespace) are concerned. – jw013 Jan 23 '15 at 18:17
  • 1
    I like this solution's readability. – chehsunliu Apr 10 '19 at 09:36
0

There is another option, using case,

FILES=FOO*
FILES=$(echo $FILES)
case $FILES in FOO\*) echo "Not found" ;; esac

This works as case expects only one argument to test.

xae
  • 2,021