Shell Eval

By Susam Pal on 06 Jan 2022

In this post, we will perform a few experiments to see the usefulness of the eval command for a particular scenario in a POSIX-compliant shell. At first, we prepare a test file that contains a space in its name and define a variable as follows:

$ echo lorem ipsum > "foo bar"
$ cmd='cat "foo bar"'

We will use this file and the variable in the experiments below. All output examples below are obtained using Dash 0.5.11 on a Debian GNU/Linux 11.2 (bullseye) system. Dash stands for Debian Almquist Shell which is a POSIX-compliant shell available in Debian. Any POSIX conforming shell should produce similar output. On Zsh, use the command emulate sh before running these examples to get similar output.

Experiment 1

Now simply enter $cmd as a command into the shell. The following error occurs:

$ $cmd
cat: '"foo': No such file or directory
cat: 'bar"': No such file or directory

The error occurs because the above command expands to the command cat followed by two arguments: "foo and bar". Such an expansion occurs due to a concept known as field splitting. Quoting from section 2.6.5 of POSIX.1-2017:

After parameter expansion, command substitution, and arithmetic expansion, the shell shall scan the results of expansions and substitutions that did not occur in double-quotes for field splitting and multiple fields can result.

The shell shall treat each character of the IFS as a delimiter and use the delimiters as field terminators to split the results of parameter expansion, command substitution, and arithmetic expansion into fields.

By default, the space character belongs to IFS. Here is an example command to verify this:

$ printf "$IFS" | od -tcx1
0000000      \t  \n
         20  09  0a

The hexadecimal code 20 in the output confirms that the space character is present in the value of IFS. Therefore, as per the POSIX specification, $cmd first expands to cat "foo bar", then it is split into three fields cat, "foo, and bar", and then the command cat is executed with two arguments "foo and bar". Since no files with those names exist, an error occurs.

Experiment 2

Next we try to double-quote the previous command to prevent field splitting and see what happens:

$ "$cmd"
dash: 8: cat "foo bar": not found

The excerpt from the POSIX.1-2017 specification quoted in the previous section shows that field splitting does not for occur variable expansions within double quotes. So the entire expansion cat "foo bar" remains intact as a single field and is then executed as a command. Since there is no such weirdly named command, we get the above error.

Experiment 3

Field splitting leads to an error as seen in the first experiment. Preventing field splitting by double-quoting the variable expansion also leads to an error as seen in the second experiment. How do we execute the command in the cmd variable?

We need a way to somehow get the shell to parse cat "foo bar" like a shell normally does, i.e., treat each unquoted token as a separate field and each quoted token as a single one. How do we get the shell to do that? Well, we can just invoke the shell itself to parse our command:

$ sh -c "$cmd"
lorem ipsum

But the above command invokes a new shell process. Can we avoid that? Yes, using the eval command:

$ eval "$cmd"
lorem ipsum