26

I want to define a shell function

#!/bin/sh

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

in such a way that I can use it in every rule of my Makefile, such as:

foo: bar
  test foo baz

To be clear, I want the shell function to be part of the Makefile. What is the most elegant way to do this? Bonus points if you can do it without calling make recursively.


Background: My actual problem is that make -n produces a very long and unreadable output. Each rule uses almost the same sequence of unreadable shell commands, and there are many rules. The above solution would make the output of make -n more useful.

Holger
  • 1,078
  • 2
  • 11
  • 12

7 Answers7

16

This solution does not rely on an external temporary file and does not force you to tinker with the SHELL variable.

TESTTOOL=sh -c '\
  do_some_complicated_tests $$1 $$2; \
  if something; then                 \
    build_thisway $$1 $$2;           \
  else                               \
    build_otherway $$1 $$2;          \
  fi' TESTTOOL

ifneq (,$(filter n,$(MAKEFLAGS)))
TESTTOOL=: TESTTOOL
endif

foo: bar
    ${TESTTOOL} foo baz

The ifneq…endif block checks for the -n flag on the command line and sets the expansion of TESTTOOL to : TESTTOOL which is easy to read and safe to execute.

The best solution could be to turn the shell function into an actual program if this is an option for you.

Michaël Le Barbier
  • 6,103
  • 5
  • 28
  • 57
  • 1
    This example doesn't work. The quoted lines after second should also be slash-continued at the end. I wanted to paste a working example but the editor makes it difficult :( – Jakub Bochenski Jan 23 '20 at 16:26
  • For some reason on my systems it would treat the whole TESTTOOL contents as a single argument resulting in `/bin/bash: sh -c '...: No such file or directory`. How to make shell split it? – Jakub Bochenski Jan 23 '20 at 16:45
  • actually the reason seems to be `.SHELLFLAGS = -eux` in my Makefile, but I don't understand why it would break – Jakub Bochenski Jan 23 '20 at 16:55
  • 1
    It should use `filter`, which matches whole words, instead of `findstring`, which will also match flags like `--no-print-directory` that contain `n`. As Jakub noted, I had to slash-continue TESTTOOL. – Mark Gates May 07 '22 at 22:40
  • @MarkGates I fixed these little flaws, thanks! – Michaël Le Barbier Jun 10 '22 at 19:50
8

Since the question is tagged gnu-make, you might be happy with Beta's solution, but I think the best solution is to 1) rename the function anything other than test (avoid names like ls and cd as well); for the purpose of discussion let's assume you've renamed it foo, 2) simply write a script named foo and invoke it from the Makefile. If you really want to define a shell function in the Makefile, just define it for each rule:

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

Not that you must have line continuations on each line of the definition of foo, and the $ are all escaped so that they are passed to the shell.

William Pursell
  • 204,365
  • 48
  • 270
  • 300
  • 1
    Thanks! Defining the function in each rule doesn't solve the problem that the output of `make -n` is too long. In this case, I'd prefer Beta's solution. However, the following could work for me: I could add the dependency `all: foo.sh` and a rule `foo.sh:` that just outputs the script into `foo.sh`. It's slow and I'd be happy to find something more elegant, but I really want everything to be in one file. – Holger Oct 08 '12 at 15:12
  • Great; this is my favourite. What I like even better is the following abbreviation: `F_DEF = foo() { as above }`, and then `F = @$(F_DEF); foo`, and then you can reuse this from anywhere else as `$(F) arg1 ... argn`. – Christoph Lange Aug 31 '14 at 09:57
8

I don't think this qualifies as "elegant", but it seems to do what you want:

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

Running this produces:

$ 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

Running it with -n produces:

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

This relies on the fact that macros defined between define and endef are not interpreted by make at all until they are actually used, so you can use shell syntax directly and you don't need to end each line with a backslash. Of course, it will bomb if you call the shell-functions macro.

Idelic
  • 14,976
  • 5
  • 35
  • 40
  • Your method of extracting the .sh file is very helpful, thanks. This is close to what I want. If I'm writing a temporary .sh file anyway, then I don't care that I'm using f and g as shell functions; I'd use f.sh and g.sh instead. Then it's not nearly as ugly because there is no need to redefine `$(SHELL)`. – Holger Oct 08 '12 at 19:03
  • The advantage of using the `SHELL` trick is that everything happens automatically and transparently. You don't have to load the `.sh` files into the shell explicitly every time you need them. – Idelic Oct 08 '12 at 19:11
  • I thought that make restarts $(SHELL) for every command? So effectively `make all` above, runs $(SHELL) and pipes "f 1 2 3 4" into its stdin. On the next line, make starts a new instance of $(SHELL) and pipes "g a b c d" into its stdin. Is that true? If so, I don't see the advantage over using separate files (since `bash --init-file ...` has to re-read the .sh-file anyway). [To be clear, my proposed alternative is to use `@./f.sh 1 2 3 4` instead of `@f 1 2 3 4`.] – Holger Oct 08 '12 at 20:35
  • This is all up to you, of course, since you asked the question, but the advantage in my opinion is that doing it through `SHELL` is much cleaner. It's also more efficient. – Idelic Oct 08 '12 at 21:05
  • Do you mean more efficient as in faster? Can you explain to me why the solution with `--init-file` is faster? I'd appreciate that. Thanks! – Holger Oct 08 '12 at 22:07
  • 2
    Because you may want to use `f.sh` more than once in the same command. Using `--init-file`, the shell only loads the function once. In any case, the difference is marginal at best, unless `f.sh` is huge. My main point is cleanliness and "elegance", not speed. By the way, for me the "ugly" part was the generation of the .sh file, not the `SHELL := ...`. – Idelic Oct 08 '12 at 22:46
7

Why not using .ONESHELL in Makefile and define a Makefile function like bash function:

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$ 

Result:

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
jeromesun14
  • 111
  • 1
  • 6
6

TL;DR:

In your makefile:

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

Long answer

I've done something similar with bash in a non-make context which works here.

I declared my shell functions in bash and then exported them with:

export -f function-name

They are then naturally available if the same shell is invoked as a sub-process, in your case, from make.

Example:

$ # define the function
$ something() { echo do something here ; }
$ export -f something

$ # the Makefile
$ cat > Makefile <<END
SHELL=bash
all: ; something
END
$

Try it out

$ make
something
do something here
$

How does this look in the environment?

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

So the question for you would be how to set the environment variables from within the Makefile. The pattern seems to be BASH_FUNC_function-name%%=() { function-body }

Directly within make

So how does this work for you? Try this makefile

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

all: ; something-else

and try it:

$ make
something-else
something else

It's a bit ugly in the Makefile but presents no ugliness for make -n

Sam Liddicott
  • 1,265
  • 12
  • 24
5

This is really ghetto, but whatever. I use zsh, but I'm sure there are bash and sh equivalents.

Makefile:

export ZDOTDIR := ./

SHELL := /usr/bin/env zsh

default:
    f

.zshenv, same directory as Makefile:

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

The ZDOTDIR variable makes zsh look in the current directory for dotfiles. Then you just stick what you want in .zshenv.

$ make
f
CHECK OUT THIS AWESOME FUNCTION!
Casey Rodarmor
  • 14,878
  • 5
  • 30
  • 33
4

Something tells me you'd be better off filtering the output of make -n, but what you ask is possible:

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)
Beta
  • 96,650
  • 16
  • 149
  • 150
  • 1
    Thanks! Unfortunately, this solution is exactly the one I was trying to avoid. – Holger Oct 08 '12 at 14:57
  • 1
    It works fine, but the output of `make -n` will become unreadable because the shell script will reappear for every target that `test` is called. Even worse, the script will be on one line. I could use filters, but then I'd need to debug the filters, too, which I want to avoid. – Holger Oct 08 '12 at 18:54