Josh Klodnicki
Josh Klodnicki

Reputation: 625

How to properly escape recipe newlines in multi-line variable?

The Question

Example 1

Consider the following user-defined function:

define func =
    if [ -f $(1) ]; then \
        printf "'%s' is a file\n" '$(1)'; \
        printf "This is a relatively long command that"; \
        printf " won't fit on one line\n"; \
    fi
endef

all:
    $(call func,foo)

This will output the following:

$ make
if [ -f foo ]; then printf "'%s' is a file\n" 'foo'; printf "This is a rel
atively long command that"; printf " won't fit on one line\n"; fi

For readability, I would like make to print the command on multiple lines, as written in the Makefile. How do I accomplish this?

What I've Tried

Example 2

The following works the way I want, but does not allow the parameterized function:

filename := foo

.PHONY: foo
foo:
    if [ -f $(filename) ]; then \
        printf "'%s' is a file\n" '$(filename)'; \
        printf "This is a relatively long command that"; \
        printf " won't fit on one line\n"; \
    fi

Output:

$ make foo
if [ -f foo ]; then \
        printf "'%s' is a file\n" 'foo'; \
        printf "This is a relatively long command that"; \
        printf " won't fit on one line\n"; \
fi

Example 3

My obvious first instinct was to escape the backslashes:

define func =
    if [ -f $(1) ]; then \\
        printf "'%s' is a file\n" '$(1)'; \\
        printf "This is a relatively long command that"; \\
        printf " won't fit on one line\n"; \\
    fi
endef

Output:

$ make
if [ -f foo ]; then \\
        printf "'%s' is a file\n" 'foo'; \\
        printf "This is a relatively long command that"; \\
        printf " won't fit on one line\n"; \\
fi
/bin/sh: \: command not found
'foo' is a file
/bin/sh: line 1: \: command not found
This is a relatively long command that/bin/sh: line 2: \: command not found
 won't fit on one line
/bin/sh: line 3: \: command not found
make: *** [Makefile:11: all] Error 127

Example 4

Okay, so why not try \\\?

$ make
if [ -f foo ]; then \ printf "'%s' is a file\n" 'foo'; \ printf "This is a
relatively long command that"; \ printf " won't fit on one line\n"; \ fi
/bin/sh: -c: line 1: syntax error: unexpected end of file
make: *** [Makefile:11: all] Error 1

Example 5

Interesting. Let's go for four...

$ make
if [ -f foo ]; then \\\\
        printf "'%s' is a file\n" 'foo'; \\\\
        printf "This is a relatively long command that"; \\\\
        printf " won't fit on one line\n"; \\\\
fi
/bin/sh: \\: command not found
'foo' is a file
/bin/sh: line 1: \\: command not found
This is a relatively long command that/bin/sh: line 2: \\: command not found
 won't fit on one line
/bin/sh: line 3: \\: command not found
make: *** [Makefile:11: all] Error 127

Now we're back to where we were last time.

What Works

Example 6

This is the only thing that seems to work:

define func =
    if [ -f $(1) ]; then #\\
        printf "'%s' is a file\n" '$(1)'; #\\
        printf "This is a relatively long command that"; #\\
        printf " won't fit on one line\n"; #\\
    fi
endef

Output:

$ make
if [ -f foo ]; then #\\
        printf "'%s' is a file\n" 'foo'; #\\
        printf "This is a relatively long command that"; #\\
        printf " won't fit on one line\n"; #\\
fi

But man, that looks ugly, and it feels hackish. There's got to be a better way to do this. Or am I just going about this the wrong way in the first place?

It seems to me that make is just confused by the magic that happens when escaping newlines within a recipe. The lines printed to the terminal during execution do not match what the shell sees. Should this be considered a bug?

I am using GNU Make 4.2.1 on Cygwin.

Edit

To clarify: make normally gives special treatment to trailing backslashes within a recipe. They do not indicate line continuation, as they do elsewhere. Instead, they indicate that multiple recipe lines are to be treated as a single command, and they are passed to the shell intact.

When not in a recipe, but defining a variable, this special treatment does not apply. The lines are simply joined, as in Example 1. This is expected.

I would expect that a double backslash would be translated to a single literal backslash in the variable, but instead both backslashes are retained. When the variable is expanded in the recipe, I would expect make to behave as if the recipe had \\ at the end of every line. If this were the case, each line would be executed separately. But as you can see from Examples 3 and 6, the lines are executed together.

The point is, it is possible to get magic backslash parsing from the expansion of a variable. The problem is the mechanics of this behavior are inconsistent and confusing.

Upvotes: 5

Views: 3686

Answers (3)

Josh Klodnicki
Josh Klodnicki

