175

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
John Kugelman
  • 349,597
  • 67
  • 533
  • 578
Nelson Tatius
  • 7,693
  • 8
  • 47
  • 70

6 Answers6

204

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)
Eldar Abusalimov
  • 24,387
  • 4
  • 67
  • 71
134

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 ;\
    }
Brent Bradburn
  • 51,587
  • 17
  • 154
  • 173
  • 9
    You may also want [`SHELL := /bin/bash`](http://stackoverflow.com/questions/589276/how-can-i-use-bash-syntax-in-makefile-targets) in your makefile to enable BASH-specific features such as [process substitution](http://unix.stackexchange.com/questions/17107/process-substitution-and-pipe). – Brent Bradburn May 28 '15 at 04:29
  • 17
    Subtle point: The space after `{` is crucial to prevent interpretation of `{set` as an unknown command. – Brent Bradburn Sep 07 '15 at 03:42
  • 9
    Subtle point #2: In the makefile it probably doesn't matter, but the distinction between wrapping with `{}` and `()` makes a big difference if you sometimes want to copy the script and run it directly from a shell prompt. You can wreak havoc on your shell instance by declaring variables, and especially modifying state with `set`, inside of `{}`. `()` prevents the script from modifying your environment, which is probably preferred. Example (**this will end your shell session**): `{ set -e ; } ; false`. – Brent Bradburn Oct 20 '15 at 19:37
  • 1
    You can include comments by using the following form: `command ; ## my comment \\` (the comment is between `;` and `\\`). This seems to work fine except that *if* you run the command manually (by copy-and-paste), the command history will include the comment in a way that breaks the command (if you try to reuse it). [Note: The syntax highlighting is broken for this comment due to the use of backslash inside backtick.] – Brent Bradburn Nov 11 '15 at 05:29
  • 1
    If you want to break a string in bash/perl/script inside makefile, close the string quote, backslash, newline, (no indentation) open string quote. Example; perl -e QUOTE print 1; QUOTE BACKSLASH NEWLINE QUOTE print 2 QUOTE – mosh Oct 30 '16 at 03:50
  • I just found that I needed to wrap compound statements with parentheses, for reasons that I can't explain at the moment: `( false && true )`. Without the parentheses, this isn't stopping the script. – Brent Bradburn Feb 28 '17 at 23:51
  • ... the preceding note about parentheses may only be relevant on some (buggy?) platforms. – Brent Bradburn Mar 01 '17 at 19:11
  • @nobar: in a Makefile, is whitespace-backslash-newline-whitespace replaced by a space or by an empty string? – glenn jackman May 14 '20 at 01:03
  • @glennjackman, In the typical case, that would be interpreted as white-space. I don't see the case where it would be considered a string (perhaps if it was somehow quoted). In my example, I generally include a semicolon in that sequence -- so it's just used to separate commands. – Brent Bradburn May 14 '20 at 02:38
  • Sometimes, you may want to use `&` instead of `;` as the command separator (puts the command in the background). – Brent Bradburn May 17 '20 at 22:08
23

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

3

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)
tripleee
  • 175,061
  • 34
  • 275
  • 318
1

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)
JesperE
  • 63,317
  • 21
  • 138
  • 197
  • 16
    Cause "every command is run in its own shell", while I'm need to use result of command1 in command2. Like in example. – Nelson Tatius Apr 12 '12 at 10:03
  • If you need to propagate information between shell invocations you need to use some form of external storage, such as a temporary file. But you can always rewrite your code to execute multiple commands in the same shell. – JesperE Apr 12 '12 at 10:12
  • +1, especially for the simplified version. And isn't it the same as doing just ``gcc `find```? – Eldar Abusalimov Apr 12 '12 at 10:45
  • 1
    Yes, it is. But you could of course do more complex things than just 'echo'. – JesperE Apr 13 '12 at 20:04
0

Found this post while looking up how to detect if docker is running from a Makefile...didnt find an example so I wrote one. Here is the GNUmakefile in context...

https://github.com/RandyMcMillan/make-docker-start

OS                                      :=$(shell uname -s)
export OS

.ONESHELL:
docker-start:##     detect whether docker is running...
    @( \
        while ! docker system info > /dev/null 2>&1; do\
        echo 'Waiting for docker to start...';\
        if [[ '$(OS)' == 'Linux' ]]; then\
         systemctl restart docker.service;\
        fi;\
        if [[ '$(OS)' == 'Darwin' ]]; then\
         open --background -a /./Applications/Docker.app/Contents/MacOS/Docker;\
        fi;\
    sleep 1;\
    done\
    )
docker-pull:docker-start##  pull alpine image
    docker pull alpine
RandyMcMillan
  • 59
  • 2
  • 3