Wednesday 20 July 2011

Vanish decades of newbie make pain

I've just cured years of makefile hell with this single line:

   SHELL:=bash -c 'exec $(SHELL) "$${@//\\$$'\''\012'\''/$$'\''\012'\''}"' --

It means that make recipes that use \ at the end of a line for multi-line recipes will now have the \ stripped off - at last you can have PROPER multi-line recipes!!!

Previously, if you had normal looking multi-line shell clauses in your make recipe, you had to do something like this:

    if blah ; \
    then update-file ; \
    else create-file ; \

Which is a bit of a hack, and is actually treated as a single line shell statement with semi-colons.

And that is fair enough unless you have a multi-line sed statement as part of your recipe - which is easy to do by accident if you are using literate programming ( with auto-syntax-quoting as one chunk is included into another. (Auto-syntax quoting is when your shell chunk included in a makefile has the $ converted to $$ automatically [something the banking industry has been doing for years])

Because make will start a new shell command every time it finds a newline in the command string (even if you hide it in a mid-line shell variable) there is no way that a backslash-newline can occur except when the human deliberately escapes it.

The single-line SHELL re-definition of mine uses bash to replace backslash-newline with newline and then passes the command to your regular shell.

And I get extra points for using exec

Now my literate programming tools can automatically and nestedly invoke sed in a bash context and then that bash in a make context with automatic quoting and have it all work nicely!

(As long as the makefile author inserts that sticking plaster at the top of the makefile)

And the newbie who uses this can use backslash-newline in a makefile in the proper intuitive way.

    if blah \
    then update-file \
    else create-file \


  1. The .ONESHELL target can be used to achieve a somewhat similar effect, but with less control

    1. Specifically, the less control that .ONESHELL enforces, is that all recipes must be handled that way
      The fine control of a trailing backslash is removed.

      Further .ONESHELL applies brute treatment to recipe prefix characters -, @ etc which might legitimately appear on the first line of a recipe as an embedded perl or multi-line sed command

  2. Since GNU Make 4.0, any special shell characters in SHELL are escaped, presumably in fear that SHELL will be invoked by the shell, even though this is only true when system() or it's other-platform equivalents are used, and not when execve() is used.

    Thus, the complex bash interpolation used is mangled by make and prevents it from working.

    .SHELLFLAGS was introduced before 4.0 and is a perhaps more suitable place to do this. .SHELLFLAGS seems to be available in the same release as the oneshell function, so, we set .SHELLFLAGS and append it to SHELL if we think .SHELLFLAGS is not supported.

    .SHELLFLAGS:=-c 'exec /bin/bash "$${@//\\$$'\''\012'\''/$$'\''\012'\''}"' -- $(if $(.SHELLFLAGS),$(.SHELLFLAGS),-c)
    ifeq (,$(filter oneshell,$(.FEATURES)))
    SHELL:=/bin/bash $(.SHELLFLAGS)