10

Does anyone know how to use a here-document redirection on a recipe?

test:
  sh <<EOF
  echo I Need This
  echo To Work
  ls
  EOF

I can't find any solution trying the usual backslash method (which basically ends with a command in a single line).

Rationale:

I have a set of multi-line recipes that I want to proxy through another command (e.g., sh, docker).

onelinerecipe := echo l1
define twolinerecipe :=
echo l1
echo l2
endef
define threelinerecipe :=
echo l1
echo l2
echo l3
endef

# sh as proxy command and proof of concept
proxy := sh

test1:
  $(proxy) <<EOF
  $(onelinerecipe)
  EOF

test2:
  $(proxy) <<EOF
  $(twolinerecipe)
  EOF

test3:
  $(proxy) <<EOF
  $(threelinerecipe)
  EOF

The solution I would love to avoid: transform multiline macros into single lines.

define threelinerecipe :=
echo l1;
echo l2;
echo l3
endef

test3:
  $(proxy) <<< "$(strip $(threelinerecipe))"

This works (I use gmake 4.0 and bash as make's shell) but it requires changing my recipes and I have a lot. Strip removes the newlines, from the macro, then everything is written in a single line.

My end goal is: proxy := docker run ...

imz -- Ivan Zakharyaschev
  • 4,921
  • 6
  • 53
  • 104
Julio Guerra
  • 5,523
  • 9
  • 51
  • 75

4 Answers4

6

Using the line .ONESHELL: somewhere in your Makefile will send all recipe lines to a single shell invocation, you should find your original Makefile works as expected.

user657267
  • 20,568
  • 5
  • 58
  • 77
  • 4
    Important missing notes: its effect is global and not local to a single rule and recipe. If it appears anywhere in the makefile then **all** recipe lines **for each** target will be provided to a single invocation of the shell. It doesn't introduce problems in my case. But also note that `.SHELLFLAGS = -ec` is required to keep the default make behaviour of leaving on error. – Julio Guerra Feb 20 '16 at 08:22
  • 1
    I have been using this solution for some days now and the only drawback I had so far is echoing issues. Because with `.ONESHELL:`, leading `@` are now valid for the entire recipe. This makes selective echoing (echo one command and not another) no longer possible. To solve this, you can only change your recipe, thus breaking a rule I was looking for: proxy transparently. Anyway, this is the only solution allowing here-doc, so I am validating it. – Julio Guerra Feb 22 '16 at 09:18
4

When make sees a multi-line block in a recipe (i.e., a block of lines all ending in \, apart from the last), it passes that block un-modifed to the shell. This generally works in bash, apart from here docs.

One way around this is to strip any trailing \s, then pass the resulting string to bash's eval. You do this in make by playing with ${.SHELLFLAGS} and ${SHELL}. You can use both of these in target-specific form if you only want it to kick in for a few targets.

.PHONY: heredoc

heredoc: .SHELLFLAGS = -c eval
heredoc: SHELL = bash -c 'eval "$${@//\\\\/}"'

heredoc:
    @echo First
    @cat <<-there \
        here line1 \
        here anotherline \
    there
    @echo Last

giving

$ make
First
here line1
here anotherline
Last

Careful with that quoting, Eugene. Note the cheat here: I am removing all backslashes, not just the ones at the ends of the line. YMMV.

bobbogo
  • 14,989
  • 3
  • 48
  • 57
  • In particular, thanks for the "When make sees a multi-line block in a recipe (i.e., a block of lines all ending in \, apart from the last), it passes that block un-modifed to the shell." bit: I've tries do use a here-doc where it is solved by that technique alone. – kostix Dec 05 '17 at 10:04
4

With GNU make, you can combine multi-line variables with the export directive to use a multi-line command without having to turn on .ONESHELL globally:

define script
cat <<'EOF'
here document in multi-line shell snippet
called from the "$@" target
EOF
endef
export script

run:; @ eval "$$script"

will give

here document in multi-line shell snippet
called from the "run" target

You can also combine it with the value function to prevent its value from being expanded by make:

define _script
cat <<EOF
SHELL var expanded by the shell to $SHELL, pid is $$
EOF
endef
export script = $(value _script)

run:; @ eval "$$script"

will give

SHELL var expanded by the shell to /bin/sh, pid is 12712
  • Using the combined `export define` syntax resulted in a _“missing separator”_ error with my `make` version on macOS (GNU Make 3.81). To avoid that error, I needed to do just `define script` first, and then `export script` separately — as in the answers at https://stackoverflow.com/a/7377522/441757 and https://unix.stackexchange.com/a/516476/130371. Maybe the combined `export define` syntax isn’t supported in version 3 — but only supported in version 4+? – sideshowbarker Aug 18 '22 at 03:15
  • 1
    @sideshowbarker yes, that [feature](https://git.savannah.gnu.org/cgit/make.git/commit/?id=5b4d419476e9fbda8ea26017f6ec15956d103ed9) only appeared in version 3.82 –  Aug 19 '22 at 04:54
0

Not a here doc but this might be a useful workaround. And it doesn’t require any GNU Make’isms. Put the lines in a subshell with parens, prepend each line with echo. You’ll need trailing sloshes and semi-colon and slosh where appropriate.

test:
( \
    echo echo I Need This ;\
    echo echo To Work ;\
    echo ls \
) \
| sh
Matthew Hannigan
  • 1,487
  • 1
  • 14
  • 17