29

I am trying to write an if statement to test whether there are any files matching a certain pattern. If there is a text file in a directory it should run a given script.

My code currently:

if [ -f /*.txt ]; then ./script fi

Please give some ideas; I only want to run the script if there is a .txt in the directory.

  • 3
    Are you sure "the directory" is supposed to be /? Also, you're missing a semicolon before fi. – depquid Jun 13 '13 at 15:44
39
[ -f /*.txt ]

would return true only if there's one (and only one) non-hidden file in / whose name ends in .txt and if that file is a regular file or a symlink to a regular file.

That's because wildcards are expanded by the shell prior to being passed to the command (here [).

So if there's a /a.txt and /b.txt, [ will be passed 5 arguments: [, -f, /a.txt, /b.txt and ]. [ would then complain that -f is given too many arguments.

If you want to check that the *.txt pattern expands to at least one non-hidden file (regular or not):

shopt -s nullglob
set -- *.txt
if [ "$#" -gt 0 ]; then
  ./script "$@" # call script with that list of files.
fi
# Or with bash arrays so you can keep the arguments:
files=( *.txt )
# apply C-style boolean on member count
(( ${#files[@]} )) && ./script "${files[@]}"

shopt -s nullglob is bash specific, but shells like ksh93, zsh, yash, tcsh have equivalent statements.

Note that it finds those files by reading the content of the directory, it doesn't try and access those files at all which makes it more efficient than solutions that call commands like ls or stat on that list of files computed by the shell.

The standard sh equivalent would be:

set -- [*].txt *.txt
case "$1$2" in
  ('[*].txt*.txt') ;;
  (*) shift; script "$@"
esac

The problem is that with Bourne or POSIX shells, if a pattern doesn't match, it expands to itself. So if *.txt expands to *.txt, you don't know whether it's because there's no .txt file in the directory or because there's one file called *.txt. Using [*].txt *.txt allows to discriminate between the two.

  • [ -f /*.txt ] is quite fast in comparison to compgen. – Daniel Böhmer Nov 23 '16 at 22:06
  • @DanielBöhmer [ -f /*.txt ] would be wrong, but in my test on a directory that contains 3425 files, 94 of which are non-hidden txt files, compgen -G "*.txt" > /dev/null 2>&1 appear to be as fast as set -- *.txt; [ "$#" -gt 0 ] (20.5 seconds for both when repeated 10000 times in my case). – Stéphane Chazelas Nov 24 '16 at 9:51
11

You could always use find:

find . -maxdepth 1 -type f -name "*.txt" 2>/dev/null | grep -q . && ./script

Explanation:

  • find . : search the current directory
  • -maxdepth 1: do not search subdirectories
  • -type f : search only regular files
  • name "*.txt" : search for files ending in .txt
  • 2>/dev/null : redirect error messages to /dev/null
  • | grep -q . : grep for any character, will return false if no characters found.
  • && ./script : Execute ./script only if the previous command was successful (&&)
  • 2
    find only returns false if it has trouble looking for files, not if it doesn't find any file. You want to pipe the output to grep -q . to check if it finds something. – Stéphane Chazelas Jun 13 '13 at 16:57
  • @StephaneChazelas you're quite right of course. Weird though, I'd tested it and it seemed to work. Must have done something strange because it doesn't any more. When will find "have trouble finding files"? – terdon Jun 13 '13 at 17:10
  • @terdon, like when some directory is inaccessible, or I/O errors or any error returned by any system call it makes. In that case, try after chmod a-x .. – Stéphane Chazelas Jun 13 '13 at 17:33
7

Here's a one liner to do it:

$ ls
file1.pl  file2.pl

files exist

$ stat -t *.pl >/dev/null 2>&1 && echo "file exists" || echo "file doesn't exist"
file exists

files don't exist

$ stat -t -- *.txt >/dev/null 2>&1 && echo "file exists" || echo "file don't exist"
file don't exist

This approach makes use of the || and && operators in bash. These are the "or" and "and" operators.

So if the stat command returns a $? equal to 0 then the first echo is called, if it returns a 1, then the second echo is called.

return results from stat

# a failure
$ stat -t -- *.txt >/dev/null 2>&1
$ echo "$?"
1

# a success
$ stat -t -- *.pl >/dev/null 2>&1
$ echo "$?"
0

This question is extensively covered over on stackoverflow:

  • 1
    Why use the non standard stat when ls -d can do the same? – Stéphane Chazelas Jun 13 '13 at 17:03
  • I thought ls -d lists a directory? Didn't seem to work when I just tried listing a directory with files in it ls -d *.pl for example. – slm Jun 13 '13 at 17:36
  • You can replace the statement to the left of && by ls *.txt and it will work as well. Make sure you send the stdout and stderr to /dev/null as suggested by @slm. – unxnut Jun 13 '13 at 17:50
  • 1
    If you use ls *.txt and there are no files present within the directory this will return a $? = 2, which will still work with the if then, but this was one of my reasons for choosing stat over ls. I wanted a 0 for success, and a 1 for a failure. – slm Jun 13 '13 at 17:57
  • ls -d is to list directories instead of their content. So ls -d just does the lstat on the file, just like GNU stat does. What non-zero exit status commands return on failure is system specific, it makes little sense to make assumptions on them. – Stéphane Chazelas Jun 13 '13 at 18:58
7

A possible solution is also Bash builtin compgen. That command returns all possible matches for a globbing pattern and has an exit code indicating whether any files matched.

compgen -G "/*.text" > /dev/null && ./script

I found this question while looking for solutions that are faster though.

  • 1
    Good find! If you're in a multi-byte locale, you can improve it slightly with LC_ALL=C compgen -G "*.txt" > /dev/null. – Stéphane Chazelas Nov 24 '16 at 9:51
4

As Chazelas points out, your script would fail if wildcard expansion matches more than one file.

However, there is a trick I use (even I don't like it very much) to get around:

PATTERN=(/*.txt)
if [ -f ${PATTERN[0]} ]; then
...
fi

How it works?

Wildcard expansion will match an array of filenames, we get the first one if there are some, otherwise null if no match.

  • IMO this is the least bad answer here. They all seem pretty horrible though, as though a basic feature is missing from the language. – plugwash Dec 27 '15 at 18:29
  • @plugwash it's intentional... *nix shell scripts have some basic flow control and a few other odds and ends but at the end of the day it's job is to glue together other commands. If bash sucks... it's because the commands you use from it suck – cb88 Aug 11 '16 at 20:45
  • 2
    That's the wrong logic (and you're missing quotes). That checks if the first matching file is a regular file. It may well be a non-regular files but there may be several other .txt files that are of type regular. Try for instance after mkdir a.txt; mkfifo b.txt; echo regular > c.txt. – Stéphane Chazelas Nov 24 '16 at 9:14
1

Simple as:

cnt=`ls \*.txt 2>/dev/null | wc -l`
if [ "$cnt" != "0" ]; then ./script fi

wc -l counts the lines in the expanded wildcard.

0

I like the previous array solution, but that could become wasteful with large numbers of files - the shell would use a great deal of memory to build the array, and only the first element would ever be tested.

Here is an alternate structure that I tested yesterday:

$ cd /etc; if [[ $(echo * | grep passwd) ]];then echo yes;else echo no;fi yes $ cd /etc; if [[ $(echo * | grep password) ]];then echo yes;else echo no;fi no

The exit value from the grep appears to be determining the path through the control structure. This also tests with regular expressions rather than shell patterns. Some of my systems have the "pcregrep" command which allows much more sophisticated regex matches.

(I did edit this answer to remove an "ls" in the command substitution after reading the above criticism for parsing it.)

-2

if you want to use an if clause, evaluate the count:

if (( `ls *.txt 2> /dev/null|wc -l` ));then...

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.