bash iterate file list, except when empty

I thought this would be simple – but it is proving more complex than I expected.

I want to iterate through all the files of a particular type in a directory, so I write this:

#!/bin/bash

for fname in *.zip ; do
   echo current file is ${fname}
done

This works as long as there is at least one matching file in the directory. However if there are no matching files, I get this:

current file is *.zip

I then tried:

#!/bin/bash

FILES=`ls *.zip`
for fname in "${FILES}" ; do
    echo current file is ${fname}
done

While the body of the loop does not execute when there are no files, I get an error from ls:

ls: *.zip: No such file or directory

How do I write a loop which cleanly handles no matching files?

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

In bash, you can set the nullglob option so that a pattern that matches nothing “disappears”, rather than treated as a literal string:

shopt -s nullglob
for fname in *.zip ; do
   echo "current file is ${fname}"
done

In POSIX shell script, you just verify that fname exists (and at the same time with [ -f ], check it is a regular file (or symlink to regular file) and not other types like directory/fifo/device…):

for fname in *.zip; do
    [ -f "$fname" ] || continue
    printf '%sn' "current file is $fname"
done

Replace [ -f "$fname" ] with [ -e "$fname" ] || [ -L "$fname ] if you want to loop over all the (non-hidden) files whose name ends in .zip regardless of their type.

Replace *.zip with .*.zip .zip *.zip if you also want to consider hidden files whose name ends in .zip.

Method 2

set ./*                               #set the arg array to glob results
${2+":"} [ -e "$1" ] &&               #if more than one result skip the stat "$1"
printf "current file is %sn" "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="85a1c5">[email protected]</a>"    #print the whole array at once

###or###

${2+":"} [ -e "$1" ] &&               #same kind of test
for    fname                          #iterate singly on $fname var for array
do     printf "file is %sn" "$fname" #print each $fname for each iteration
done

In a comment here you mention invoking a function…

file_fn()
    if     [ -e "$1" ] ||               #check if first argument exists
           [ -L "$1" ]                  #or else if it is at least a broken link
    then   for  f                       #if so iterate on "$f"
           do : something w/ "$f"
           done
    else   command <"${1-/dev/null}"    #only fail w/ error if at least one arg
    fi

 file_fn *

Method 3

Use find

export -f myshellfunc
find . -mindepth 1 -maxdepth 1 -type f -name '*.zip' -exec bash -c 'myshellfunc "$0"' {} ;

You MUST export your shell function with export -f for this to work.
Now find executes bash which executes your shell function, and remains at the current dir level only.

Method 4

Instead of:

FILES=`ls *.zip`

Try:

FILES=`ls * | grep *.zip`

This way if ls fails (which it does in your case) it will grep the failed output and return as a blank variable.

current file is      <---Blank Here

You can add some logic to this to make it return “No File Found”

#!/bin/bash

FILES=`ls * | grep *.zip`
if [[ $? == "0" ]]; then
    for fname in "$FILES" ; do
        echo current file is $fname
    done
else
    echo "No Files Found"
fi

This way if the previous command succeeded (exited with a 0 value) then it will print the current file, otherwise it would print “No Files Found”


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x