69

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?

symcbean
  • 5,008
  • 2
  • 25
  • 37
  • 9
    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
  • 5
    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`?](http://unix.stackexchange.com/questions/128985/why-not-parse-ls); also see the link near the top of that page to BashGuide's [ParsingLs](http://mywiki.wooledge.org/ParsingLs) article. – PM 2Ring Nov 01 '15 at 11:03
  • @cuonglm: you were first to point out I should be using nullglob (getting the scriptname back was a result of the glob being expanded before the ls command was executed, hence 'ls *.zip' became simply 'ls'). Can you reporst your answer as a coment and I'll tick it off. – symcbean Nov 01 '15 at 21:59
  • @symcbean: [chepner's answer](http://unix.stackexchange.com/a/240004/38906) did. Don't use `ls`, just use a plain glob. – cuonglm Nov 02 '15 at 01:25
  • 1
    Possible duplicate of [Why does my shell script choke on whitespace or other special characters?](https://unix.stackexchange.com/questions/131766/why-does-my-shell-script-choke-on-whitespace-or-other-special-characters) – mgutt Aug 10 '19 at 09:56

4 Answers4

88

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.

Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501
chepner
  • 7,341
  • 1
  • 26
  • 27
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.

Dani_l
  • 4,720
  • 1
  • 18
  • 34
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 *
mikeserv
  • 57,448
  • 9
  • 113
  • 229
-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"

Kip K
  • 123
  • 4
  • 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 02 '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 02 '15 at 15:48