1

I am trying to loop through files of a list of specified extensions with a bash script. I tried the solution given at Matching files with various extensions using for loop but it does not work as expected. The solution given was:

for file in "${arg}"/*.{txt,h,py}; do

Here is my version of it:

for f in "${arg}"/*.{epub,mobi,chm,rtf,lit,djvu}
    do
        echo "$f"
    done

When I run this in a directory with an epub file in it, I get:

/*.epub
/*.mobi
/*.chm
/*.rtf
/*.lit
/*.djvu

So I tried changing the for statement:

for f in "${arg}"*.{epub,mobi,chm,rtf,lit,djvu}

Then I got:

089281098X.epub
*.mobi
*.chm
*.rtf
*.lit
*.djvu

I also get the same result with:

for f in *.{epub,mobi,chm,rtf,lit,djvu}

So it seems that the "${arg}" argument is unnecessary.

Although either of these statements finds files of the specified extensions and can pass them to a program, I get read errors from the unresolved *. filenames.

I am running this on OS X Mountain Lion. I was aware that the default bash shell was outdated so I upgraded it from 3.2.48 to 4.2.45 using homebrew to see if this was the problem. That didn't help so I am wondering why I am getting these unexpected results. Is the given solution wrong or is the OS X bash shell somehow different from the *NIX version? Is there perhaps an alternate way to accomplish the same thing that might work better in the OS X bash shell?

Community
  • 1
  • 1
hmj6jmh
  • 630
  • 5
  • 13
  • Hi. Could you explain in more detail what you're trying to do? Since the code is broken, it's hard to use *that* as a description of what you want. – jpaugh Feb 19 '14 at 01:40

4 Answers4

2

This may be a BASH 4.2ism. It does not work in my BASH which is still 3.2. However, if you shopt -s extglob, you can use *(...) instead:

shopt -s extglob
for file in *.*(epub|mobi|chm|rtf|lit|djvu)
do
    ...
done

@David W.: shopt -s extglob for f in .(epub|mobi|chm|rtf|lit|djvu) results in: 089281098X.epub @kojiro: arg=. shopt -s nullglob for f in "${arg}"/.{epub,mobi,chm,rtf,lit,djvu} results in: ./089281098X.epub shopt -s nullglob for f in "${arg}".{epub,mobi,chm,rtf,lit,djvu} results in: 089281098X.epub So all of these variations work but I don't understand why. Can either of you explain what is going on with each variation and what ${arg} is doing? I would really like to understand this so I can increase my knowledge. Thanks for the help.

In mine:

for f in *.*(epub|mobi|chm|rtf|lit|djvu)

I didn't include ${arg} which expands to the value of $arg. The *(...) matches the pattern found in the parentheses which is one of any of the series of extensions. Thus, it matches *.epub.

Kojiro's:

arg=. 
shopt -s nullglob 
for f in "${arg}"/*.{epub,mobi,chm,rtf,lit,djvu}

Is including $arg and the slash in his matching. Thus, koriro's start with ./ because that's what they are asking for.

It's like the difference between:

echo *

and

echo ./*

By the way, you could do this with the other expressions too:

echo *.*(epub|mobi|chm|rtf|lit|djvu)

The shell is doing all of the expansion for you. It's really has nothing to do with the for statement itself.

David W.
  • 105,218
  • 39
  • 216
  • 337
  • +1 Ah yes, the lovely `extglob`. I had forgotten it had this expression. – kojiro Feb 19 '14 at 02:50
  • @David W.: Thanks for the explanantion. BTW `echo *.*(epub|mobi|chm|rtf|lit|djvu)` gives an error: `-bash: syntax error near unexpected token '('`. I don't know why. – hmj6jmh Feb 19 '14 at 16:20
  • You have `shopt -s extglob` set? For some reason, Bash doesn't do extended blobbing automatically. – David W. Feb 19 '14 at 16:38
0

A glob has to expand to an existing, found name, or it is left alone with the asterisk intact. If you have an empty directory, *.foo will expand to *.foo. (Unless you use the nullglob Bash extension.)

The problem with your code is that you start with an arg, $arg, which is apparently empty or undefined. So your glob, ${arg}/*.epub expands to /*.epub because there are no files ending in ".epub" in the root directory. It's never looking in the current directory. For it to do that, you'd need to set arg=. first.

In your second example, the ${arg}*.epub does expand because $arg is empty, but the other files don't exist, so they continue not to expand as globs. As I hinted at before, one easy workaround would be to activate nullglob with shopt -s nullglob. This is bash-specific, but will cause *.foo to expand to an empty string if there is no matching file. For a strict POSIX solution, you would have to filter out unexpanded globs using [ -f "$f" ]. (Then again, if you wanted POSIX, you couldn't use brace expansion either.)

kojiro
  • 74,557
  • 19
  • 143
  • 201
  • @David W.: `shopt -s extglob` `for f in *.*(epub|mobi|chm|rtf|lit|djvu)` results in: `089281098X.epub` @kojiro: `arg=.` `shopt -s nullglob` `for f in "${arg}"/*.{epub,mobi,chm,rtf,lit,djvu}` results in: `./089281098X.epub` `shopt -s nullglob` `for f in "${arg}"*.{epub,mobi,chm,rtf,lit,djvu}` results in: `089281098X.epub` So all of these variations work but I don't understand why. Can either of you explain what is going on with each variation and what `${arg}` is doing? I would really like to understand this so I can increase my knowledge. Thanks for the help. – hmj6jmh Feb 19 '14 at 06:01
  • See the appending on my original answer. – David W. Feb 19 '14 at 13:27
0

To summarize, the best solutions are to use (most intuitive and elegant):

shopt -s extglob
for f in *.*(epub|mobi|chm|rtf|lit|djvu)

or, in keeping with the original solution given in the referenced thread (which was wrong as stated):

shopt -s nullglob
for f in "${arg}"*.{epub,mobi,chm,rtf,lit,djvu}
hmj6jmh
  • 630
  • 5
  • 13
-1

This should do it:

for file in $(find ./ -name '*.epub' -o -name '*.mobi' -o -name '*.chm' -o -name '*.rtf' -o -name '*.lit' -o -name '*.djvu'); do
  echo $file
done
Sammitch
  • 30,782
  • 7
  • 50
  • 77
  • 2
    -1 Parsing the output of `find` is almost as [bad as parsing `ls`](http://mywiki.wooledge.org/ParsingLs). It will break if any of the found files have whitespace in their names. Plus, if `find` is the right answer, then just do `find ./ -name '*.epub' -name …` and let find do the printing. – kojiro Feb 19 '14 at 02:03
  • @kojiro 1. Are you kidding me? The only solution offered in that article is to use a glob. Not exactly the most flexible solution, particularly if you need to work on subdirectories. 2. OP has a for loop for "no reason" go complain at them. I'm operating under the assumption that the for loop is necessary for whatever they *actually* want to accomplish. – Sammitch Feb 19 '14 at 17:28
  • I stand by what I said – looping over a wordsplit command substitution is rarely a good idea. 1. A glob is extremely flexible, especially with extensions like nullglob, extglob and globstar, but even without them it's not clear OP wanted to recurse. 2. Again, if `find` is the right answer, the entire answer can be expressed with `find`. If `find … -print` isn't right, `find … -exec` is. Barring that, `find … -print0 | xargs -0 …`. – kojiro Feb 19 '14 at 18:32