Assign variable once when first used in Makefile [duplicate] - makefile

This question already has answers here:
How to get "at most once" semantics in variable assignments?
(2 answers)
Closed 4 years ago.
How do I only invoke a make shell function once in a variable expansion, and only when the variable is first used? I don't want to use := to expand at declaration time (ie: simple expansion) because the expansion is expensive and only some of my targets need the variable.
I tried using conditional variable assignment but it invokes the shell every time, for example, the following invokes shell ls twice:
.PHONY: test
FILES ?= $(warning Invoking the shell)$(shell ls)
test:
echo $(FILES) one
echo $(FILES) two

Move the assignment inside the recipe
test:
FILES=$$(ls) ;\
echo $$FILES one ;\
echo $$FILES two

Related

How to execute a command that is the arguments to the script? [duplicate]

This question already has answers here:
What is the difference between $* and $#
(4 answers)
How do I pass on script arguments that contain quotes/spaces?
(2 answers)
Closed last month.
I want a bash script, call it args, to execute a command that is the arguments to the script.
In particular, I would like this command (note multiple blanks):
$./args echo 'foobar *0x0'
to execute this precise command:
echo 'foobar *0x0'
I tried this in args:
#!/bin/bash
set -x
$*
but it doesn't work:
./args echo 'foobar *0x0'
+ echo foobar '*0x0'
foobar *0x0
Witness the single space, as well as moved single quotes.
With $#, the result is exactly the same, so please don't close the question on the account of differences between $* and $#. Also, blanks are not my only problem, there is the *0x0.
#!/bin/bash
"$#"
This expands to all of the command-line arguments with spacing and quoting intact. $*, by contrast, is subject to unwanted word splitting and globbing since it's not quoted.

Why does a glob expression passed in an argument net work when referenced as $1? [duplicate]

