27

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?

  • 7
    Add shopt -s nullglob before running the for loop. – cuonglm Oct 30 '15 at 14:07
  • @cuolnglm: spookily this results in ls returning the name of the executing script rather than an empty list on this RHEL5 box (bash 3.2.25) if I do FILES=ls *.zip; for fname in "${FILES}"... but it does work as expected with for fname in *.zip ; do.... – symcbean Oct 30 '15 at 14:12
  • 3
    Use for file in *.zip, not `ls ...`. @cuonglm's suggestion is so that *.zip expands to nothing when the pattern doesn't match any file. ls without arguments lists the current directory. – Stéphane Chazelas Oct 30 '15 at 14:16
  • @cuonglm Post that as an answer – Barmar Oct 30 '15 at 19:28
  • 1
    This question discusses why parsing the output of ls is generally to be avoided: Why not parse ls?; also see the link near the top of that page to BashGuide's ParsingLs article. – PM 2Ring Nov 1 '15 at 11:03
28

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 '%s\n' "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.

  • shopt -s nullglob did not work for me on Ubuntu 17.04, but [ -f "$fname" ] || continue worked well. – koppor Sep 2 '17 at 12:24
  • @koppor It sounds like you aren't actually using bash. – chepner Sep 2 '17 at 12:55
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 %s\n" "$@"    #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 %s\n" "$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 *
2

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.

  • Which recurses through subdirectories, and I want to invoke a bash function (not script) for the matches. – symcbean Oct 30 '15 at 15:15
  • @symcbean I've edited to limit to single dir and handle bash functions – Dani_l Nov 1 '15 at 10:34
-3

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"

  • 1
    I think it is a bad idea to add one more process (grep) rather than trying to fix the issue by using a better tool (find) or changing the relevant setting for the current solution (with shopt -s nullglob) – Thomas Baruchel Nov 2 '15 at 10:05
  • 1
    According to the OP's comment on their original post the shopt -s nullglob does not work. I tried find while verifying my answer and it kept failing. I think because of the export thing Dani said. – Kip K Nov 2 '15 at 15:48

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.