是否可以在Makefile中创建多行字符串变量


122

我想创建一个makefile变量,该变量是多行字符串(例如,电子邮件发布公告的正文)。就像是

ANNOUNCE_BODY="
Version $(VERSION) of $(PACKAGE_NAME) has been released

It can be downloaded from $(DOWNLOAD_URL)

etc, etc"

但是我似乎找不到办法。可能吗?

Answers:


172

是的,您可以使用define关键字声明多行变量,如下所示:

define ANNOUNCE_BODY
Version $(VERSION) of $(PACKAGE_NAME) has been released.

It can be downloaded from $(DOWNLOAD_URL).

etc, etc.
endef

棘手的部分是将多行变量从makefile中取出。如果您只是使用“ echo $(ANNOUNCE_BODY)”做显而易见的事情,您将看到其他人在此处发布的结果-shell尝试将变量的第二行和后续行作为命令本身处理。

但是,您可以将变量值按原样作为环境变量导出到Shell,然后从Shell作为环境变量引用它(而不是make变量)。例如:

export ANNOUNCE_BODY
all:
    @echo "$$ANNOUNCE_BODY"

请注意,使用$$ANNOUNCE_BODY指示外壳环境变量引用,而不是$(ANNOUNCE_BODY),这将是常规的make变量引用。另外,请确保在变量引用周围使用引号,以确保换行符不会被外壳程序本身解释。

当然,此技巧可能对平台和外壳敏感。我在Ubuntu Linux上使用GNU bash 3.2.13对其进行了测试;YMMV。


1
export ANNOUNCE_BODY仅在规则内设置变量-不允许引用$$ ANNOUNCE_BODY来定义其他变量。
anatoly techtonik 2013年

@techtonik如果要ANNOUNCE_BODY在其他变量定义中使用值,则只需像其他make变量一样引用它即可。例如,OTHER=The variable ANNOUNCE_BODY is $(ANNOUNCE_BODY)。当然,export如果您想OTHER退出命令,仍然需要技巧。
埃里克·梅尔斯基

25

另一种“使多行变量从makefile中退回”的方法(被Eric Melski称为“棘手的部分”)是计划使用subst函数用替换define多行字符串中引入的换行符\n。然后使用-e with echo来解释它们。您可能需要设置.SHELL = bash以获得执行此操作的回显。

这种方法的优点是,您还可以将其他此类转义字符放入文本中并让它们受到尊重。

这种综合了到目前为止提到的所有方法。

您结束时:

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})'

请注意,最终回声上的单引号至关重要。


4
请注意,“ echo -e”不可移植。您可能应该更喜欢printf(1)。
MadScientist 2012年

2
好的答案,但是,我必须删除=after define ANNOUNCE_BODY才能运行它。
mschilli 2014年

13

假设您只想在标准输出上打印变量的内容,还有另一种解决方案:

do-echo:
    $(info $(YOUR_MULTILINE_VAR))

1
此禁止操作规则产生了不必要的消息:make: 'do-echo' is up to date.。通过使用“ no op”命令,我可以使其静音:@: $(info $(YOUR_MULTILINE_VAR))
Guillaume Papin

@GuillaumePapin有点晚了,但是您可以.PHONY用来告诉Makefile没有什么可以检查该规则的。Makefile最初是供编译器使用的,如果我没记错的话,make它也在做一些我不理解的魔术,以期望该规则不会改变任何东西,因此假定它已经完成。.PHONY do-echo在您的文件中添加将告诉make您忽略它并继续运行代码。
M3D

您可以放置$(info ...)在make rule之外。它将仍然生成输出。
丹尼尔·史蒂文斯

文档:进行控制功能
Daniel Stevens

3

是。您可以使用以下命令换行\

VARIABLE="\
THIS IS A VERY LONG\
TEXT STRING IN A MAKE VARIABLE"

更新

啊,你要换行吗?那不,我不认为Vanilla Make有任何办法。但是,您始终可以在命令部分中使用此处文档

[这不起作用,请参阅MadScientist的评论]

foo:
    echo <<EOF
    Here is a multiple line text
    with embedded newlines.
    EOF

的确如此,但是它没有给我任何格式(换行符)。它只是变成一行文本
jonner

多行此处文档无法按GNU Make中的描述运行。
Matt B.

3
食谱中的多行文档在支持POSIX标准的任何标准版本的make中均不起作用:make标准要求食谱的每一行都必须在单独的shell中运行。Make不会对命令进行任何分析以判断它是否为此处文档,并以不同的方式处理它。如果您知道可以支持此功能的make的某些变体(我从未听说过),则可能应该明确声明。
MadScientist 2012年

2

