DevC
DevC

Reputation: 7423

how to use target specific variable in gnu make

I have a makefile like this:

file1 = "path/to/some/file"
header="col1;col2;col3"

$(file1):
      some steps to create the file

call_perl_script:$(file1)
      ${perl} script.pl in=header

The header is currently hardcoded, and also contained in the generated file1. I need to fetch the header from file1. Somehow I have changed it like

file1 = "path/to/some/file"

$(file1):
      some steps to create the file
      $(eval header="$(shell $(sed) -n "/^col1;col2;col3/p" $(file1))")

call_perl_script: $(file1)
      ${perl} script.pl in=$(header)

It works fine but want to know if it is correct way to work with target specific variable. The header does not get its value passed until used with eval.

Also if I print $(header) in the call_perl_script target, it prints correctly but if I use an if condition to check if the variable is empty and set a default value, then it does not work. It sets the value of header in the if block irrespective of the value from the sed output.

call_perl_script: $(file1)
      ${echo} $(header)
ifeq "$(header)" ""
      $(eval header="col1;col2;col3")
endif
      ${perl} script.pl in=$(header)

Upvotes: 1

Views: 244

Answers (2)

Kevin E
Kevin E

Reputation: 3326

It's unclear from the original question, but I presume that file1 is part of the useful output of the Makefile, and needs to be kept around after the Perl script runs.

Regardless, it seems fine to use a command substitution here, which saves you from having to create extra targets or temp files, and clean those up afterwards.

file1 = path/to/some/file

$(file1):
      some steps to create the file

call_perl_script: $(file1)
      perl script.pl in="`sed -n '/^col1;col2;col3/p' $<`"

The double quotes around the backticks are absolutely required here to prevent the shell from treating the ;s as command separators, which would otherwise result in an error. You could use GNU Make's $(shell …) function here instead of backticks, but it doesn't save any typing, and isn't portable to non-GNU systems, if that's a concern.

Assuming the header is the first line of the file, you can simplify that last target further, to just:

call_perl_script: $(file1)
      perl script.pl in="`head -n 1 $<`"

The head command is specified by POSIX, so it's basically guaranteed to be available. You could also use sed -n 1p.

If you're unfamiliar with command substitutions (the commands in `backticks` here), they run a command and return its output, with trailing newlines removed. In Bash and other modern shells, you usually see this as $(command with args). However, by default, Makefile recipes run with /bin/sh, the POSIX/Bourne shellnot your user's login shell—unless otherwise specified.

The $< above refers to the rule's first dependency, $(file1); see "Automatic Variables" in the manual, if this was unfamiliar to you.

Confusion about passing variables between Makefile targets

I think the OP's question originates from the somewhat-confusing concept of variables' "scope" in Makefiles. There are really two separate ones:

  1. a "global" scope for make variables, defined outside any recipes
  2. —and— a scope for shell variables
    • because each Make recipe line runs in its own subshell, each recipe line is a separate scope for shell variables*

You can send Make variables in to the scope of a recipe with

  • direct substitution, using Make's $(VARNAME) or ${VARNAME} variable syntax
  • —or— export a Make variable to the recipe's environment with export VARNAME outside the recipe, then $$VARNAME using shell variable syntax inside the recipe

…but you can't go the other way. This means it's just not possible to pass values between Makefile targets with variables alone.

If you're thinking of reaching for $(eval …), now you have two problems. There is almost definitely a simpler solution for what you're trying to do.

Aside about quoting

For clarity, I recommend omitting quotes from Makefile variable definitions for simple string values like header in the OP's first example. Make itself doesn't care about those quotes, but the shell does.

Instead, wrap the Make variables in quotes when referenced within your recipes, where the shell would require them. For example:

all: works-but-not-optimal quotes-cause-problems better also-okay

works-but-not-optimal: GREET = "\nHi! How are you?\n"
works-but-not-optimal:
    @printf $(GREET)

quotes-cause-problems: GREET = "Hallo! Wie geht es Ihnen?"
quotes-cause-problems:
    @printf "\n$(GREET)\n"

better: MSG = ¡Hola, buenos días!
better:
    @printf "\n$(MSG)\n"

also-okay: export MSG = Buon giorno!
also-okay:
    @printf "\n$$MSG\n"

In Makefiles, leading whitespace for top-level variable definitions is trimmed, but all remaining internal or trailing whitespace is preserved intact. That does mean, however, that you need to watch out for trailing whitespace (:set list for the vi users) in variable definitions. It's never easy!

Make isn't great about handling filenames with spaces in them either, as targets or dependencies, so these are best avoided. You'll also have challenges if you need to represent quotes within quotes—good luck with those backslashes! Consider using typographic quotes instead, if they're just for display purposes.

By the way, all three examples above demonstrate target-specific variables as that phrase is used in the GNU Make manual.

Hope that helps (someone)!


* …unless you take steps to force multiple recipe lines to run in the same subshell, which is out of scope for this already-sprawling answer. ;)

Upvotes: 0

andrewdotn
andrewdotn

Reputation: 34823

I don’t think target-specific variables will help you here, because they’re usually static things. For example, if you need to silence one type of warning for one specific C file, you can add a rule like foo.o: CFLAGS+=-Whatever.

The problem you’re running into is that $(eval header=...) is only executed when $(file1) is made. If it already exists, then the target won’t get rebuilt, and header won’t get set.

A more natural way of doing this in a Makefile would be to save the header to a separate file. That way, it will automatically get regenerated whenever $(file) changes:

.DELETE_ON_ERROR:

file = foo.txt

call_perl_script: $(file) $(file).header
        echo perl script.pl in="$(shell cat $(file).header)"

$(file):
        echo "col1;col2;col3;$$(head -c1 /dev/random)" > $(file)

%.header: %
        sed -n '/^col1;col2;col3/p' $< > $@

clean::
        rm -f $(file)
        rm -f *.header

which results in:

echo "col1;col2;col3;$(head -c1 /dev/random)" > foo.txt
sed -n '/^col1;col2;col3/p' foo.txt > foo.txt.header
perl script.pl in="col1;col2;col3;?"

However this is still a bit of a kludge, so for long-term maintainability, you may want to consider updating script.pl to parse out the header itself.

Upvotes: 2

Related Questions