Hi! I'm trying to do something like the following:
for file in `find . *.foo`
do
somecommand $file
done
But the command isn't working because $file is very odd. Because my directory tree has crappy file names (including spaces), I need to escape the find command. But none of the obvious escapes seem to work:
-ls gives me the space-delimited filename fragments
-fprint doesn't do any better.
I also tried: for file in "find . *.foo -ls"; do echo $file; done
- but that gives all of the responses from find in one long line.
Any hints? I'm happy for any workaround, but am frustrated that I can't figure this out.
Thanks, Alex
(Hi Matt!)
-
Instead of relying on the shell to do that work, rely on find to do it:
find . -name "*.foo" -exec somecommand "{}" \;Then the file name will be properly escaped, and never interpreted by the shell.
dfa : for GNU find, -exec somecommand {} + is better -
xargs is your friend. You will also want to investigate the -0 (zero) option with it.
find(with-print0) will help to produce the list. The Wikipedia page has some good examples.Another useful reason to use
xargs, is that if you have many files (dozens or more), xargs will split them up into individual calls to whatever xargs is then called upon to run (in the first wikipedia example,rm)Steve Jessop : "xargs will split them up into individual calls". Or if this isn't desirable, as happens in rare cases, then use "-n1 -r" to get the same behaviour as the for loop. -r is a GNU extension, -n is POSIX.Alister Bulman : +1 for the tip, onebyonelhunath : -1 because xargs is broken unless used with the -0 option and using the -0 option with find is pretty silly seeing as find itself has a -exec {} + predicate that does exactly the same thing.Alister Bulman : '-0 |xargs' is easier to remember than escaping the call to '-exec {}' however, which can get complicated. -
find . -name '*.foo' -print0 | xargs -0 -n 1 somecommandIt does get messy if you need to run a number of shell commands on each item, though.
Steve Jessop : Actually, you need -r (and hence GNU find or similar) to exactly emulate the for loop. xargs will otherwise execute the command with no arguments if the input is 0-length.lhunath : or you could use find's -exec {} + to avoid the convolution: find . -name '*.foo' -exec somecommand {} \; -
find . -name '*.foo' -print0 | xargs -0 sh -c 'for F in "${@}"; do ...; done' "${0}" -
You have plenty of answers that explain well how to do it; but for the sake of completion I'll repeat and add to it:
xargsis only ever useful for interactive use (when you know all your filenames are plain - no spaces or quotes) or when used with the-0option. Otherwise, it'll break everything.findis a very useful tool; put using it to pipe filenames intoxargs(even with-0) is rather convoluted asfindcan do it all itself with either-exec command {} \;or-exec command {} +depending on what you want:find /path -name 'pattern' -exec somecommand {} \; find /path -name 'pattern' -exec somecommand {} +The former runs
somecommandwith one argument for each file recursively in/paththat matchespattern.The latter runs
somecommandwith as many arguments as fit on the command line at once for files recursively in/paththat matchpattern.Which one to use depends on
somecommand. If it can take multiple filename arguments (likerm,grep, etc.) then the latter option is faster (since you runsomecommandfar less often). Ifsomecommandtakes only one argument then you need the former solution. So look atsomecommand's man page.More on
find: http://mywiki.wooledge.org/UsingFindIn
bash,foris a statement that iterates over arguments. If you do something like this:for foo in "$bar"you're giving
forone argument to iterate over (note the quotes!). If you do something like this:for foo in $baryou're asking
bashto take the contents ofbarand tear it apart wherever there are spaces, tabs or newlines (technically, whatever characters are inIFS) and use the pieces of that operation as arguments to for. That is NOT filenames. Assuming that the result of a tearing long string that contains filenames apart wherever there is whitespace yields in a pile of filenames is just wrong. As you have just noticed.The answer is: Don't use
for, it's obviously the wrong tool. The abovefindcommands all assume thatsomecommandis an executable inPATH. If it's abashstatement, you'll need this construct instead (iterates overfind's output, like you tried, but safely):while read -r -d ''; do somebashstatement "$REPLY" done < <(find /path -name 'pattern' -print0)This uses a
while-readloop that reads parts of the stringfindoutputs until it reaches aNULLbyte (which is what-print0uses to separate the filenames). SinceNULLbytes can't be part of filenames (unlike spaces, tabs and newlines) this is a safe operation.If you don't need
somebashstatementto be part of your script (eg. it doesn't change the script environment by keeping a counter or setting a variable or some such) then you can still usefind's-execto run yourbashstatement:find /path -name 'pattern' -exec bash -c 'somebashstatement "$1"' -- {} \; find /path -name 'pattern' -exec bash -c 'for file; do somebashstatement "$file"; done' -- {} +Here, the
-execexecutes abashcommand with three or more arguments.- The bash statement to execute.
- A
--.bashwill put this in$0, you can put anything you like here, really. - Your filename or filenames (depending on whether you used
{} \;or{} +respectively). The filename(s) end(s) up in$1(and$2,$3, ... if there's more than one, of course).
The
bashstatement in the firstfindcommand here runssomebashstatementwith the filename as argument.The
bashstatement in the secondfindcommand here runs afor(!) loop that iterates over each positional parameter (that's what the reducedforsyntax -for foo; do- does) and runs asomebashstatementwith the filename as argument. The difference here between the very firstfindstatement I showed with-exec {} +is that we run only onebashprocess for lots of filenames but still onesomebashstatementfor each of those filenames.All this is also well explained in the
UsingFindpage linked above. -
I had to do something similar some time ago, renaming files to allow them to live in Win32 environments:
#!/bin/bash IFS=$'\n' function RecurseDirs { for f in "$@" do newf=echo "${f}" | sed -e 's/[\\/:\*\?#"\|<>]/_/g'if [ ${newf} != ${f} ]; then echo "${f}" "${newf}" mv "${f}" "${newf}" f="${newf}" fi if [[ -d "${f}" ]]; then cd "${f}" RecurseDirs $(ls -1 ".") fi done cd .. } RecurseDirs .This is probably a little simplistic, doesn't avoid name collisions, and I'm sure it could be done better -- but this does remove the need to use basename on the find results (in my case) before performing my sed replacement.
I might ask, what are you doing to the found files, exactly?
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.