Reputation: 625

I found a nicer (albeit still hacky) solution that seems to work:

define func =
    if [ -f $(1) ]; then $\\
        printf "'%s' is a file\n" '$(1)'; $\\
        printf "This is a relatively long command that"; $\\
        printf " won't fit on one line\n"; $\\
    fi
endef

all:
    @echo vvvvvvvv
    $(call func,foo)
    @echo ^^^^^^^^

Output:

$ make
vvvvvvvv
if [ -f foo ]; then \
        printf "'%s' is a file\n" 'foo'; \
        printf "This is a relatively long command that"; \
        printf " won't fit on one line\n"; \
fi
'foo' is a file
This is a relatively long command that won't fit on one line
^^^^^^^^

I think this is how it works:

  1. During the first scan for line continuations, the $ is ignored, so the parser sees \\, assumes the backslash is escaped, and leaves the lines intact.

  2. When expanding the function, $\ is recognized as a variable. Assuming you haven't actually assigned a variable named \, this expands to nothing.

  3. Now we are left with a single backslash at the end of the line, which is treated as if it were literally typed into the recipe.

Upvotes: 1

bobbogo
bobbogo

Reputation: 15493

Urk! This is due to make's noddy parser.

Recipes are stored as-is. They are expanded as-and-when make is about to call the shell. Once the entire recipe has been expanded, the first line is passed to a shell. If that command succeeds, then the second is run. Wash, rinse, repeat. Backslashes at the end of line are preserved, with the effect that the following line is passed at the same time as the first.

In recursive variable definition however, backslashes at the end of line are removed as the definition is read

define oneline =
  aa \
  bb \
  cc
endef

$(error [$(value oneline)])

which gives

$ make Makefile:9: *** [ aa bb cc]. Stop.

What we are aiming for

We need to massage make's syntax so that a variable expands to exactly this text:

target:
    if [ -f foo ]; then \
        printf "'%s' is a file\n" 'foo'; \
        printf "This is a relatively long command that"; \
        printf " won't fit on one line\n"; \
    fi

which we then simply feed to make via something like

$(eval ${var})

Backslashes

Just replace each newline with a space-backslash-newline-tab quad using $(subst).

A function to do that:

empty :=
tab := ${empty}   # Trailing tab
define \n :=


endef

stanza = $(subst ${\n}, \${\n}${tab})

To check it works:

define func =
  if [ -f $1 ]; then
    printf "'%s' is a file\n" '$1';
    printf "This is a relatively long command that";
    printf " won't fit on one line\n";
  fi
endef

$(error [$(call stanza,$(call func,foo))])

giving:

Makefile:23: *** [ if [ -f foo ]; then \ printf "'%s' is a file\n" 'foo'; \ printf "This is a relatively long command that"; \ printf " won't fit on one line\n"; \ fi]. Stop.

Note that the definition of func now has no annotation at the end of its lines.

Putting it all together

define \n :=


endef
empty :=
tab := ${empty}        # Trailing tab

stanza = $(subst ${\n}, \${\n}${tab},$1)

define func =
  if [ -f $1 ]; then
    printf "'%s' is a file\n" '$1';
    printf "This is a relatively long command that";
    printf " won't fit on one line\n";
  fi
endef

define rule =
  target:
        echo vvv
        $(call stanza,$(call func,foo))
        echo ^^^
endef

$(eval ${rule})

Leading to

$ touch foo; make -R --warn echo vvv vvv if [ -f foo ]; then \ printf "'%s' is a file\n" 'foo'; \ printf "This is a relatively long command that"; \ printf " won't fit on one line\n"; \ fi 'foo' is a file This is a relatively long command that won't fit on one line echo ^^^ ^^^

An interesting academic exercise. Still can't put literal backslashes in either :)

Upvotes: 3

Vroomfondel
Vroomfondel

Reputation: 2898

define newline :=
$(strip)
$(strip)
endef

define func =
    if [ -f $(1) ]; then \$(newline)\
       printf "'%s' is a file\n" '$(1)'; \$(newline)\
       printf "This is a relatively long command that"; \$(newline)\
       printf " won't fit on one line\n"; \$(newline)\
    fi
endef

func2 = if [ -f $(1) ]; then \$(newline)   printf "'%s' is a file\n" '$(1)'; \$(newline)   printf "This is a relatively long command that"; \$(newline)   printf " won't fit on one line\n"; \$(newline)fi

all:
    $(call func,foo)
    @echo --------------
    $(call func2,foo)

The first one seems to be space-stripped. The second looks nice at the output but.. oh well, seems like being stuck between a rock and a hard place :/

Upvotes: 1

Related Questions