The working command
When I copy and paste the command (
/usr/bin/kid3-cli -c 'get''1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac'
) into an interactive bash shell, i.e. at the command prompt, it works.
A good starting point is to understand what happens here; then it's easier to see that in other cases something else happens.
Your command is:
/usr/bin/kid3-cli -c 'get''1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac'
The shell splits the command to words, it uses unescaped and unquoted spaces and tabs as delimiters. The single-quotes inform the shell that get
is a single word (it would be a single word even without quotes) and that 1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac
is a single word (it would not be a single word without quotes). In effect the words are:
/usr/bin/kid3-cli
-c
get
1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac
Note the quotes did their job and have been removed.
Then the shell runs /usr/bin/kid3-cli
and gives it exactly three arguments: -c
, get
and 1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac
. kid3-cli
cannot know if and where quotes were used. Apparently the tree arguments is a tuple the tool can understand without errors.
The troublesome command
Now this:
"$KID3" "$ARG1" "$file"
A double-quoted non-array variable always expands to exactly one word (unless it's an error e.g. because of set -u
and the variable being unset). These three variables will expand to exactly three words:
/usr/bin/kid3-cli
from the expansion of"$KID3"
-c 'get'
from the expansion of"$ARG1"
'1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac'
from the expansion of"$file"
Quotes that appear from expanded variables are not special. They may be special later, when (if!) you tell the shell (or another shell, or whatever) to parse the result again (example: eval
). Here you do not have to and do not need to tell the shell to parse the result again; moreover, doing this would be a bad practice.
The shell runs /usr/bin/kid3-cli
and gives it exactly two arguments: -c 'get'
and '1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac'
. Not only -c
and get
are within a single argument; also the single-quotes get to kid3-cli
. Apparently this is not something the tool can understand in the way you wanted.
What about echo
?
echo
works by printing all its arguments (except arguments interpreted as options) verbatim, with a single space character between each non-last argument and the next; plus by default one trailing newline character.
echo "$KID3" "$ARG1" "$file"
results in the following words:
echo
/usr/bin/kid3-cli
from the expansion of"$KID3"
-c 'get'
from the expansion of"$ARG1"
'1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac'
from the expansion of"$file"
The shell runs echo
and gives it exactly three arguments: /usr/bin/kid3-cli
, -c 'get'
and '1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac'
. You see:
/usr/bin/kid3-cli -c 'get''1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac'
where some spaces come from variables, other spaces come from echo
. Note the output would be identical if echo
got /usr/bin/kid3-cli
, -c
, 'get'
, '1-01
, -
, Johann Strauss
, -
, and __Waldmeister___ Ouverture.flac'
(8 arguments) or if echo
got everything as just one long argument.
If you prepend echo
to your working command, i.e. if you give it arguments being the words of the working command, i.e. if you run:
echo /usr/bin/kid3-cli -c 'get''1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac'
then the output will be:
/usr/bin/kid3-cli -c get 1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac
again without any indication which spaces were added by echo
.
echo
is not a good tool to "visualize" arguments. Personally I prefer printf '<%s>\n'
. In your case printf '<%s>\n'"$KID3" "$ARG1" "$file"
would print:
</usr/bin/kid3-cli><-c 'get'><'1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac'>
and it would be pretty clear -c 'get'
is a single argument and the single-quotes got to printf
.
There is still ambiguity: if an argument was /usr/bin/kid3-cli>\n<-c 'get'
(where \n
denotes a newline character here, but I do mean a literal newline character inside the argument), then my printf
would show it like separate arguments: /usr/bin/kid3-cli
and -c 'get'
. Still, arguments with >\n<
inside are rare (at least in my workflow; adjust <%s>\n
to your needs).
Unquoted $ARG1
If you set:
ARG1="-c get"file="1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac"
and if you run:
"$KID3" $ARG1 "$file"
then this would work (with the default $IFS
). Unquoted $ARG1
would be split to two words: -c
and get
. Note if you want kid3-cli
to see get
(not 'get'
), then you cannot store -c 'get'
inside ARG1
. Similarly file="'…'"
would also be wrong here.
Unquoted $ARG1
is not that bad if the value of the variable is static, totally under your control, without characters that trigger filename generation (globbing), with $IFS
that results in splitting the value exactly where you want. In general you shall always quote.
The main thing is shell variables are for storing data, not for storing shell code. In /usr/bin/kid3-cli -c 'get'…
you type, the single quotes are syntactically meaningful to the shell, so is the space just after -c
. These parts are shell code and storing them in a variable is wrong. Sometimes (like here) you can get away with storing meaningful space(s) and not quoting the variable, it's still a bad practice.
Other ideas here: How can we run a command stored in a variable?
Solution
(Not the only solution, see the already linked question. A reliable, safe, elegant enough solution that fits the structure of your code.)
In Bash you can use an array to store more than one word reliably:
KID3=$(command -v kid3-cli)ARG1=( -c 'get' )file="1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac""$KID3" "${ARG1[@]}" "$file"
The single-quotes embracing get
are not needed, I just wanted to show you they may be there. "${ARG1[@]}"
will be expanded to two words: -c
and get
.
With this approach you can as well use just one array you build gradually:
cmnd=( "$(command -v kid3-cli)" )cmnd+=( -c get )cmnd+=( '1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac' )"${cmnd[@]}"
(the quotes embracing 1-01 …….flac
are important). Or use one array you build in one step:
cmnd=( "$(command -v kid3-cli)" -c get '1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac' )"${cmnd[@]}"
In each case the resulting command will be equivalent to:
command kid3-cli -c get "1-01 - Johann Strauss - __Waldmeister___ Ouverture.flac"
and unless you have a good reason to use the variable(s), you should just run the above, probably even without the word command
.