Linux – Calling commands in bash script with parameters which have embedded spaces (eg filenames)


I am trying to write a bash script which does some processing on music files. Here is the script so far:



find `pwd` -iname "*.mp3" -o -iname "*.flac" | while read f
    echo "$f"
    exiftool "${arr[@]}"


This fails with:

[johnd:/tmp/tunes] 2 $ ./ 
./ line 9: syntax error near unexpected token `$(f)'
./ line 9: `    $arr=($(f))'
[johnd:/tmp/tunes] 2 $ 

I have tried many different incantations, none of which have worked. The bottom line is I'm trying to call a command exiftool, and one of the parameters of that command is a filename which may contain spaces. Above I'm trying to assign the filename $f to an array and pass that array to exiftool, but I'm having trouble with the construction of the array.

Immediate question is, how do I construct this array? But the deeper question is how, from within a bash script, do I call an external command with parameters which may contain spaces?

Best Solution

You actually did have the call-with-possibly-space-containing-arguments syntax right (program "${args[@]}"). There were several problems, though.

Firstly, $(foo) executes a command. If you want a variable's value, use $foo or ${foo}.

Secondly, if you want to append something onto an array, the syntax is array+=(value) (or, if that doesn't work, array=("${array[@]}" value)).

Thirdly, please separate filenames with \0 whenever possible. Newlines are all well and good, but filenames can contain newlines.

Fourthly, read takes the switch -d, which can be used with an empty string '' to specify \0 as the delimiter. This eliminates the need to mess around with IFS.

Fifthly, be careful when piping into while loops - this causes the loop to be executed in a subshell, preventing variable assignments inside it from taking effect outside. There is a way to get around this, however - instead of piping (command | while ... done), use process substitution (while ... done < <(command)).

Sixthly, watch your process substitutions - there's no need to use $(pwd) as an argument to a command when . will do. (Or if you really must have full paths, try quoting the pwd call.)


The script, revised:

while read -r -d '' f; do
    echo "$f" # For debugging?
done < <(find . -iname "*.mp3" -o -iname "*.flac" -print0)
exiftool "${arr[@]}"

Another way

Leveraging find's full capabilities:

find . -iname "*.mp3" -o -iname "*.flac" -exec exiftool {} +
# Much shorter!

Edit 1

So you need to save the output of exiftool, manipulate it, then copy stuff? Try this:

while read -r -d '' f; do
    echo "$f" # For debugging?
done < <(find . -iname "*.mp3" -o -iname "*.flac" -print0)
# Warning: somewhat misleading syntax highlighting ahead
newfilename="$(exiftool "${arr[@]}")"
newfilename="$(manipulate "$newfilename")"
cp -- "$some_old_filename" "$newfilename"

You probably will need to change that last bit - I've never used exiftool, so I don't know precisely what you're after (or how to do it), but that should be a start.

Related Question