How can I do foreach *.mp3 file recursively in a bash script?

13,637

Solution 1

Too many of these answers use shell expansion to store the results of a find. This is not something you should do lightly.

Let's say I have 30,000 songs, and the titles of these songs average around 30 characters. Let's not even get into the white space problem for now.

My find will return over 1,000,000 characters, and it's very likely that my command line buffer isn't that big. If I did something like this:

for file in $(find -name "*.mp3")
do
    echo "some sort of processing"
done

The problem (besides the white space in file names) is that your command line buffer will simply drop off the overflow from the find. It may even fail absolutely silently.

This is why the xargs command was created. It makes sure that the command line buffer never overflows. It will execute the command following the xargs as many times as necessary to protect the command line buffer:

$ find . -name "*.mp3" | xargs ...

Of course, using xargs this way will still choke on white space, but modern implementations of xargs and find have a way of handling this issue:

$ find . -name "*.mp3 -print0 | xargs --null ...

If you can guarantee that file names won't have tabs or \n (or double spaces) in them, piping a find into a while loop is better:

find . -name "*.mp3" | while read file
do

The pipeline will send the files to the while read before the command line buffer is full. Even better, the read file reads in an entire line and will put all items found in that line into $file. It isn't perfect because the read still breaks on white space so file names such as:

I will be in \n your heart in two lines.mp3
I   love song names with     multiple spaces.mp3
I \t have \t a \t thing \t for \t tabs.mp3.

Will still fail. The $file variable aill see them as:

I will be in 
your heart in two lines.mp3
I love song names with multiple spaces.mp3
I have a thing for tabs.mp3.

In order to get around this problem, you have to use find ... -print0 to use nulls as input dividers. Then either change IFS to use nulls, or use the -d\0 parameter in the while read statement in BASH shell.

Solution 2

There are lots of ways to skin this cat. I would use a call to the find command myself:

for file in $(find . -name '*.mp3') do
  echo $file
  TITLE=$(id3info "$file" | grep '^=== TIT2' | sed -e 's/.*: //g')
  ARTIST=$(id3info "$file" | grep '^=== TPE1' | sed -e 's/.*: //g')
  echo "$ARTIST - $TITLE"
done

If you have spaces in your filenames then it's best to use the -print0 option to find; one possible way is this:

find . -name '*.mp3' -print0 | while read -d $'\0' file
do
  echo $file
  TITLE=$(id3info "$file" | grep '^=== TIT2' | sed -e 's/.*: //g')
  ARTIST=$(id3info "$file" | grep '^=== TPE1' | sed -e 's/.*: //g')
  echo "$ARTIST - $TITLE"
done

alternatively you can save and restore IFS. Thanks to David W.'s comments and, in particular, for pointing out that the while loop version also has the benefit that it will handle very large numbers of files correctly, whereas the first version which expands a $(find) into a for-loop will fail to work at some point as shell expansion has limits.

Solution 3

This works with most filenames (including spaces) but not newlines, tabs or double spaces.

find . -type f -name '*.mp3' | while read i; do
   echo "$i"
done

This works with all filenames.

find . -type f -name '*.mp3' -print0 | while IFS= read -r -d '' i; do
   echo "$i"
done

But if you only want to run one command you can use xargs example:

find . -type f -name '*.mp3' -print0 | xargs -0 -l echo

Solution 4

find . -name *.mp3 -exec echo {} \;

The string {} is replaced by the current file name being processed everywhere it occurs in the arguments to the command, not just in arguments where it is alone, as in some versions of find.

Please check the find man for further info http://unixhelp.ed.ac.uk/CGI/man-cgi?find

Share:
13,637
RandomName
Author by

RandomName

Updated on June 05, 2022

Comments

  • RandomName
    RandomName almost 2 years

    The following works fine within the current folder, but I would like it to scan sub folders as well.

    for file in *.mp3
    
    do
    
    echo $file
    
    done
    
  • drquicksilver
    drquicksilver about 11 years
    True. There are solutions by changing IFS or using -print0. I'll edit one in.
  • David W.
    David W. about 11 years
    What if I find 1,000,000 files? What happens is that the shell expands the $(find . -name "*.mp3) clause and replaces it in my for. It is unlikely that my command line buffer is that big, and what happens is my find will simply fail. You should be very, very careful of using shell expansion into a for loop.
  • drquicksilver
    drquicksilver about 11 years
    @DavidW. The pipe to a while loop should also fix that?
  • David W.
    David W. about 11 years
    @drquicksilver Your second alternative is the best way.
  • Adrian Frühwirth
    Adrian Frühwirth about 11 years
    +1. There should be a huge red READ THIS BEFORE YOU PROCEED sign in case someone enters a code snippet that tries to parse ls that points to the properly answered questions. It appears that the amount of questions with this kind of problem is so vast that it would actually save both the OP as well as the helpful souls here a lot of time. And given the amount of wrong answers to this kind of problem here, oh boy ...
  • gniourf_gniourf
    gniourf_gniourf over 8 years
    This is completely unsafe and broken (whatever the EDIT claims): it does break with filenames containing spaces or glob characters.
  • Charles Duffy
    Charles Duffy over 6 years
    Why are you putting incorrect code before the correct code? Folks who are skimming for an answer tend to go top-to-bottom (and not always reading very thoroughly).
  • rafark
    rafark over 4 years
    The best answer IMO. The selected answer has a lot of "text"before getting into the actual answer.