只是Eric Melski回答的附言:您可以在文本中包括命令的输出,但是必须使用Makefile语法“ $(shell foo)”而不是shell语法“ $(foo)”。例如:

define ANNOUNCE_BODY  
As of $(shell date), version $(VERSION) of $(PACKAGE_NAME) has been released.  

It can be downloaded from $(DOWNLOAD_URL).  

endef

2

这没有给出here文档,但是它确实以适合管道的方式显示多行消息。

=====

MSG = this is a\\n\
multi-line\\n\
message

method1:
     @$(SHELL) -c "echo '$(MSG)'" | sed -e 's/^ //'

=====

您还可以使用Gnu的可调用宏:

=====

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 "---"

=====

这是输出:

=====

$ make method1 method2
Method 1:
this is a
multi-line
message
---
Method 2:
this is a
multi-line
message
---
$

=====


1

为什么不使用字符串中的\ n字符来定义行尾?还要添加多余的反斜杠以将其添加到多行中。

ANNOUNCE_BODY=" \n\
Version $(VERSION) of $(PACKAGE_NAME) has been released \n\
\n\
It can be downloaded from $(DOWNLOAD_URL) \n\
\n\
etc, etc"

我更喜欢埃里克·梅尔斯基(Erik Melski)的答案,但这可能已经为您解决了问题,具体取决于您的应用程序。
Roalt

我对此有疑问。这基本上可以正常工作,除非我在每行的开头(第一行除外)看到一个额外的“空格”。这会发生在你身上吗?我可以将所有文本放在一行中,以\ n分隔,这样可以有效地创建我喜欢的输出。问题在于Makefile本身看起来很难看!
Shahbaz 2012年

我找到了解决方法。$(subst \n ,\n,$(TEXT))尽管我希望有更好的方法,但我还是把文字通了!
Shahbaz


1

您应该使用“ define / endef” Make结构:

define ANNOUNCE_BODY
Version $(VERSION) of $(PACKAGE_NAME) has been released.

It can be downloaded from $(DOWNLOAD_URL).

etc, etc.
endef

然后,您应该将此变量的值传递给shell命令。但是,如果使用Make变量替换执行此操作,则会导致命令拆分为多个:

ANNOUNCE.txt:
  echo $(ANNOUNCE_BODY) > $@               # doesn't work

Qouting也无济于事。

传递值的最佳方法是通过环境变量传递值:

ANNOUNCE.txt: export ANNOUNCE_BODY:=$(ANNOUNCE_BODY)
ANNOUNCE.txt:
  echo "$${ANNOUNCE_BODY}" > $@

注意:

  1. 为该特定目标导出了变量,因此您可以重复使用该环境,不会造成太多污染;
  2. 使用环境变量(变量名前后用双qoutes和大括号括起来);
  3. 在变量周围使用引号。没有它们,换行符将丢失,所有文本将显示在一行上。

1

本着.ONESHELL的精神,在.ONESHELL面临挑战的环境中可能会变得非常接近:

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

使用示例如下所示:

define TEST
printf '>\n%s\n' "Hello
World\n/$$$$/"
endef

all:
        $(call oneshell,$(TEST))

显示输出(假设pid 27801):

>
Hello
World\n/27801/

这种方法确实允许一些额外的功能:

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

这些shell选项将:

  • 在执行时打印每个命令
  • 在第一个失败的命令上退出
  • 将未定义的shell变量的使用视为错误

其他有趣的可能性可能会提示自己。


1

我最喜欢alhadis的答案。但是要保持列格式,请再添加一件事。

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

输出:

:: 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

程序的提要应该很容易找到。我建议在自述文件和/或手册页中添加此级别的信息。当用户运行时make,他们通常会期望开始构建过程。

1
我想很多次只看make目标列表。您的评论没有任何意义。如果用户需要3秒钟来知道该做什么,那么用户的期望就无关紧要,而代替诸如此类的任何信息,有时可能要花费数小时。
Xennex81 2015年

1
用期望作为做某事的理由也是一个循环的争论:因为人们期望它,所以我们必须去做,因为我们期望它,所以他们就必须去做。
Xennex81

1

与OP并不完全相关,但是希望这会在将来对某人有所帮助。(因为这个问题是Google搜索中最多的问题)。

在我的Makefile中,我想将文件的内容传递给docker build命令,在大为震惊之后,我决定:

 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)

请参见下面的示例。

nb:在我的特殊情况下,我想使用在https://vsupalov.com/build-docker-image-clone-private-repo-ssh-key/中的示例在映像构建期间传递ssh密钥(使用一个多阶段docker构建以克隆git repo,然后在构建的第二阶段从最终映像中删除ssh密钥)

生成文件