This question already has answers here:
Using a glob expression passed as a bash script argument
(1 answer)
Bash glob parameter only shows first file instead of all files
(4 answers)
Closed 2 years ago.
I have a bash script called TestList and I want it to take the argument Small/*.c
Small is a directory and it contains files:
bar.c bar.h foo.c foo.h main.c
The goal is to get Small and *.c as seperate strings in my bash script but whenever I try to do this *.c always becomes bar.c
for example:
echo `ls Small/*.c``
outputs: Small/bar.c Small/foo.c Small/main.c
echo `ls $1``
outputs: Small/bar.c
Mark your argument with quotes, so your call would be something like ./TestList "Small/*.c". This will yield the same output.
The reason why your command wasn't working as expected before is because * is a special character that needs to be escaped. Therefore, you can also supply Small/\*.c as an argument, without the quotes.

quotation marks in bash script around $#? [duplicate]

This question already has answers here:
Parsing/passing command line arguments to a bash script - what is the difference between "$#" and "$*"?
(3 answers)
When to wrap quotes around a shell variable?
(5 answers)
How to preserve double quotes in $# in a shell script?
(6 answers)
Closed 5 years ago.
I found the following snippet of bash script from How can I run a function from a script in command line?
$ cat test.sh
testA() {
echo "TEST A $1";
}
testB() {
echo "TEST B $2";
}
"$#"
This works well.
One of the response of this answer is Use "$#" in most cases. $# is not safe in some cases
I'm wondering why $# needs quotation marks, "$#" , in the last line.
What makes it difference with or without quotation marks around $# in the bash script?
"$#" quotes each positional paremeter that is expanded. For example:
# script.sh
cat "$#"
cat $#
If you do script "a b" the first cat will try to emit a file literally named a b. Since the positional parameters aren't quoted for the second cat, it will try to emit individual files named a and b. This can cause all kinds of problems since the user quoted the value passed to script and expected it to be handled as one word/unit.
Note that "$#" is a bit special since if you do script "a b" "c d" this expands to: cat "a b" "c d" in the script rather than cat "a b c d". The second line would do cat a b c d which tries to concatenate four different files rather than two files with spaces in the name.

Testing the eval function of GNU make

The GNU make manual says that the eval function expands the arguments and then feeds the results of the expansion to make parser. The following is quoted from GNU make manual.
The argument to the eval function is expanded, then the results of that expansion are parsed as makefile syntax.
I don't quite understand how the make parser process the text fed by eval, so
I write following makefile to test.
define myprint
echo "this is a line"
endef
goal:
$(eval $(call myprint))
gcc -o goal test.c
I know that the correct invocation of myprint should be only use the call function: $(call myprint) and delete the 'Tab' character before echo. I write the makefile in this form just to test the eval function.
My expectation: first the eval function expands myprint, which is an echo command preceded by a 'Tab', and the 'Tab' is used to make the expanded text to be a legal recipe. Then the eval feeds the expanded text to maker parser, who will identify the text to be a recipe, and run it. As the command is legal, the makefile should run properly.
However, I meet such an error:
Makefile:6: *** recipe commences before first target. Stop.
Could somebody explain why make produce such an error?
the results of that expansion are parsed as makefile syntax
Your use of eval is different: you would like it to be parsed as shell syntax. You can write:
define myprint
echo "this is a line"
endef
goal:
$(myprint)
gcc -o goal test.c
or:
define myprint
echo "this is a $(1)"
endef
goal:
$(call myprint,line)
gcc -o goal test.c
Because after make expansion the recipes are valid shell syntax. But not what you wrote because the expansion of eval is still interpreted as make syntax, not shell. To illustrate a typical use of eval and call, consider this:
define myprint
echo "this is a $(1)"
endef
define mygoal
$(1):
$$(call myprint,line)
gcc -o $(1) $(2).c
endef
$(eval $(call mygoal,goal,test))
It is a bit more tricky than two first examples (without eval) but it illustrates the real purpose of eval: programmatically instantiate make constructs. Here is how it works, step by step:
During the first phase of its 2-phases algorithm, make expands the $(eval... function call, that is:
Expand the parameter of the $(eval... function (the $(call... function):
Expand the parameters of the $(call... function (goal and test). No effect in our case.
Assign the result to the temporary variables $(1) and $(2).
Expand the mygoal variable in this context, which replaces $(1), $(2) and $$(call... by goal, test and $(call..., respectively.
Instantiates (in memory) the result as a make construct, a complete rule in this case:
goal:
$(call myprint,line)
gcc -o goal test.c
The first phase continues but it has no effect on this instantiated rule because the recipes are expanded by make during the second phase.
During the second phase, when the time comes to build the goal target, make expands the recipe before executing it, that is:
Expand the $(call myprint... parameter (line, no effect).
Assign the result to temporary parameter $(1).
Expand variable myprint in this context, which produces:
echo "this is a line"
All this is thus the same as if we had written the rule:
goal:
echo "this is a line"
gcc -o goal test.c
Note the double $$ in the initial definition of mygoal:
It’s important to realize that the eval argument is expanded twice;
first by the eval function, then the results of that expansion are
expanded again when they are parsed as makefile syntax. This means you
may need to provide extra levels of escaping for “$” characters when
using eval.
$(eval …) needs a syntactically complete makefile fragment. It cannot be used to paste tokens into other makefile constructs. Perhaps the manual does not explain this clearly enough, but it's implemented by reading its argument as if it were an included makefile.
#RenaudPacalet, I write following makefile to test whether the expansion of call 'eats' one dollar.
define myprint
echo "In my print $$(ls)"
endef
goal:
$(call myprint)
$(info $(call myprint))
gcc -o goal test.c
Its output is:
echo "In my print $(ls)"
echo "In my print $(ls)"
In my print call.mk ... (files list)
As $(call myprint) outputs "In my print $(ls)" correctly, it must be expanded to echo "In my print $$(ls)" first, then it will be expanded to the correct shell command echo "In my print $(ls)". So I think the call function does not 'eats' one dollar.
Another evidence is the output of info function. The GNU make manual says:
$(info text…)
This function does nothing more than print its (expanded) argument(s) to standard output.
From the manual we can infer that make will expand the arguments of the info function. As the output of the info is echo "In my print $(ls)", so the arguments before expansion should be echo "In my print $$(ls)". So we can conclude that the call function doesn't 'eat' one dollar.

Error "command not found" when setting value to variable [duplicate]

This question already has answers here:
Indirect variable assignment in bash
(7 answers)
What is indirect expansion? What does ${!var*} mean?
(6 answers)
Closed 6 years ago.
I have the following test.sh script:
#!/bin/bash
foo=0
bar=foo;
${bar}=1
echo $foo;
Output:
./test.sh: line 4: foo=1: command not found
0
Why the "command not found" error? How to change script to "echo $foo" outputs 1?
That's not the way to do indirection unfortunately. To do what you want you could use printf like so
printf -v "$bar" "1"
which will store the value printed (here 1 in the variable name given as an argument to -v which when $bar expands here will be foo
Also, you could use declare like
declare "$bar"=1
which will do variable substitution before executing the declare command.
In your attempt the order of bash processing is biting you. Before variable expansion is done the line is split into commands. A command can include variable assignments, however, at that point you do not have a variable assignment of the form name=value so that part of the command is not treated as an assignment. After that, variable expansion is done and it becomes foo=1 but by then we're done deciding if it's an assignment or not, so just because it now looks like one doesn't mean it gets treated as such.
Since it was not processed as a variable assignment, it must not be treated as a command. You don't have a command named foo=1 in your path, so you get the error of command not found.
You need to use the eval function, like
#!/bin/bash
foo=0
bar=foo;
eval "${bar}=1"
echo $foo;
The ${bar}=1 will first go through the substitution process so it becomes foo=1, and then the eval will evaluate that in context of your shell

Resources