1

Why this works:

[ -r /tmp ] && echo "tt" >/tmp/taa
cat taa
tt

But the following doesn't and how to fix that, preserving variable for reusability?

COMD='[ -r /tmp ] && echo "tt"'
$COMD >/tmp/taa
bash: [: missing `]

Thanks

Edit: eval indeed works. However, I don't get why, and in the question, that mine was marked as a duplicate of, there is no explanation either. Moreover, if the part starting with && is removed it works and even actually returns right results without eval:

COMD='[ -r /tmp ]'
$COMD
echo $?
0

COMD='[ -r /tmpdddd ]'
$COMD
echo $?
1
igorepst
  • 143
  • 9
  • 2
    $COMD is a variable you need either define it as an alias or function or eval $COMD it – αғsнιη Jun 07 '18 at 19:17
  • If you have not tampered with /tmp, everybody (owner, group, others) have read,write,execute permissions, so you need not check for that. – sudodus Jun 07 '18 at 19:41
  • 1
    @sudodus, This is just an example. Moreover, in the real script, the directory in question comes as an input from a user and should be validated either at local or remote (via SSH) machine – igorepst Jun 07 '18 at 19:45
  • 1
    Storing code in a variable is a really bad idea. If you want reusability use a function. – Chris Davies Jun 07 '18 at 20:17

2 Answers2

3

Because the && operator is not recognized after variable expansion. Neither are the quotes: if that worked, it would print "tt". (Try: cmd='echo "tt"'; $cmd)

Make it a function instead:

checktmp() {
    [ -r /tmp ] && echo "tt"
}
checktmp

Functions are for code, variables for data.

ilkkachu
  • 138,973
3

You wouldn't expect:

var='echo test && reboot'
echo $var

To output test and then reboot, would you?

Instead, it outputs echo test && reboot

That's the same for

var='echo test && reboot'
$var

Now, where it becomes confusing is that in most Bourne-like shells, it doesn't fail with an error that says that the echo test && reboot command is not found. Instead, and unless you have modified $IFS, it outputs test && reboot.

That's because, in those shells, leaving a variable unquoted applies some sort of split+glob operator.

Because $var is not quoted, it is split into words (according to the $IFS special parameter): echo, test, &&, reboot, and the command to run is derived from the first one (echo) and all the words passed as arguments to that command. && is an argument to echo there. Only a literal unquoted && would be understood as the && operator of the sh language. Like in C, var="foo, bar"; printf(var) doesn't mean two arguments are passed to printf just because var happens to contain , which is an operator in the syntax of the C language.

Actually, if we go back in history to the very first versions of Unix in the early 70s, their sh (nowadays referred to as the Thompson shell) would have run reboot there. Well, sh back then didn't support variables, but it did have positional parameters.

Here using Unix V1 on a PDP11 emulator:

$ cat myscript
echo $1
$ sh myscript 'echo test; reboot'
echo test
No command
$ ls -l /bin/sh
total   12
 88 lxrwr-  1 sys    5312 Jan  1 00:00:00 sh

(thankfully, there was no reboot command back then, hence the No command error).

That may sound crazy by today's standard, but remember Unix V1 ran on machines with a few hundred kilobytes of memory and megabytes of storage, so things had to be kept simple back then. See how that sh is significantly smaller than /bin/true (the command that does nothing) on a modern system (27KB on my system (not counting the dynamic linker or the dynamic libraries it's linked to) against 5KB for that sh).

Unix V7 in the late 70s came with the Bourne shell, the new sh then. A lot more advanced, but still didn't break completely compatibility with its predecessor. Same for csh that came at about the same time on BSD. It's probably the main reason for that awkward behaviour of the Bourne shell: parameter expansion still undergoes some forms of expansion, like that splitting into words and also globbing (but not as far as full sh language re-interpretation as in the early Unix shells as that would be crazy), for backward compatibility with the Thomson shells.

Some shells like rc, fish or zsh (that one a Bourne-like shell) have eventually fixed that later on, but many including bash have kept backward compatibility with the Bourne shell.

Now, if you want to interpret a string as shell code, that's what the eval command is for:

eval "$var"

(note the quotes to prevent the split+globbing in Bourne-like shells).

But if you want to store code in a variable, it would make much more sense to use a function like in other languages:

f() { echo test && reboot; }