...
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 .
...

Docker文件

...
ARG MY_VAR_ENCODED

RUN mkdir /root/.ssh/  && \
    echo "${MY_VAR_ENCODED}" | base64 -d >  /path/to/my/file/in/container
... 

1

在GNU Make 3.82及更高版本中,.ONESHELL当涉及多行外壳片段时,该选项是您的朋友。将其他答案的提示放在一起,我得到:

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))"

将上面的示例复制并粘贴到编辑器中时,请确保<tab>保留所有字符,否则version目标将被破坏!

请注意,这.ONESHELL将导致Makefile中的所有目标对其所有命令使用单个shell。


不幸的是,这行不通:make version printf "Version 1.2.3 of foo-bar has been released. /bin/sh: 1: Syntax error: Unterminated quoted string make: *** [version] Error 2(GNU make
3,81

@blueyed,我刚刚使用GNU Make 3.82和GNU bash 4.2.45(1)-release对其进行了测试:它按预期工作。另外,请检查@printf ...语句前面是否有领先的TAB字符(而不是空格)-看起来TAB始终显示为4个空格...
sphakka,2014年

似乎.ONESHELL是3.82版中的新功能。
2014年

顺便说一句:使用空格而不是制表符时的错误将是*** missing separator. Stop.
2014年

0

并不是真正有用的答案,而只是表明'define'不能像Axe那样起作用(不适合在注释中):

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”,因此它尝试将ANNOUNCE BODY的第二行解释为命令。


0

它为我工作:

ANNOUNCE_BODY="first line\\nsecond line"

all:
    @echo -e $(ANNOUNCE_BODY)

0

GNU Makefile可以执行以下操作。这很丑陋,我不会说您应该这样做,但在某些情况下我会这样做。

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 如果不存在,则创建一个.profile文件。

在应用程序仅在POSIX shell环境中使用GNU Makefile的情况下使用此解决方案。该项目不是一个开源项目,平台兼容性是一个问题。

目的是创建一个Makefile,该文件可促进特定类型工作区的设置和使用。Makefile附带了各种简单的资源,而无需诸如其他特殊档案之类的东西。从某种意义上说,它是一个shell档案。然后,一个过程可以说诸如将Makefile放到文件夹中进行操作。make workspace,然后做make blah,等等。

可能会棘手的是弄清楚要引用的内容。上面的工作可以完成,并且与在Makefile中指定一个here文档的想法很接近。对于一般用途而言,这是否是一个好主意还完全是另外一个问题。


0

我相信跨平台使用的最安全答案是每行使用一个回显:

  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" >> $@

这样可以避免对echo版本进行任何假设。


0

使用字符串替换

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

然后在你的食谱,放

    @echo $(subst | ,$$'\n',$(ANNOUNCE_BODY))

之所以可行,是因为Make会替换所有出现的(请注意空格)并将其替换为换行符($$'\n')。您可以将等效的shell脚本调用看作是这样的:

之前:

$ 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"

后:

$ 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"

我不确定 $'\n'在非POSIX系统上是否可用,但是如果您可以访问单个换行符(即使通过从外部文件读取字符串),则基本原理相同。

如果您有许多这样的消息,则可以使用来减少噪音:

print = $(subst | ,$$'\n',$(1))

像这样调用它的位置:

@$(call print,$(ANNOUNCE_BODY))

希望这对某人有帮助。=)


我最喜欢这个。但是要保持列格式,请再添加一件事。`概要:= ::概要:Makefile \ | :: \ | ::用法:\ | :: make ..........:生成此消息\ | ::简介。:生成此消息\ | ::清洁....:消除不需要的中间体和目标\ | ::使所有......::从头开始编译整个系统\ endef
jlettvin

注释不允许使用代码。将作为答复发送。我最喜欢这个。但是要保持列格式,请再添加一件事。概要:= ::概要:Makefile` ::``| ::用法:``| :: make ..........:生成此消息。::简介。:生成此消息` ::清洁....:消除不需要的中间体和目标。::使所有......::从完整的`endef`编译整个系统
jlettvin

@jlettvin查看我对您答案的答复。绝对不应将程序的概要嵌入到Makefile中,尤其是不要将其作为默认任务。

0

或者,您可以使用printf命令。这在OSX或功能较少的其他平台上很有用。

要简单地输出多行消息:

all:
        @printf '%s\n' \
            'Version $(VERSION) has been released' \
            '' \
            'You can download from URL $(URL)'

如果您尝试将字符串作为arg传递给另一个程序,则可以这样进行:

all:
        /some/command "`printf '%s\n' 'Version $(VERSION) has been released' '' 'You can download from URL $(URL)'`"
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.