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