Implementing `make check` or `make test` - testing

How can I implement a simple regression test framework with Make? (I’m using GNU Make, if that matters.)
My current makefile looks something like this (edited for simplicity):
OBJS = jscheme.o utility.o model.o read.o eval.o print.o
%.o : %.c jscheme.h
gcc -c -o $# $<
jscheme : $(OBJS)
gcc -o $# $(OBJS)
.PHONY : clean
clean :
-rm -f jscheme $(OBJS)
I’d like to have a set of regression tests, e.g., expr.in testing a “good” expression & unrecognized.in testing a “bad” one, with expr.cmp & unrecognized.cmp being the expected output for each. Manual testing would look like this:
$ jscheme < expr.in > expr.out 2>&1
$ jscheme < unrecognized.in > unrecognized.out 2>&1
$ diff -q expr.out expr.cmp # identical
$ diff -q unrecognized.out unrecognized.cmp
Files unrecognized.out and unrecognized.cmp differ
I thought to add a set of rules to the makefile looking something like this:
TESTS = expr.test unrecognized.test
.PHONY test $(TESTS)
test : $(TESTS)
%.test : jscheme %.in %.cmp
jscheme < [something.in] > [something.out] 2>&1
diff -q [something.out] [something.cmp]
My questions:
• What do I put in the [something] placeholders?
• Is there a way to replace the message from diff with a message saying, “Test expr failed”?

Your original approach, as stated in the question, is best. Each of your tests is in the form of a pair of expected inputs and outputs. Make is quite capable of iterating through these and running the tests; there is no need to use a shell for loop. In fact, by doing this you are losing the opportunity to run your tests in parallel, and are creating extra work for yourself in order to clean up temp files (which are not needed).
Here's a solution (using bc as an example):
SHELL := /bin/bash
all-tests := $(addsuffix .test, $(basename $(wildcard *.test-in)))
.PHONY : test all %.test
BC := /usr/bin/bc
test : $(all-tests)
%.test : %.test-in %.test-cmp $(BC)
#$(BC) <$< 2>&1 | diff -q $(word 2, $?) - >/dev/null || \
(echo "Test $# failed" && exit 1)
all : test
#echo "Success, all tests passed."
The solution directly addresses your original questions:
The placeholders you're looking for are $< and $(word 2, $?) corresponding to the prerequisites %.test-in and %.test-cmp respectively. Contrary to the #reinierpost comment temp files are not needed.
The diff message is hidden and replaced using echo.
The makefile should be invoked with make -k to run all the tests regardless of whether an individual test fails or succeeds.
make -k all will only run if all the tests succeed.
We avoid enumerating each test manually when defining the all-tests variable by leveraging the file naming convention (*.test-in) and the GNU make functions for file names. As a bonus this means the solution scales to tens of thousands of tests out of the box, as the length of variables is unlimited in GNU make. This is better than the shell based solution which will fall over once you hit the operating system command line limit.

Make a test runner script that takes a test name and infers the input filename, output filename and smaple data from that:
#!/bin/sh
set -e
jscheme < $1.in > $1.out 2>&1
diff -q $1.out $1.cmp
Then, in your Makefile:
TESTS := expr unrecognised
.PHONY: test
test:
for test in $(TESTS); do bash test-runner.sh $$test || exit 1; done
You could also try implementing something like automake's simple test framework.

What I ended up with looks like this:
TESTS = whitespace list boolean character \
literal fixnum string symbol quote
.PHONY: clean test
test: $(JSCHEME)
for t in $(TESTS); do \
$(JSCHEME) < test/$$t.ss > test/$$t.out 2>&1; \
diff test/$$t.out test/$$t.cmp > /dev/null || \
echo Test $$t failed >&2; \
done
It’s based on Jack Kelly’s idea, with Jonathan Leffler’s tip included.

I'll address just your question about diff. You can do:
diff file1 file2 > /dev/null || echo Test blah blah failed >&2
although you might want to use cmp instead of diff.
On another note, you might find it helpful to go ahead and take
the plunge and use automake. Your Makefile.am (in its entirety)
will look like:
bin_PROGRAMS = jscheme
jscheme_SOURCES = jscheme.c utility.c model.c read.c eval.c print.c jscheme.h
TESTS = test-script
and you will get a whole lot of really nice targets for free, including a pretty full-featured test framework.

Related

Assigning variables in Makefile recipe

In one of my Makefile recipes, I want to create a temporary file, pass the name of that file to a shell command and assign the output of that command to a make variable so that I can use that subsequently. For the life of me, I cannot get it to work.
For the purpose of debugging I have tried to boil down the problem to the most simple target I could come up with:
.PHONY: foo
foo:
$(eval TMPFILE = $(shell mktemp -p ./))
dd if=/dev/random of=${TMPFILE} bs=1 count=512
$(eval FOO = $(shell wc -c ${TMPFILE}))
#echo FOO: ${FOO}
Here is what happens:
❯ make foo
dd if=/dev/random of=./tmp.K1au4WrZ76 bs=1 count=512
512+0 records in
512+0 records out
512 bytes copied, 0.00287818 s, 178 kB/s
FOO: 0 ./tmp.K1au4WrZ76
So somehow, wc thinks the file is empty. But when I check the TMPFILE it has 512 bytes, as expected:
❯ wc -c tmp.K1au4WrZ76
512 tmp.K1au4WrZ76
Can someone, please enlighten me what is going on here and how to do that correctly?
Thanks
Phil
Update: Based on the answer I put together this target which works as desired:
.PHONEY: foo
.ONESHELL:
foo:
set -e
TMPFILE=`mktemp -p ./`
dd if=/dev/random of=$$TMPFILE bs=1 count=512
FOO=`wc -c $$TMPFILE`
#echo FOO: $$FOO
Thanks!
Make always expands the entire recipe (all lines of the recipe) first, before it starts any shell commands. So all your eval, etc. operations are invoked before any shell command, such as dd, is run.

How to run awk script on multiple files

