The construct $( ... )
in your
for x in $(find ... | ... | ... ) ; do ... ; done
executes whatever is in $( ... )
and passes the newline separated output that you would see in the terminal if you had executed the ...
command from the shell prompt to the for
construct as a long list of names separated by blanks, as in
% ls -d1 b*
bianco nodi.pdf
bin
b.txt
% echo $(ls -d1 b*)
bianco nodi.pdf bin b.txt
%
now, the for
cycle assign to i
the first item in the list, in my example bianco
and of course it's not what you want...
This situation is dealt with this idiom, in which the shell reads ONE WHOLE LINE at a time
% ls -d1 b* | while read i ; do echo "$i" ; ... ; done
in your case
find . -name "*.mp3" -printf "%h\n" | sort -u | while read i ; do
echo "$i"
done
hth, ciao
Edit
My anser above catches the most common case of blanks inside the filename, but it still fails if one has blanks at the beginning or the end of the filename and it fails also if there are newlines embedded in the filename.
Hence, I'm modifying my answer quite a bit, according to the advice from BroSlow (see comments below).
find . -name "*.mp3" -printf "%h\0" | \
sort -uz | while IFS= read -r -d '' i ; do
...
done
Key points
find
's printf
now separates filenames with a NUL
.
sort
, by the -z
option, splits elements to be sorted on NUL
s rather than on newlines.
IFS=
stops completely the shell habit of splitting on generic whitespace.
read
's option -d
(this is a bashism) means that the input is split on a particular character (by default, a newline).
Here I have -d ''
that bash sees as specifying NUL
, where BroSlow had $'\0'
that bash expands, by the rules of parameter expansion, to ''
but may be clearer as one can see an explicit reference to the NUL
character.
I like to close with "Thank you, BroSlow".