Multi-line bash commands in makefile

BashMakefile

Bash Problem Overview


Considering that every command is run in its own shell, what is the best way to run a multi-line bash command in a makefile? For example, like this:

for i in `find`
do
    all="$all $i"
done
gcc $all

Bash Solutions


Solution 1 - Bash

You can use backslash for line continuation. However note that the shell receives the whole command concatenated into a single line, so you also need to terminate some of the lines with a semicolon:

foo:
    for i in `find`;     \
    do                   \
        all="$$all $$i"; \
    done;                \
    gcc $$all

But if you just want to take the whole list returned by the find invocation and pass it to gcc, you actually don't necessarily need a multiline command:

foo:
    gcc `find`

Or, using a more shell-conventional $(command) approach (notice the $ escaping though):

foo:
    gcc $$(find)

Solution 2 - Bash

As indicated in the question, every sub-command is run in its own shell. This makes writing non-trivial shell scripts a little bit messy -- but it is possible! The solution is to consolidate your script into what make will consider a single sub-command (a single line).

Tips for writing shell scripts within makefiles:

  1. Escape the script's use of $ by replacing with $$
  2. Convert the script to work as a single line by inserting ; between commands
  3. If you want to write the script on multiple lines, escape end-of-line with \
  4. Optionally start with set -e to match make's provision to abort on sub-command failure
  5. This is totally optional, but you could bracket the script with () or {} to emphasize the cohesiveness of a multiple line sequence -- that this is not a typical makefile command sequence

Here's an example inspired by the OP:

mytarget:
	{ \
	set -e ;\
	msg="header:" ;\
	for i in $$(seq 1 3) ; do msg="$$msg pre_$${i}_post" ; done ;\
	msg="$$msg :footer" ;\
	echo msg=$$msg ;\
	}

Solution 3 - Bash

The ONESHELL directive allows to write multiple line recipes to be executed in the same shell invocation.

all: foo

SOURCE_FILES = $(shell find . -name '*.c')

.ONESHELL:
foo: ${SOURCE_FILES}
	FILES=()
	for F in $^; do
		FILES+=($${F})
	done
	gcc "$${FILES[@]}" -o $@

There is a drawback though : special prefix characters (‘@’, ‘-’, and ‘+’) are interpreted differently.

https://www.gnu.org/software/make/manual/html_node/One-Shell.html

Solution 4 - Bash

Of course, the proper way to write a Makefile is to actually document which targets depend on which sources. In the trivial case, the proposed solution will make foo depend on itself, but of course, make is smart enough to drop a circular dependency. But if you add a temporary file to your directory, it will "magically" become part of the dependency chain. Better to create an explicit list of dependencies once and for all, perhaps via a script.

GNU make knows how to run gcc to produce an executable out of a set of .c and .h files, so maybe all you really need amounts to

foo: $(wildcard *.h) $(wildcard *.c)

Solution 5 - Bash

What's wrong with just invoking the commands?

foo:
       echo line1
       echo line2
       ....

And for your second question, you need to escape the $ by using $$ instead, i.e. bash -c '... echo $$a ...'.

EDIT: Your example could be rewritten to a single line script like this:

gcc $(for i in `find`; do echo $i; done)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionNelson TatiusView Question on Stackoverflow
Solution 1 - BashEldar AbusalimovView Answer on Stackoverflow
Solution 2 - BashBrent BradburnView Answer on Stackoverflow
Solution 3 - BashJean-Baptiste PoittevinView Answer on Stackoverflow
Solution 4 - BashtripleeeView Answer on Stackoverflow
Solution 5 - BashJesperEView Answer on Stackoverflow