0

Given the function

hello () { echo "one"; echo "two"; }

Inside a script I'm using cmd="$(declare -f hello); hello" then I try executing the command using bash -c $cmd, however that fails due to the behavior of declare -f of removing the last ; in the declaration, is there a way to prevent that?

Details

For hello () { echo "one"; echo "two"; } declare -f results in

hello () {
    echo "one";
    echo "two"
}

instead of:

hello () {
    echo "one";
    echo "two";
}

2 Answers 2

0

It only fails because you do not correctly quote the $cmd variable when it is expanded.

declare -p "respects" semicolons – it outputs them where they're necessary to reconstruct source code semantically identical to the original source code that was parsed.

But semicolons are not needed at the end of a line; and the output of declare -p has each command in a separate line, so it could just as well have no semicolons at all and it would remain functionally identical.

(Once the function has been parsed, its original source is lost – the in-memory representation has neither semicolons nor line-breaks, all of that is reconstructed by 'declare'.)

What you need to do is preserve those line breaks by using double-quotes around the "$cmd" expansion:

$ cmd="$(declare -f hello); hello"
$ bash -c "$cmd"
one
two

Without the quotes, the variable's contents are split at whitespace:

$ echo $cmd
hello () { echo one; echo two }; hello
2
  • 1
    "What you need to do is preserve those line breaks" – Yes, but in the first place we need $cmd to expand to exactly one word. The (undisclosed) error was (): line 1: hello: command not found because the expanded command was bash -c hello () …. The lack of the semicolon allegedly being the culprit is the OP's guess and you kinda followed it. The code failed because of splitting between hello and (); at this point the rest was irrelevant. The solution is the same: proper quoting; but the explanation about semicolons and line breaks applies to a problem the parser did not even get to. Commented Nov 30, 2023 at 13:24
  • IMO this answer misdiagnoses the problem, I explained this in the above comment. It's been over 48 hours and the answer has not been fixed. Therefore I'm posting my own answer and I'm downvoting yours. Commented Dec 3, 2023 at 22:50
1

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.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .