在 makefile 中多行 bash 命令

179

考虑到每个命令都在自己的 shell 中运行,那么在 makefile 中运行多行 bash 命令的最佳方法是什么?例如:

for i in `find`
do
    all="$all $i"
done
gcc $all
6个回答

210

您可以使用反斜杠进行行续。但请注意,shell会将整个命令连接成单个行,因此您还需要在某些行末尾添加分号以终止它们:

foo:
    for i in `find`;     \
    do                   \
        all="$$all $$i"; \
    done;                \
    gcc $$all

但是,如果你只想获取find调用返回的整个列表,并将其传递给gcc,实际上并不一定需要多行命令:

foo:
    gcc `find`

或者,使用更符合shell习惯的$(command)方法(注意需要转义$):

foo:
    gcc $$(find)

1
是的,简短回答如下:
  1. $:=$$
  2. 用\连接多行。顺便说一句,Bash 的人通常建议用$$(find)替换find调用。
- Yauhen Yakimovich

136
如问题所示,每个子命令都在自己的shell中运行。这使得编写复杂的shell脚本有点混乱,但是这是可能的!解决方案是将您的脚本合并为make将视为单个子命令(单行)的形式。
编写makefile中的shell脚本的提示:
  1. 通过将$替换为$$来避免脚本使用$
  2. 通过在命令之间插入;将脚本转换为单行工作
  3. 如果您想将脚本写在多行上,请使用\来转义行尾
  4. 可选择以set -e开头,以匹配make的中止子命令失败的设置。您还可以使用set -e -o pipefail确保管道命令中的错误导致脚本中止(注意:这是bashism,因此需要SHELL := /bin/bash或类似设置)
  5. 这是完全可选的,但您可以使用(){}括起脚本,以强调多行序列的连贯性-这不是一个典型的makefile命令序列

以下是受OP启发的示例:

mytarget:
    { \
    set -e ;\
    msg="header:" ;\
    for i in $$(seq 1 3) ; do msg="$$msg pre_$${i}_post" ; done ;\
    msg="$$msg :footer" ;\
    echo msg=$$msg ;\
    }

9
你可能还想在你的makefile中加入SHELL := /bin/bash,以启用BASH特定的功能,如进程替换 - Brent Bradburn
17
微妙之处:{ 后面的空格非常关键,可以防止将 {set 解释为未知命令。 - Brent Bradburn
9
微妙的要点 #2:在 makefile 中可能并不重要,但是在有时需要复制脚本并直接从 shell 提示符运行时,使用 {}() 进行包装的区别非常大。 在 {} 中声明变量,特别是使用 set 修改状态,可能会对您的 shell 实例造成破坏。 () 可以防止脚本修改您的环境,这很可能是更好的选择。 例如(这将结束您的 shell 会话):{ set -e ; } ; false - Brent Bradburn
1
您可以使用以下形式包含注释:command;## my comment \\(注释在;\\之间)。这种方式似乎很好用,但是 如果 您手动运行命令(通过复制和粘贴),命令历史记录将包含注释,导致无法重复使用该命令。[注意:由于在反引号内使用反斜杠,因此此注释的语法突出显示已损坏。] - Brent Bradburn
1
如果你想在makefile中打破bash/perl/script字符串,请关闭字符串引号,反斜杠,换行符(无缩进),打开字符串引号。例如:perl -e QUOTE print 1; QUOTE BACKSLASH NEWLINE QUOTE print 2 QUOTE - mosh
显示剩余5条评论

26

ONESHELL指令允许编写多行配方在同一Shell调用中执行。

all: foo

SOURCE_FILES = $(shell find . -name '*.c')

.ONESHELL:
foo: ${SOURCE_FILES}
    FILES=()
    for F in $^; do
        FILES+=($${F})
    done
    gcc "$${FILES[@]}" -o $@

不过,有一个缺点:特殊前缀字符(‘@’、‘-’和 ‘+’)的解释不同。

https://www.gnu.org/software/make/manual/html_node/One-Shell.html


完美的,为什么这不在顶部! - xeruf

3
当然,编写Makefile的正确方法是实际记录哪些目标依赖于哪些源文件。在简单情况下,所提出的解决方案将使foo依赖于自身,但当然,make足够聪明,可以消除循环依赖关系。但是,如果您向目录中添加临时文件,则它将“神奇地”成为依赖项链的一部分。最好通过脚本一次性创建一个显式的依赖项列表。
GNU make知道如何运行gcc以从一组.c.h文件生成可执行文件,因此您可能真正需要的只是:
foo: $(wildcard *.h) $(wildcard *.c)

1

仅仅调用命令有什么问题吗?

foo:
       echo line1
       echo line2
       ....

关于您的第二个问题,您需要使用$$来转义$,即bash -c '... echo $$a ...'
编辑:您的示例可以重写为单行脚本,如下所示:
gcc $(for i in `find`; do echo $i; done)

17
因为“每个命令都在自己的 shell 中运行”,所以我需要在命令2中使用命令1的结果。就像这个例子一样。 - Nelson Tatius
如果您需要在shell调用之间传递信息,您需要使用某种外部存储,例如临时文件。但是您始终可以重写代码以在同一shell中执行多个命令。 - JesperE
+1,特别是对于简化版本。这不就等同于只执行“gcc find”吗? - Eldar Abusalimov
1
是的,没错。但你当然可以做比“echo”更复杂的事情。 - JesperE

0
在查找如何从Makefile检测Docker是否正在运行时,发现了这篇文章...没有找到示例,所以我写了一个。下面是上下文中的GNUmakefile...

https://github.com/RandyMcMillan/make-docker-start

OS                                      :=$(shell uname -s)
export OS

.ONESHELL:
docker-start:##     detect whether docker is running...
    @( \
        while ! docker system info > /dev/null 2>&1; do\
        echo 'Waiting for docker to start...';\
        if [[ '$(OS)' == 'Linux' ]]; then\
         systemctl restart docker.service;\
        fi;\
        if [[ '$(OS)' == 'Darwin' ]]; then\
         open --background -a /./Applications/Docker.app/Contents/MacOS/Docker;\
        fi;\
    sleep 1;\
    done\
    )
docker-pull:docker-start##  pull alpine image
    docker pull alpine

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接