Shell – What is a Subshell in Make Documentation

makeprocessshell

I'm reading a book about the make command and there is a paragraph:

If any of the prerequisites has an associated rule, make attempts to
update those first. Next, the target file is considered. If any
prerequisite is newer than the target, the target is remade by
executing the commands. Each command line is passed to the shell and
is executed in its own subshell.

Could you explain the concept of subshell used there and why is it necessary to use such a subshell?

Best Answer

make(1) itself does not know how to run shell commands. It could have been made to do so, but the Unix Way is to have well-separated concerns: make(1) knows how to build dependency graphs that determine what has to be made, and sh(1) knows how to run commands.

The point the author is trying to make there is that you must not write those command lines such that a later one depends on a former one, except through the filesystem. For example, this won't work:

sometarget: some.x list.y of-dependencies.z
    foo=`run-some-command-here`
    run-some-other-command $$foo

If this were a two-line shell script, the first command's output would be passed as an argument to the second command. But since each of these commands gets run in a separate sub-shell, the $foo variable's value gets lost after the first sub-shell exists, so there is nothing to pass to the first.

One way around this, as hinted above, is to use the filesystem:

TMPDIR=/tmp
TMPFILE=$(TMPDIR)/example.tmp
sometarget: some.x list.y of-dependencies.z
    run-some-command-here > $(TMPFILE)
    run-some-other-command `cat $(TMPFILE)`

That stores the output of the first command in a persistent location so the second command can load the value up.

Another thing that trips make(1) newbies up sometimes is that constructs that are usually broken up into multiple lines for readability in a shell script have to be written on a single line or wrapped up into an external shell script when you do them in a Makefile. Loops are a good example; this doesn't work:

someutilitytarget:
    for f in *.c
    do
        munch-on $f
    done

You have to use semicolons to get everything onto a single line instead:

someutilitytarget:
    for f in *.c ; do munch-on $f ; done

For myself, whenever doing that gives me a command line longer than 80 characters or so, I move it into an external shell script so it's readable.

Related Question