Heredoc in a Makefile?
Asked Answered
C

8

36

Is this possible at all and how?

Update: I need this because I create a file both from dynamic and static data.

Use case: I have a test directory. Each C file produces a test executable. With

SRCS = $(wildcard [a-z]*.c)

I can add new tests as needed and make will find the new tests, compile, run and valgrind them. I also use git. I would like .gitignoreto include the executables.

So there. How to create .gitignore and include static data, i.e. the files I want to be ignored (*.o and depend) and also the executables dynamically?

Cinemascope answered 3/5, 2011 at 17:2 Comment(0)
T
40

Another GNU Make solution.

You can do it using the define and export commands as follows:

define GITIGNOREDS
*.o
depend
endef

SRCS = $(wildcard [a-z]*.c)
EXES = $(SRCS:.c=)


export GITIGNOREDS
.gitignore: $(SRCS)
    echo $(EXES) | sed 's/ /\n/g' > $@
    echo "$$GITIGNOREDS" >> $@

You have to be careful of make expansions (i.e. $(x)) inside the define block though.

Tonsure answered 11/9, 2011 at 10:8 Comment(0)
T
31

Yes, you can. As others note, you probably shouldn't, but you can. Ash's answer has one solution involving define commands, but that is combersome and can make it tricky to get variables expanded to the right values. Another trick is to use the .ONESHELL: special target.

Sometimes you would prefer that all the lines in the recipe be passed to a single invocation of the shell. There are generally two situations where this is useful: first, it can improve performance in makefiles where recipes consist of many command lines, by avoiding extra processes. Second, you might want newlines to be included in your recipe command (for example perhaps you are using a very different interpreter as your SHELL). If the .ONESHELL special target appears anywhere in the makefile then all recipe lines for each target will be provided to a single invocation of the shell. Newlines between recipe lines will be preserved.

