Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

The situation is, I have an MP3 player mpg321 that accepts a list of files as argument. I keep my music in a directory named "music", in which there are a few more directories. I just want to play all of them, so I run the program with

mpg321 $(find /music -iname "*\.mp3")

. The problem is, some file names have whitespace in them, and the program breaks those names into smaller parts and complains about missing files. Wrapping the result of find in quotes

mpg321 "$(find /music -iname "*\.mp3")"

does not help because all will become one big "file name", which is obviously not found.

How can I do this then? If that matters, I am using bash, but will be switching to zsh soon.

share|improve this question

5 Answers 5

up vote 9 down vote accepted

Try using find's -print0 or -printf option in combination with xargs like this:

find /music -iname "*\.mp3" -print0 | xargs -0 mpg321

How this works is explained by find's manual page:

-print0

True; print the full file name on the standard output, followed by a null character (instead of the newline character that -print uses). This allows file names that contain newlines or other types of white space to be correctly interpreted by programs that process the find output. This option corresponds to the -0 option of xargs.

share|improve this answer
    
Sorry for all the editing. I seem to remember the -print0 xarg -0 pattern failing on other types of whitespace, but I couldn't find an example. –  Steven D Oct 1 '10 at 16:35
    
oh yes that works! but I still wonder why mpg321 $(find /music -iname "*\.mp3" -print0) doesn't? –  phunehehe Oct 1 '10 at 17:13
1  
Well, I believe that doesn't work for two reasons: (1) the file names are separated by null characters, which is not what mpg321 is expecting at all and (2) xargs is doing the work of escaping the other whitespace, not find. –  Steven D Oct 1 '10 at 17:18
2  
@phunehehe, @Steven: mpg321 has nothing to do with it, it's the shell that's breaking up the output of find into separate arguments. And -print0 | xargs -0 will work with every possible file name. –  Gilles Oct 1 '10 at 18:53
find /music -iname "*\.mp3" -exec mpg123 {} +

With GNU find, you can also use -print0 and xargs -0, but there's little point in learning yet another tool. The -exec ... {} + syntax gets little mention because Linux acquired it later than -print0, but there's no reason not to use it now.

With zsh or bash 4, this is a lot simpler:

mpg123 **/*.[Mm][Pp]3

In zsh only, you can make a (part of a) pattern case-insensitive:

mpg123 (#i)**/*.mp3
share|improve this answer
    
+1 to this, "find -print0" is an ugly hack. –  p-static Oct 2 '10 at 6:04

I think Steven's solution is best, but another way is to use xargs' -I flag, which lets you specify a string that will then be replaced in the command with the argument (instead of just appending the argument onto the end of the command). You can use that to quote the argument:

find /music -iname "*\.mp3" | xargs -0 -Ifoo mpg321 "foo"
share|improve this answer

Another way of doing this is to escape all special characters that come in your file names. E.g.:

find /music -iname "*\.mp3" | sed 's!\([] \*\$\/&[]\)!\\\1!g' | xargs mpg321

This will basically pass the properly escaped file names to xargs for execution and there won't be any issues.

share|improve this answer
1  
This still breaks on newlines in filenames. And you might as well just sed 's|.|\\&|g' - that's what POSIX recommends anyway. –  mikeserv Jun 10 at 2:23

It is usually best to directly -exec ${tgt_process} \{\} + but if you do need to get a reliably delimited list of filenames in a file or stream from find for whatever reason, then you can do this:

find -exec sh -c 'printf "///%s///\n" "$@"' -- \{\} +

What you get out of that is two unique strings. At the head of every filename is the string \n/// and at the tail of every filename is the string ///\n. These two strings do not occur anywhere else in find's output except for in those positions regardless of whatever characters the filenames contain.

Additionally the above usage is baseline POSIX portable and can be relied upon to work on nearly any unix system. This is not true of the use of a null byte delimiter - despite its convenience - recommended by some others.

But, again, this is only necessary if you can't directly -exec your $tgt_process for whatever reason, as that should be your goal. For one thing, the above method still requires parsing. For instance, if you wanted each filename shell quoted you'd first have to ensure any hard-quotes in the file-name were escaped:

find ... + | sed 's|'\''|&"&"&|g;s|///|'\''|g'

That outputs a properly shell-escaped array of filenames regardless of whatever their constituent characters might be. Now you just have to hope that your application on the receiving end won't mangle it.

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.