【问题标题】:How to define global shell functions in a Makefile?如何在 Makefile 中定义全局 shell 函数?
【发布时间】:2012-09-28 07:03:42
【问题描述】:

我要定义一个shell函数

#!/bin/sh

test ()
{
  do_some_complicated_tests $1 $2;
  if something; then
    build_thisway $1 $2;
  else
    build_otherway $1 $2;
  fi
}

以这样一种方式,我可以在我的 Makefile 的每个规则中使用它,例如:

foo: bar
  test foo baz

明确地说,我希望 shell 函数成为 Makefile 的一部分。最优雅的方法是什么?如果您可以在不递归调用 make 的情况下做到这一点,则可以加分。


背景: 我的实际问题是make -n 产生了一个非常长且不可读的输出。每个规则都使用几乎相同的不可读的 shell 命令序列,并且规则很多。上述解决方案将使make -n 的输出更有用。

【问题讨论】:

  • 我做了@JonathanLeffler 说做不到的事情?至少在一个月内,我会对自己感到非常满意。
  • 为什么不把代码放到一个shell脚本中然后调用呢?

标签: shell makefile gnu-make sh


【解决方案1】:

为什么不在 Makefile 中使用 .ONESHELL 并定义一个 Makefile 函数,如 bash 函数:

jeromesun@km:~/workshop/hello.test$ tree
.
├── Makefile
└── mkfiles
    └── func_test.mk

1 directory, 2 files
jeromesun@km:~/workshop/hello.test$ cat Makefile 
.ONESHELL:

