The lack of double-quotes around the variable in bash -c $cmd
caused the command to be equivalent to:
bash -c hello '()' …
where hello
is a command string, ()
is the name you inadvertently gave to this exact bash
and …
denotes the rest of arguments from the expansion of unquoted $cmd
. In other words the content of $cmd
was split to multiple arguments.
The important thing is the command was very different than bash -c 'hello () …'
you hoped for, where the entire string in quotes is a command string.
Your newly called bash
tried to execute hello
and nothing more, while for it $0
was ()
and all the remaining arguments ({
, echo
, "one";
, echo
, "two"
and }
from the expansion of unquoted $cmd
)
became positional parameters ($1
, $2
, …) the shell did not use anyway, they were totally irrelevant.
The error was:
(): line 1: hello: command not found
(it's a shame you did not disclose it in the question). You can see the shell really named itself ()
. You can get the same error from
bash -c hello '()' arbitrary garbage which is also irrelevant
If you managed to force declare -f
to put a semicolon where you wanted, it would not make any difference because the semicolon would belong to irrelevant arguments; the shell code would still be the sole hello
and the error would be the same.
A solution is to double-quote the variable:
bash -c "$cmd"
It's something you should (almost) always do.
The above command is equivalent to bash -c 'hello () …'
where everything you wanted to be interpreted as shell code is a command string.
Note this other answer (when I'm writing this it's revision 1) also advises double-quoting, but the rationale is wrong. That answer concentrates on preserving line breaks, something you can do by not including the newline character in IFS
. Taking care of line breaks is not enough, try the following code:
hello () { echo "one"; echo "two"; }
cmd="$(declare -f hello); hello"
IFS=' '
bash -c $cmd
The code does preserve newline characters, but it still splits $cmd
on spaces, so the command string is again sole hello
and the error is the same.
Yes, we do want to preserve line breaks, but in the first place we want the whole expanded $cmd
to became a single argument, the command string. Double-quoting is the right way to do this and it also preserves line breaks. My point is: if the content of $cmd
gets split between hello
and ()
then the semicolon (or the lack of it) or the line breaks (or the lack of them) will be irrelevant anyway. Splitting is the problem. The other answer explains and solves a problem that did not even occur (splitting broke your code earlier). Solving that problem makes no sense, unless we solve the real problem first. The solution in the other answer "by chance" solves the real problem as well, in fact it is the right solution to the real problem. This makes the answer "work" and look right, but it's not a good answer.
It's kinda like this: Imagine you try to hammer a screw and you keep hitting your finger. You think if you don't hit the finger, you will eventually drive the screw. You think a screw with a bigger head would help (because it's easier to hit its head). Then someone tells you to use a screwdriver instead of the hammer and the rationale is you don't hit your finger while operating a screwdriver. The solution works very well: you don't need to change the screw, you no longer hit your finger, you are able to drive the screw easily. But the real reason to use a screwdriver has nothing to do with finger-hitting-ability: the screwdriver fits the screw and the hammer does not.