74

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?

8
  • 9
    Add shopt -s nullglob before running the for loop.
    – cuonglm
    CommentedOct 30, 2015 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
    CommentedOct 30, 2015 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.CommentedOct 30, 2015 at 14:16
  • 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
    CommentedNov 1, 2015 at 11:03
  • 1
    Possible duplicate of Why does my shell script choke on whitespace or other special characters?
    – mgutt
    CommentedAug 10, 2019 at 9:56

4 Answers 4

94

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.

5
  • 3
    shopt -s nullglob did not work for me on Ubuntu 17.04, but [ -f "$fname" ] || continue worked well.
    – koppor
    CommentedSep 2, 2017 at 12:24
  • 4
    @koppor It sounds like you aren't actually using bash.
    – chepner
    CommentedSep 2, 2017 at 12:55
  • 1
    +1 for a POSIX solution.CommentedDec 19, 2020 at 17:24
  • Note that this comes with an annoying downside outside of for loops: If for example in your current dir there are NO .txt files, a simple ls *.txt will now fall back to ls thus list ALL files of the directory.
    – phil294
    CommentedMay 25, 2024 at 16:33
  • shopt -u nullglob after the loop.
    – chepner
    CommentedMay 26, 2024 at 13:01
3
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 * 
0
    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.

    2
    • Which recurses through subdirectories, and I want to invoke a bash function (not script) for the matches.
      – symcbean
      CommentedOct 30, 2015 at 15:15
    • @symcbean I've edited to limit to single dir and handle bash functions
      – Dani_l
      CommentedNov 1, 2015 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"

    2
    • 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)CommentedNov 2, 2015 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
      CommentedNov 2, 2015 at 15:48

    You must log in to answer this question.

    Start asking to get answers

    Find the answer to your question by asking.

    Ask question

    Explore related questions

    See similar questions with these tags.