#3135 Issue closed: Hardcoded 'eval $command' is problematic and against "final power to the user"

Labels: discuss / RFC, fixed / solved / done

jsmeix opened issue at 2024-01-22 11:02:

In default.conf we describe several cases
where the user can specify commands to be run by ReaR
and we run those user specified commands usually via

eval $command

to make it easier for the user to specify e.g.

command='echo current date: $( date )'

and get $... evaluated not when it is specified
but when it is executed.

But in general eval arbitrary $... is problematic, cf.
https://github.com/rear/rear/pull/3089#discussion_r1412293833
and the subsequent comments therein.

And with the hardcoded eval the user can not specify
when he does not want to get $... evaluated like

command='echo to show the date use "echo current date: $( date )"'

because

# command='echo to show the date use "echo current date: $( date )"'

# $command
to show the date use "echo current date: $( date )"

# eval $command
to show the date use echo current date: Mon 22 Jan 2024 11:53:31 AM CET

By the way: Note the mising double quote characters with eval ...

So our hardcoded 'eval $command' is problematic
and it is against final power to the user.

Therefore I am thinking about to remove our hardcoded 'eval'
and explain in default.conf that the user must manually
specify 'eval' when he needs it.

So instead of

command='echo current date: $( date )'

the user would have to specify

command='eval echo current date: $( date )'

and ReaR could simply and safely call plain $command
for example like

# command='eval echo current date: $( date )'

# $command ; sleep 1 ; $command
current date: Mon 22 Jan 2024 11:59:39 AM CET
current date: Mon 22 Jan 2024 11:59:40 AM CET

pcahyna commented at 2024-01-26 18:21:

This would be a big compatibility break, so it should belong to ReaR 3.0, not 2.8.

I am not even convinced that this change would be an improvement. Yes, in https://github.com/rear/rear/pull/3089#discussion_r1412293833 I noted that eval of an arbitrary variable expansion is problematic. But I said that in the context of something like eval echo "Launching \'$RECOVERY_COMMANDS_LABEL\' automatically", where the user does not expect that the $RECOVERY_COMMANDS_LABEL string is being passed to eval (one expects it to be some arbitrary string for display). As I clarified in https://github.com/rear/rear/pull/3089#discussion_r1412295915 , I believe eval "$command" to be fine. $command is documented to be executed this way, so it is not an arbitrary string.

Your argument about "with the hardcoded eval the user can not specify
when he does not want to get $... evaluated" is interesting, but I don't consider it convincing. Let's say I want the literal string to show the date use "echo current date: $( date )" displayed. In plain shell, I would use

echo 'to show the date use "echo current date: $( date )"'

without the single quotes it would not have worked either, so to assign it to command one needs to quote it properly at one more level:

command="echo 'to show the date use \"echo current date: \$( date )\"'"

which then works as expected:

$ eval "$command"
to show the date use "echo current date: $( date )"

unwieldy? yes. Against "final power to the user"? I would not say so. The bash quoting rules are well documented and it is quite easy to understand what one needs to put into $command: it must get assigned the same string as one would type at the command line. The complexity arises from the amount of quoting that is sometimes needed to achieve the result. The $'...' quoting mechanism can help a bit here - one is forced to quote only the ' character and nothing else:

$ command=$'echo \'to show the date use "echo current date: $( date )"\''
$ eval "$command"
to show the date use "echo current date: $( date )"

which looks a bit better.

Your suggestion to call the command as merely $command sounds simpler, but this simplicity is IMO superficial and hides lots of surprising complexity. Let's say that in your example I want to put some extra spaces for readability:

# note the doubled spaces
$ command='echo to show the date use  "echo current date:  $( date )"'

result:

$ $command
to show the date use "echo current date: $( date )"

where are my spaces? Maybe you would expect the extra space before " to disappear, but what about the one before $(, which is inside a quoted string? What has happened? Let's examine a simpler example:

# note the triple spaces
$ command="echo 'a   b   c'"
$ $command
'a b c'

where are my triple spaces and what is the ' character doing there? Let's examine the result of the $command expansion:

$ for i in $command ; do $i ; done

bash: 'a: command not found
bash: b: command not found
bash: c': command not found

What happens is that the shell performs whitespace splitting on the value of $command, resulting in for words:
echo 'a b c' and then executes this sequence (with echo as the command and 'a b c' as its three arguments).

I must say that when you let the shell to split the command string into words and then execute the sequence of words, it is so difficult for me how to make it do what I need that I much prefer eval with the "quoting hell" needed to prepare its input (which is in most cases even quite unnecessary).

Worse, how do you execute multiple commands?

$ command="echo a  b  c ; echo d e f"
$ $command
a b c ; echo d e f

not only are the extra spaces eaten - the ; is not intepreted and the second echo is not executed at all. Yes, you could use eval:

$ command="eval echo a  b  c ; echo d e f"
$ $command
a b c
d e f

-- almost right, but I still don't know how to get the extra spaces there, some word splitting is still going on:

$ command="eval echo 'a  b  c' ; echo d e f"
$ $command
a b c
d e f

not quite right either, neither is

$ command="eval 'echo a  b  c ; echo d e f'"

:

$ $command
bash: echo a b c ; echo d e f: command not found

I admit I am lost here.

pcahyna commented at 2024-01-26 19:49:

The underlying problem is that with eval $command it gets processed twice: when command gets first assigned in the config file ( command=... ) and then when eval $command evaluates it. Both steps have fairly easy rules. With merely $command you get another processing step after $command is expanded but before it gets executed: word splitting on whitespace (and probably other stuff, like filename expansion), which complicates the process, despite the execution of $command being simpler than the execution of eval $command.

jsmeix commented at 2024-02-07 14:15:

@pcahyna
thank you so much for your detailed explanations.

In particular your reasoning

I believe eval "$command" to be fine.
$command is documented to be executed this way,
so it is not an arbitrary string.
...
The bash quoting rules are well documented and it is quite easy
to understand what one needs to put into $command:
it must get assigned the same string
as one would type at the command line.
The complexity arises from the amount of quoting
that is sometimes needed to achieve the result.

convinced me that the current eval "$command" is fine
and now with this issue here it is even perfectly well
documented WHY the current eval "$command" is fine.


[Export of Github issue for rear/rear.]