I need to run a command on hundreds of files and I need help to get a loop to do this:
have a list of input files /path/dir/file1.csv, file2.csv, ..., fileN.csv
need to run a script on all those input files
script is something like: command input=/path/dir/file1.csv output=output1
I have tried things like:
for f in /path/dir/file*.csv; do command, but how do I get to read and write the new file every time?
Thank you....
Try this, (after changing /path/to/data to the correct path. Same with /path/to/awkscript and other places, pointing to your test data.)
#!/bin/bash
cd /path/to/data
for f in *.csv ; do
echo "awk -f /path/to/awkscript \"$f\" > ${f%.csv}.rpt"
#remove_me awk -f /path/to/awkscript "$f" > ${f%.csv}.rpt
done
make the script "executable" with
chmod 755 myScript.sh
The echo version will help you ensure the script is going to work as expected. You still have to carefully examine that output OR work on a copy of your data so you don't wreck you base-line data.
You could take the output of the last iteration
awk -f /path/to/awkscript myFileLast.csv > myFileLast.rpt
And copy/paste to cmd-line to confirm it will work.
WHen you comfortable that the awk script works as you need, then comment out the echo awk .. line, and uncomment the word #remove_me (and save your bash script).
for f in /path/to/files/*.csv ; do
bname=`basename $f`
pref=${bname%%.csv}
awk -f /path/to/awkscript $f > /path/to/store/output/${pref}_new.txt
done
Hopefully this helps, I am on my blackberry so there may be typos

Grep query in C shell script not performing properly

When I run the grep command on the command prompt, the output is correct. However, when I run it as part of a script, I only get partial output. Does anyone know what is wrong with this programme?
#!/bin/csh
set res = `grep -E "OPEN *(OUTPUT|INPUT|I-O|EXTEND)" ~/work/lst/TXT12UPD.lst`
echo $res
Your wildcard is probably being processed by the shell calling awk rather than as part of the awk script.
try escaping the * with a \ (i.e. \*)

Makefile variable initialization and export

somevar := apple
export somevar
update := $(shell echo "v=$$somevar")
all:
#echo $(update)
I was hoping to apple as output of command, however it's empty, which makes me think export and := variable expansion taking place on different phases. how to overcome this?
The problem is that export exports the variable to the subshells used by the commands; it is not available for expansion in other assignments. So don't try to get it from the environment outside a rule.
somevar := apple
export somevar
update1 := $(shell perl -e 'print "method 1 $$ENV{somevar}\n"')
# Make runs the shell command, the shell does not know somevar, so update1 is "method 1 ".
update2 := perl -e 'print "method 2 $$ENV{somevar}\n"'
# Now update2 is perl -e 'print "method 2 $$ENV{somevar}\n"'
# Lest we forget:
update3 := method 3 $(somevar)
all:
echo $(update1)
$(update2)
echo $(update3)
perl -e 'print "method 4 $$ENV{somevar}\n"'
The output is:
echo method 1
method 1
perl -e 'print "method 2 $ENV{somevar}\n"'
method 2 apple
echo method 3 apple
method 3 apple
perl -e 'print "method 4 $ENV{somevar}\n"'
method 4 apple
#Beta's answer contains the crucial pointer: with GNU make, variables marked with export are only available to [the shells launched for] recipe commands (commands that are part of rules), regrettably not to invocations of $(shell ...) (they only see the environment that make itself saw when it was launched).
There is a workaround, however: explicitly pass the exported variable as an environment variable to the shell function:
update := $(shell somevar='$(somevar)' perl -e 'print "$$ENV{somevar}"')
By prepending the shell command with <var>=<val>, that definition is added as an environment variable to the environment that the command sees - this is a generic shell feature.
Caveat: #Kaz points out in a comment that this method misbehaves if $(somevar) contains certain chars., because the variable expansion is verbatim (no escaping), which can break the resulting shell command, and suggests the following variant to also work with embedded ' instances (breaks the input value into single-quoted substrings with quoted ' spliced in):
update := $(shell somevar='$(subst ','\'',$(somevar))' perl -e 'print "$$ENV{somevar}"')
This should work with all values except multi-line ones (which are rare; there is no workaround for multi-line values that I'm aware of).
On a side note, literal $ chars. in values must be represented as $$, otherwise make will interpret them as references to its own variables.
Note that I've deliberately NOT chosen the OP's original statement, update := $(shell echo "v=$$somevar"), for demonstration, because it contains a pitfall that muddles the issue: due to how the shell evaluates a command line, somevar=apple echo v=$somevar does NOT evaluate to v=apple, because the $somevar reference is expanded before somevar=apple takes effect. To achieve the desired effect in this case, you'd have to use 2 statements: update := $(shell export somevar="$(somevar)"; echo "v=$$somevar")
As for the bug-vs.-feature debate:
While it can be argued that the shell function should see the same environment as recipe commands, the documentation makes no such promise - see http://www.gnu.org/software/make/manual/make.html#Shell-Function. Conversely, http://www.gnu.org/software/make/manual/make.html#Variables_002fRecursion only mentions making exported variables available to recipe commands.
Running the makefile
foo:=apple
export foo
all:
#echo ">"$(shell echo "$$foo")
#echo ">""$$foo"
gives for me (with foo undefined in the environment)
$ make
>
>apple
$ make foo=bar
>
>apple
$ export foo=bar; make
>bar
>apple
$ export foo=bar; make foo=bar
>bar
>bar
Try using the quoted form (update := "v=$$somevar") and let the shell handle expansion when a command is run (you'll still need the export)
Although export does not play nicely with $(shell ...), there is a simple workaround. We can pass the data to the shell script via the command line.
Now of course, environment passage is robust against issues of escaping and quoting. However, the shell language has a single quote quoting method '...' which handles everything. The only problem is that there is no way to get a single quote in there; but of course that is solved by terminating the quote, backslash-escaping the needed single quote and starting a new quote: In other words:
ab'cd -> 'ab'\''cd'
In the shell script executed by $(shell ...) we just generate a variable assignment of the form var='$(...)', where $(...) is some make expression that interpolates suitably escaped material. Thus, Makefile:
somevar := apple with 'quoted' "stuff" and dollar $$signs
shell_escape = $(subst ','\'',$(1))
update := $(shell v='$(call shell_escape,$(somevar))'; echo $$v > file.txt)
.phony: all
all:
cat file.txt
Sample run:
$ make
cat file.txt
apple with 'quoted' "stuff" and dollar $signs
If we want to communicate an environment variable to a command, we can do that using the shell syntax VAR0=val0 VAR1=val1 ... VARn=valn command arg .... This can be illustrated by some minor alterations to the above Makefile:
somevar := apple with 'quoted' "stuff" and dollar $$signs
shell_escape = $(subst ','\'',$(1))
update := $(shell somevar='$(call shell_escape,$(somevar))' env > file.txt)
.phony: all
all:
grep somevar file.txt
Run:
$ make
grep somevar file.txt
somevar=apple with 'quoted' "stuff" and dollar $signs
file.txt contains a dump of environment variables, where we can see somevar. If export in GNU Make did the right thing, we would have been able to just do:
export somevar
update := $(shell env > file.txt)
but the end result is the same.
Since the end result you want is to echo $(update), you would shell_escape anyway, even if GNU Make passed exported vars to $(shell ...). That is to say, look at one more Makefile:
somevar := apple with 'quoted' "stuff" and dollar $$signs
shell_escape = $(subst ','\'',$(1))
update := $(shell v='$(call shell_escape,$(somevar))'; echo $$v)
.phony: all
all:
#echo '$(call shell_escape,$(update))'
#echo $(update)
Output:
apple with 'quoted' "stuff" and dollar $signs
apple with quoted stuff and dollar

Is it possible to create a multi-line string variable in a Makefile

I want to create a makefile variable that is a multi-line string (e.g. the body of an email release announcement). something like
ANNOUNCE_BODY="
Version $(VERSION) of $(PACKAGE_NAME) has been released
It can be downloaded from $(DOWNLOAD_URL)
etc, etc"
But I can't seem to find a way to do this. Is it possible?
Yes, you can use the define keyword to declare a multi-line variable, like this:
define ANNOUNCE_BODY
Version $(VERSION) of $(PACKAGE_NAME) has been released.
It can be downloaded from $(DOWNLOAD_URL).
etc, etc.
endef
The tricky part is getting your multi-line variable back out of the makefile. If you just do the obvious thing of using "echo $(ANNOUNCE_BODY)", you'll see the result that others have posted here -- the shell tries to handle the second and subsequent lines of the variable as commands themselves.
However, you can export the variable value as-is to the shell as an environment variable, and then reference it from the shell as an environment variable (NOT a make variable). For example:
export ANNOUNCE_BODY
all:
#echo "$$ANNOUNCE_BODY"
Note the use of $$ANNOUNCE_BODY, indicating a shell environment variable reference, rather than $(ANNOUNCE_BODY), which would be a regular make variable reference. Also be sure to use quotes around your variable reference, to make sure that the newlines aren't interpreted by the shell itself.
Of course, this particular trick may be platform and shell sensitive. I tested it on Ubuntu Linux with GNU bash 3.2.13; YMMV.
Another approach to 'getting your multi-line variable back out of the makefile' (noted by Eric Melski as 'the tricky part'), is to plan to use the subst function to replace the newlines introduced with define in your multi-line string with \n. Then use -e with echo to interpret them. You may need to set the .SHELL=bash to get an echo that does this.
An advantage of this approach is that you also put other such escape characters into your text and have them respected.
This sort of synthesizes all the approaches mentioned so far...
You wind up with:
define newline
endef
define ANNOUNCE_BODY
As of $(shell date), version $(VERSION) of $(PACKAGE_NAME) has been released.
It can be downloaded from $(DOWNLOAD_URL).
endef
someTarget:
echo -e '$(subst $(newline),\n,${ANNOUNCE_BODY})'
Note the single quotes on the final echo are crucial.
Assuming you only want to print the content of your variable on standard output, there is another solution :
do-echo:
$(info $(YOUR_MULTILINE_VAR))
Yes. You escape the newlines with \:
VARIABLE="\
THIS IS A VERY LONG\
TEXT STRING IN A MAKE VARIABLE"
update
Ah, you want the newlines? Then no, I don't think there's any way in vanilla Make. However, you can always use a here-document in the command part
[This does not work, see comment from MadScientist]
foo:
echo <<EOF
Here is a multiple line text
with embedded newlines.
EOF
Not completely related to the OP, but hopefully this will help someone in future.
(as this question is the one that comes up most in google searches).
In my Makefile, I wanted to pass the contents of a file, to a docker build command,
after much consternation, I decided to:
base64 encode the contents in the Makefile (so that I could have a single line and pass them as a docker build arg...)
base64 decode the contents in the Dockerfile (and write them to a file)
see example below.
nb: In my particular case, I wanted to pass an ssh key, during the image build, using the example from https://vsupalov.com/build-docker-image-clone-private-repo-ssh-key/ (using a multi stage docker build to clone a git repo, then drop the ssh key from the final image in the 2nd stage of the build)
Makefile
...
MY_VAR_ENCODED=$(shell cat /path/to/my/file | base64)
my-build:
#docker build \
--build-arg MY_VAR_ENCODED="$(MY_VAR_ENCODED)" \
--no-cache \
-t my-docker:build .
...
Dockerfile
...
ARG MY_VAR_ENCODED
RUN mkdir /root/.ssh/ && \
echo "${MY_VAR_ENCODED}" | base64 -d > /path/to/my/file/in/container
...
Why don't you make use of the \n character in your string to define the end-of-line? Also add the extra backslash to add it over multiple lines.
ANNOUNCE_BODY=" \n\
Version $(VERSION) of $(PACKAGE_NAME) has been released \n\
\n\
It can be downloaded from $(DOWNLOAD_URL) \n\
\n\
etc, etc"
Just a postscript to Eric Melski's answer: You can include the output of commands in the text, but you must use the Makefile syntax "$(shell foo)" rather than the shell syntax "$(foo)". For example:
define ANNOUNCE_BODY
As of $(shell date), version $(VERSION) of $(PACKAGE_NAME) has been released.
It can be downloaded from $(DOWNLOAD_URL).
endef
You should use "define/endef" Make construct:
define ANNOUNCE_BODY
Version $(VERSION) of $(PACKAGE_NAME) has been released.
It can be downloaded from $(DOWNLOAD_URL).
etc, etc.
endef
Then you should pass value of this variable to shell command. But, if you do this using Make variable substitution, it will cause command to split into multiple:
ANNOUNCE.txt:
echo $(ANNOUNCE_BODY) > $# # doesn't work
Qouting won't help either.
The best way to pass value is to pass it via environment variable:
ANNOUNCE.txt: export ANNOUNCE_BODY:=$(ANNOUNCE_BODY)
ANNOUNCE.txt:
echo "$${ANNOUNCE_BODY}" > $#
Notice:
Variable is exported for this particular target, so that you can reuse that environment will not get polluted much;
Use environment variable (double qoutes and curly brackets around variable name);
Use of quotes around variable. Without them newlines will be lost and all text will appear on one line.
This doesn't give a here document, but it does display a multi-line message in a way that's suitable for pipes.
=====
MSG = this is a\\n\
multi-line\\n\
message
method1:
#$(SHELL) -c "echo '$(MSG)'" | sed -e 's/^ //'
=====
You can also use Gnu's callable macros:
=====
MSG = this is a\\n\
multi-line\\n\
message
method1:
#echo "Method 1:"
#$(SHELL) -c "echo '$(MSG)'" | sed -e 's/^ //'
#echo "---"
SHOW = $(SHELL) -c "echo '$1'" | sed -e 's/^ //'
method2:
#echo "Method 2:"
#$(call SHOW,$(MSG))
#echo "---"
=====
Here's the output:
=====
$ make method1 method2
Method 1:
this is a
multi-line
message
---
Method 2:
this is a
multi-line
message
---
$
=====
With GNU Make 3.82 and above, the .ONESHELL option is your friend when it comes to multiline shell snippets. Putting together hints from other answers, I get:
VERSION = 1.2.3
PACKAGE_NAME = foo-bar
DOWNLOAD_URL = $(PACKAGE_NAME).somewhere.net
define nwln
endef
define ANNOUNCE_BODY
Version $(VERSION) of $(PACKAGE_NAME) has been released.
It can be downloaded from $(DOWNLOAD_URL).
etc, etc.
endef
.ONESHELL:
# mind the *leading* <tab> character
version:
#printf "$(subst $(nwln),\n,$(ANNOUNCE_BODY))"
Make sure, when copying and pasting the above example into your editor, that any <tab> characters are preserved, else the version target will break!
Note that .ONESHELL will cause all targets in the Makefile to use a single shell for all their commands.
GNU `make' manual, 6.8: Defining Multi-Line Variables
In the spirit of .ONESHELL, it's possible to get pretty close in .ONESHELL challenged environments:
define _oneshell_newline_
endef
define oneshell
#eval "$$(printf '%s\n' '$(strip \
$(subst $(_oneshell_newline_),\n, \
$(subst \,\/, \
$(subst /,//, \
$(subst ','"'"',$(1))))))' | \
sed -e 's,\\n,\n,g' -e 's,\\/,\\,g' -e 's,//,/,g')"
endef
An example of use would be something like this:
define TEST
printf '>\n%s\n' "Hello
World\n/$$$$/"
endef
all:
$(call oneshell,$(TEST))
That shows the output (assuming pid 27801):
>
Hello
World\n/27801/
This approach does allow for some extra functionality:
define oneshell
#eval "set -eux ; $$(printf '%s\n' '$(strip \
$(subst $(_oneshell_newline_),\n, \
$(subst \,\/, \
$(subst /,//, \
$(subst ','"'"',$(1))))))' | \
sed -e 's,\\n,\n,g' -e 's,\\/,\\,g' -e 's,//,/,g')"
endef
These shell options will:
Print each command as it is executed
Exit on the first failed command
Treat use of undefined shell variables as an error
Other interesting possibilities will likely suggest themselves.
I like alhadis's answer best. But to keep columnar formatting, add one more thing.
SYNOPSIS := :: Synopsis: Makefile\
| ::\
| :: Usage:\
| :: make .......... : generates this message\
| :: make synopsis . : generates this message\
| :: make clean .... : eliminate unwanted intermediates and targets\
| :: make all ...... : compile entire system from ground-up\
endef
Outputs:
:: Synopsis: Makefile
::
:: Usage:
:: make .......... : generates this message
:: make synopsis . : generates this message
:: make clean .... : eliminate unwanted intermediates and targets
:: make all ...... : compile entire system from ground-up
Not really a helpful answer, but just to indicate that 'define' does not work as answered by Ax (did not fit in a comment):
VERSION=4.3.1
PACKAGE_NAME=foobar
DOWNLOAD_URL=www.foobar.com
define ANNOUNCE_BODY
Version $(VERSION) of $(PACKAGE_NAME) has been released
It can be downloaded from $(DOWNLOAD_URL)
etc, etc
endef
all:
#echo $(ANNOUNCE_BODY)
It gives an error that the command 'It' cannot be found, so it tries to interpret the second line of ANNOUNCE BODY as a command.
It worked for me:
ANNOUNCE_BODY="first line\\nsecond line"
all:
#echo -e $(ANNOUNCE_BODY)
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 "$(PROFILE)" | sed -e 's/^[ ]//' >.profile
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.
I believe the safest answer for cross-platform use would be to use one echo per line:
ANNOUNCE.txt:
rm -f $#
echo "Version $(VERSION) of $(PACKAGE_NAME) has been released" > $#
echo "" >> $#
echo "It can be downloaded from $(DOWNLOAD_URL)" >> $#
echo >> $#
echo etc, etc" >> $#
This avoids making any assumptions of on the version of echo available.
Use string substitution:
VERSION := 1.1.1
PACKAGE_NAME := Foo Bar
DOWNLOAD_URL := https://go.get/some/thing.tar.gz
ANNOUNCE_BODY := Version $(VERSION) of $(PACKAGE_NAME) has been released. \
| \
| It can be downloaded from $(DOWNLOAD_URL) \
| \
| etc, etc
Then in your recipe, put
#echo $(subst | ,$$'\n',$(ANNOUNCE_BODY))
This works because Make is substituting all occurrences of |  (note the space) and swapping it with a newline character ($$'\n'). You can think of the equivalent shell-script invocations as being something like this:
Before:
$ echo "Version 1.1.1 of Foo Bar has been released. | | It can be downloaded from https://go.get/some/thing.tar.gz | | etc, etc"
After:
$ echo "Version 1.1.1 of Foo Bar has been released.
>
> It can be downloaded from https://go.get/some/thing.tar.gz
>
> etc, etc"
I'm not sure if $'\n' is available on non-POSIX systems, but if you can gain access to a single newline character (even by reading a string from an external file), the underlying principle is the same.
If you have many messages like this, you can reduce noise by using a macro:
print = $(subst | ,$$'\n',$(1))
Where you'd invoke it like this:
#$(call print,$(ANNOUNCE_BODY))
Hope this helps somebody. =)
As an alternative you can use the printf command. This is helpful on OSX or other platforms with less features.
To simply output a multiline message:
all:
#printf '%s\n' \
'Version $(VERSION) has been released' \
'' \
'You can download from URL $(URL)'
If you are trying to pass the string as an arg to another program, you can do so like this:
all:
/some/command "`printf '%s\n' 'Version $(VERSION) has been released' '' 'You can download from URL $(URL)'`"