A couple words of warning:

  • This will affect how all recipes in your Makefile work.
  • Line prefix operators such as - or @ to suppress output or ignore errors only work on the first line of all recipes and take effect for all lines following.
  • Some old versions or GNU Make (such as that running by default on Travis's CI system even in the new containers) may ignore this directive leading to strange bugs and gotchas. Make sure you know your target environments.

With that out of the way, here's how it might look to generate a markdown file:

SHELL = bash
.ONESHELL:
MYVAR = "Some Title"

file.md:
    cat <<- EOF > $@
        $(MYVAR)
        ========

        This stuff will all be written to the target file. Be sure
        to escape dollar-signs and backslashes as Make will be scanning
        this text for variable replacements before bash scans it for its
        own strings.

        Otherwise formatting is just as in any other bash heredoc. Note
        I used the <<- operator which allows for indentation. This markdown
        file will not have whitespace at the start of lines.

        Here is a programmatic way to generate a markdwon list all PDF files
        in the current directory:

        `find -maxdepth 1 -name '*.pdf' -exec echo " + {}" \;`
    EOF

Note one additional gotcha is that Make skips blank lines. If having a blank line in the content of your heredoc is important, you need to make sure to indent that line with the appropriate level of whitespace to match the heredoc or Make will eat it and not even pass it to cat!

Triplet answered 22/3, 2016 at 7:41 Comment(2)
just to add a note. oneshell doesnt always work. bash 3.2 which is default on quite a few systems ignores oneshell.Constrictor
@JimmyMGLim Bash does not ingore ONESHELL. Perhaps some old versions of make could (or perhaps you don't have GNU make at all, you have a substitute), but bash as a shell has always handled multiline input.Triplet
S
7

GNU Makefile can do things like the following. It is ugly, and I won't say you should do it, but I do in certain situations.

.profile = \
\#!/bin/sh.exe\n\
\#\n\
\# A MinGW equivalent for .bash_profile on Linux.  In MinGW/MSYS, the file\n\
\# is actually named .profile, not .bash_profile.\n\
\#\n\
\# Get the aliases and functions\n\
\#\n\
if [ -f \$${HOME}/.bashrc ]\n\
then\n\
  . \$${HOME}/.bashrc\n\
fi\n\
\n\
export CVS_RSH="ssh"\n  
#
.profile:
        echo -e "$($(@))" | sed -e 's/^[ ]//' >$(@)

make .profile creates a .profile file if one does not exist.

This solution was used where the application will only use GNU Makefile in a POSIX shell environment. The project is not an open source project where platform compatibility is an issue.

The goal was to create a Makefile that facilitates both setup and use of a particular kind of workspace. The Makefile brings along with it various simple resources without requiring things like another special archive, etc. It is, in a sense, a shell archive. A procedure can then say things like drop this Makefile in the folder to work in. Set up your workspace enter make workspace, then to do blah, enter make blah, etc.

What can get tricky is figuring out what to shell quote. The above does the job and is close to the idea of specifying a here document in the Makefile. Whether it is a good idea for general use is a whole other issue.

Soften answered 21/7, 2011 at 20:28 Comment(0)
R
4

It depends on how determined you are. It is best to assume it won't work. Write a shell script to be launched instead of a here document in the makefile.

Fails 1

heredoc:
    cat - <<!
    This is the heredoc.
    !

This produces:

cat - <<!
This is the heredoc.
make: This: No such file or directory
make: *** [heredoc] Error 1

Each line is executed separately - oops.

Fails 2

heredoc:
    cat - <<! \
    This is the heredoc.\
    !

This generated:

cat: This: No such file or directory
cat: is: No such file or directory
cat: the: No such file or directory
cat: heredoc.!: No such file or directory
make: *** [heredoc] Error 1

There may be methods using a specific version of make (GNU make, for example, can execute all commands for an action in a single subshell, I believe), but then you have to specify your portability requirements. For regular (say POSIX-compliant) make, assume here docs do not work.

Rior answered 3/5, 2011 at 17:8 Comment(1)
+1 thanks for showing in detail why Heredocs in Makefiles are a bad idea.Cinemascope
E
4

If you're using Gnu Make, use a 'define' to create a multi-line text variable, and a rule to echo it into your file. See 6.8 Defining Multi-Line Variables of https://www.gnu.org/software/make/manual/make.html#Appending

Like this:

  define myvar
  # line 1\nline 2\nline 3\n#etc\n
  endef

  myfile.txt:
         /bin/echo -e "$(myvar)) >myfile.txt

To create this, it helps to use an editor, create the file you want to have, append "\n" to the end of every line, and then join them all into a single string. Paste that into your makefile.

Tested with GNU Make 3.81 on linux.

Emplane answered 10/11, 2014 at 18:41 Comment(3)
Did you read Ash Berlin's answer from three years ago? Yours is same as his.Cinemascope
Mine is simpler and doesn't need the 'export' his is using. Mine is different than his.Emplane
this worked great for me after i changed the echo command, using a bash wrapper for the echo: bash -c 'echo -e "$(myvar)"' >myfile.txtBernadinebernadotte
T
1

As close to a heredoc as I could get:

Makefile:

switch-version:
    @printf "services:\n\
      myservice:\n\
        build:\n\
          args:\n\
            my_version: $(version)\n\
            some_other_arg: foo\n\
        image: my_image\n\
    " > docker-compose.override.yml

Result:

$ make switch-version version=1.2.3
$ cat docker-compose.override.yml 
services:
  myservice:
    build:
      args:
        my_version: 1.2.3
        some_other_arg: foo
    image: my_image
$ 
Terribly answered 31/8, 2022 at 2:25 Comment(0)
C
0
SRCS = (wildcard [a-z]*.c)
EXES = $(SRCS:.c=)
GITIGNOREDS = *.o depend

.gitignore: $(SRCS)
        echo $(EXES) | sed 's/ /\n/g' > $@
        echo $(GITIGNOREDS) | sed 's/ /\n/g' > $@

Important is the GITIGNOREDS line. I would have preferred a heredoc like

GITIGNOREDS = <<EOT
*.o
depend
EOT

but am also happy with a file list, separated by spaces, and a sed script to translate the space into newlines.

Edit As proposed by Ryan V. Bissell: Use a separate test subdirectory which you add to gitignore. And everything falls into place. It's simple.

Cinemascope answered 5/5, 2011 at 11:39 Comment(1)
I know this is an old question, but: did you ever consider that you could just have make send all the .o & executable files to a ./test/out subdirectory, and then just add a static './test/out' line to your .gitignore?Donata
R
-1

It appears there is no way to do a here-document in Makefile. However, there is a possible workaround. Use echo to send the here-document data:

all:
    echo "some text" | myScript.sh
Redan answered 9/5, 2017 at 16:19 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.