-include mkfiles/*.mk

test:
        @$(call func_test, one, two)
jeromesun@km:~/workshop/hello.test$ cat mkfiles/func_test.mk 
.ONESHELL:

define func_test
    echo "func_test parameters: 0:$0, 1:$1, 2:$2"
    if [ ! -d abc ]; then
        mkdir abc
        echo "abc not found"
    else
       echo "abc found"
    fi
endef
jeromesun@km:~/workshop/hello.test$ 

结果:

jeromesun@km:~/workshop/hello.test$ make test 
func_test parameters: 0:func_test, 1: one, 2: two
abc not found
jeromesun@km:~/workshop/hello.test$ make test 
func_test parameters: 0:func_test, 1: one, 2: two
abc found

【讨论】:

    【解决方案2】:

    TL;DR:

    在您的生成文件中:

    SHELL=bash
    BASH_FUNC_command%%=() { ... }
    export BASH_FUNC_command%%
    

    长答案

    我已经在非 make 上下文中使用 bash 完成了类似的操作。

    我在 bash 中声明了我的 shell 函数,然后将它们导出为:

    export -f function-name
    

    如果从 make 调用相同的 shell 作为子进程,那么它们自然是可用的。

    例子:

    $ # define the function
    $ something() { echo do something here ; }
    $ export -f something
    
    $ # the Makefile
    $ cat > Makefile <<END
    SHELL=bash
    all: ; something
    END
    $
    

    试试看

    $ make
    something
    do something here
    $
    

    这在环境中看起来如何?

    $ env | grep something
    BASH_FUNC_something%%=() { echo do something here
    

    所以你的问题是如何从 Makefile 中设置环境变量。该模式似乎是 BASH_FUNC_function-name%%=() { function-body }

    直接在 make 中

    那么这对您有什么作用?试试这个makefile

    SHELL=bash
    define BASH_FUNC_something-else%%
    () {
      echo something else
    }
    endef
    export BASH_FUNC_something-else%%
    
    all: ; something-else
    

    试试看:

    $ make
    something-else
    something else
    

    在 Makefile 中有点难看,但对于 ma​​ke -n 来说并不难看

    【讨论】:

      【解决方案3】:

      此解决方案不依赖外部临时文件,也不会强迫您修改 SHELL 变量。

      TESTTOOL=sh -c '\
        do_some_complicated_tests $$1 $$2; \
        if something; then
          build_thisway $$1 $$2;
        else
          build_otherway $$1 $$2;
        fi' TESTTOOL
      
      ifneq (,$(findstring n,$(MAKEFLAGS)))
      TESTTOOL=: TESTTOOL
      endif
      
      foo: bar
          ${TESTTOOL} foo baz
      

      ifneq…endif 块检查命令行上的-n 标志,并将TESTTOOL 的扩展设置为: TESTTOOL,这易于阅读且执行安全。

      如果您愿意,最好的解决方案是将 shell 函数转换为实际程序。

      【讨论】:

      • 这个例子不起作用。第二个之后的引用行也应该在最后以斜线继续。我想粘贴一个工作示例,但编辑器使它变得困难:(
      • 出于某种原因,在我的系统上,它会将整个 TESTTOOL 内容视为单个参数,从而导致 /bin/bash: sh -c '...: No such file or directory。如何让shell拆分呢?
      • 实际上原因似乎是我的Makefile中的.SHELLFLAGS = -eux,但我不明白为什么它会中断
      【解决方案4】:

      这真的是贫民区,但无论如何。我使用 zsh,但我确信有 bash 和 sh 等价物。

      生成文件:

      export ZDOTDIR := ./
      
      SHELL := /usr/bin/env zsh
      
      default:
          f
      

      .zshenv,与 Makefile 相同的目录:

      f () { echo 'CHECK OUT THIS AWESOME FUNCTION!' }
      

      ZDOTDIR 变量使 zsh 在当前目录中查找点文件。然后你只需在.zshenv 中粘贴你想要的。

      $ make
      f
      CHECK OUT THIS AWESOME FUNCTION!
      

      【讨论】:

        【解决方案5】:

        我不认为这符合“优雅”的条件,但它似乎可以满足您的需求:

        ##
        ## --- Start of ugly hack
        ##
        
        THIS_FILE := $(lastword $(MAKEFILE_LIST))
        
        define shell-functions
        : BEGIN
          # Shell syntax here
          f()
          {
            echo "Here you define your shell function. This is f(): $@"
          }
        
          g()
          {
            echo "Another shell function. This is g(): $@"
          }
        : END
        endef
        
        # Generate the file with function declarations for the shell
        $(shell sed -n '/^: BEGIN/,/^: END/p' $(THIS_FILE) > .functions.sh)
        
        # The -i is necessary to use --init-file
        SHELL := /bin/bash --init-file .functions.sh -i
        
        ##
        ## -- End of ugly hack
        ##
        
        all:
            @f 1 2 3 4
            @g a b c d
        

        运行它会产生:

        $ make -f hack.mk
        Here you define your shell function. This if f(): 1 2 3 4
        Another shell function. This is g(): a b c d
        

        使用-n 运行它会产生:

        $ make -f hack.mk -n
        f 1 2 3 4
        g a b c d
        

        这依赖于 defineendef 之间定义的宏在实际使用之前根本不会被 make 解释这一事实,因此您可以直接使用 shell 语法而无需结束每个带反斜杠的行。当然,如果你调用shell-functions宏,它会爆炸。

        【讨论】:

        • 您提取 .sh 文件的方法非常有用,谢谢。这接近我想要的。如果我正在编写一个临时的 .sh 文件,那么我不在乎我使用 f 和 g 作为 shell 函数;我会改用 f.sh 和 g.sh 。那么它就没有那么丑了,因为不需要重新定义$(SHELL)
        • 使用SHELL 技巧的优点是一切都自动且透明地发生。您不必每次需要 .sh 文件时都将它们显式加载到 shell 中。
        • 我认为 make 会为每个命令重新启动 $(SHELL) ?因此,上面的make all 有效地运行 $(SHELL) 并将“f 1 2 3 4”通过管道传输到其标准输入中。在下一行,make 启动一个新的 $(SHELL) 实例并将 "g a b c d" 通过管道传输到它的标准输入中。真的吗?如果是这样,我看不到使用单独文件的优势(因为bash --init-file ... 无论如何都必须重新读取 .sh 文件)。 [要清楚,我建议的替代方案是使用@./f.sh 1 2 3 4 而不是@f 1 2 3 4。]
        • 当然,这完全取决于您,因为您提出了这个问题,但在我看来,这样做的好处是通过SHELL 更干净。它也更有效率。
        • 因为您可能希望在同一命令中多次使用f.sh。使用--init-file,shell 只加载函数一次。无论如何,差异充其量是微不足道的,除非f.sh 很大。我的主要观点是清洁和“优雅”,而不是速度。顺便说一句,对我来说,“丑陋”的部分是 .sh 文件的生成,而不是 SHELL := ...
        【解决方案6】:

        由于问题被标记为 gnu-make,您可能对 Beta 的解决方案感到满意,但我认为最好的解决方案是 1) 将函数重命名为 test 以外的任何名称(避免使用像 lscd 这样的名称以及);出于讨论的目的,假设您已将其重命名为 foo,2) 只需编写一个名为 foo 的脚本并从 Makefile 中调用它。如果你真的想在 Makefile 中定义一个 shell 函数,只需为每个规则定义它:

        F = foo() { \
          do_some_complicated_tests $$1 $$2; \
          if something; then \
            build_thisway $$1 $$2; \
          else \
            build_otherway $$1 $$2; \
          fi \
        }
        
        all: bar
          @$(F); foo baz bar
        

        并不是foo 的定义的每一行都必须有续行,$ 都被转义以便传递给 shell。

        【讨论】:

        • 谢谢!在每条规则中定义函数并不能解决make -n的输出过长的问题。在这种情况下,我更喜欢 Beta 的解决方案。但是,以下内容可能对我有用:我可以添加依赖项all: foo.sh 和规则foo.sh:,它只是将脚本输出到foo.sh。它很慢,我很乐意找到更优雅的东西,但我真的希望所有内容都在一个文件中。
        • 太棒了;这是我最喜欢的。我更喜欢的是下面的缩写:F_DEF = foo() { as above },然后是F = @$(F_DEF); foo,然后你可以在其他任何地方重复使用它作为$(F) arg1 ... argn
        【解决方案7】:

        有些事情告诉我你最好过滤make -n 的输出,但你问的是可能的:

        define test
          @echo do some tests with $(1) and $(2); \
          SOMETHING=$(1)_3 ; \
          if [ $$SOMETHING == foo_3 ]; then \
            echo build this way $(1) $(2); \
          else \
            echo build another way $(1) $(2) ; \
          fi
        endef
        
        someTarget:
            $(call test,foo,bar)
        
        someOtherTarget:
            $(call test,baz,quartz)
        

        【讨论】:

        • 谢谢!不幸的是,这个解决方案正是我试图避免的。
        • 它工作正常,但是make -n 的输出将变得不可读,因为对于调用test 的每个目标,shell 脚本都会重新出现。更糟糕的是,脚本将在一行中。我可以使用过滤器,但我也需要调试过滤器,这是我想避免的。
        猜你喜欢
        • 2011-05-26
        • 2018-05-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-07-03
        • 2013-05-31
        • 1970-01-01
        • 2015-05-07
        相关资源
        最近更新 更多