Support conditional modifiers on all assignment operators

Rework the single "?=" operator to instead allow a "?" modifier to be
prepended to ANY assignment operator.  If "?" is given then the
variable is assigned (using whatever operator comes next) if and only
if the variable is not already defined.  If it is defined then no
action is taken (the right-hand side is not expanded, etc.)

* NEWS: Announce this new feature.
* doc/make.texi: Modify the documentation around assignment operators.
* src/variable.h: Remove the f_conditional variable flavor.
(do_variable_definition): Add an argument specifying conditional.
* src/variable.c (parse_variable_definition): Use the existing flag
"conditional" to remember if we saw "?" rather than the flavor.
When we see "?" skip it and continue trying to parse an assignment.
(try_variable_definition): Pass condition to do_variable_definition().
(initialize_file_variables): Ditto.
(do_variable_definition): Check for conditional up-front: quit if set.
Remove handling of obsolete f_conditional flavor.
* src/read.c (eval_makefile): MAKEFILE_LIST is not conditional.
(do_define): Unset conditional for define with no operator.  Pass the
conditional flag to do_variable_definition().
(construct_include_path): .INCLUDE_DIRS is not conditional.
* src/load.c (load_file): .LOADED is not conditional.
* tests/scripts/variables/conditional: Add new tests.
This commit is contained in:
Paul Smith 2024-01-11 10:05:27 -05:00
parent 82708b3a3a
commit 1eff20f6f6
7 changed files with 316 additions and 94 deletions

8
NEWS
View File

@ -41,6 +41,14 @@ https://sv.gnu.org/bugs/index.php?group=make&report_id=111&fix_release_id=111&se
The check in GNU Make 4.3 for suffix rules with prerequisites didn't check
single-suffix rules, only double-suffix rules. Add the missing check.
* New feature: Any assignment operator can be made conditional
GNU Make has long supported the conditional operator "?=" which creates a
recursive variable set to a value if and only if the variable is not already
defined. In this release, the "?" can precede any assignment operator to
make it conditional. For example, "?:=" creates a simply-expanded variable
and expands the right-hand side if and only if the variable is not already
defined. The constructs "?::=", "?:::=", and "?!=" also behave as expected.
* New feature: Unload function for loaded objects
When a loaded object needs to be unloaded by GNU Make, it will invoke an
unload function (if one is defined) beforehand that allows the object to

View File

@ -1572,18 +1572,18 @@ reference this section as you become familiar with them, in later
chapters.
@subheading Variable Assignment
@cindex +=, expansion
@cindex =, expansion
@cindex ?=, expansion
@cindex +=, expansion
@cindex :=, expansion
@cindex ::=, expansion
@cindex :::=, expansion
@cindex !=, expansion
@cindex +=, expansion
@cindex define, expansion
Variable definitions are parsed as follows:
@example
@var{immediate} = @var{deferred}
@var{immediate} ?= @var{deferred}
@var{immediate} := @var{immediate}
@var{immediate} ::= @var{immediate}
@var{immediate} :::= @var{immediate-with-escape}
@ -1598,10 +1598,6 @@ define @var{immediate} =
@var{deferred}
endef
define @var{immediate} ?=
@var{deferred}
endef
define @var{immediate} :=
@var{immediate}
endef
@ -1638,6 +1634,21 @@ in the variable named on the left, and that variable is considered a
recursively expanded variable (and will thus be re-evaluated on each
reference).
@subsubheading Conditional Assignment Modifier
@cindex ?=, expansion
@cindex ?:=, expansion
@cindex ?::=, expansion
@cindex ?:::=, expansion
@cindex ?!=, expansion
Adding a conditional modifier (@pxref{Conditional Assignment, ,Conditional
Variable Assignment}) to an assignment operator does not change the expansion
style for that operator: if the variable is not defined then the assignment
will be made using expansion as defined above.
If the variable is already defined, then the right-hand side of the assignment
is ignored and not expanded.
@subheading Conditional Directives
@cindex ifdef, expansion
@cindex ifeq, expansion
@ -5822,9 +5833,19 @@ POSIX specification in Issue 8 to provide portability.
@cindex conditional variable assignment
@cindex variables, conditional assignment
@cindex ?=
There is another assignment operator for variables, @samp{?=}. This
is called a conditional variable assignment operator, because it only
has an effect if the variable is not yet defined. This statement:
@cindex ?:=
@cindex ?::=
@cindex ?:::=
@cindex ?!=
Any assignment operator can be prefixed with a conditional operator, @samp{?}.
If this modifier is provided then the assignment will proceed normally, but
@emph{only if} the variable is not already defined.
If the variable is already defined, the assignment is ignored and the
right-hand side (value) of the assignment is not processed.
For example this statement:
@example
FOO ?= bar
@ -5840,8 +5861,18 @@ ifeq ($(origin FOO), undefined)
endif
@end example
Note that a variable set to an empty value is still defined, so
@samp{?=} will not set that variable.
More generally a statement of the form:
@example
NAME ?<op> VALUE
@end example
will do nothing if the variable @samp{NAME} is defined, and will perform the
operation @code{NAME <op> VALUE} for any assignment operation @samp{<op>} if
@samp{NAME} is not defined.
Note that a variable set to an empty value is still defined, so assignments
modified with @samp{?} will not set that variable.
@node Advanced
@section Advanced Features for Reference to Variables
@ -6142,7 +6173,6 @@ Several variables have constant initial values.
@cindex :=
@cindex ::=
@cindex :::=
@cindex ?=
@cindex !=
To set a variable from the makefile, write a line starting with the variable
@ -6174,31 +6204,12 @@ amount of memory on the computer. You can split the value of a
variable into multiple physical lines for readability
(@pxref{Splitting Lines, ,Splitting Long Lines}).
Most variable names are considered to have the empty string as a value if
you have never set them. Several variables have built-in initial values
that are not empty, but you can set them in the usual ways
(@pxref{Implicit Variables, ,Variables Used by Implicit Rules}).
Several special variables are set
automatically to a new value for each rule; these are called the
@dfn{automatic} variables (@pxref{Automatic Variables}).
If you'd like a variable to be set to a value only if it's not already
set, then you can use the shorthand operator @samp{?=} instead of
@samp{=}. These two settings of the variable @samp{FOO} are identical
(@pxref{Origin Function, ,The @code{origin} Function}):
@example
FOO ?= bar
@end example
@noindent
and
@example
ifeq ($(origin FOO), undefined)
FOO = bar
endif
@end example
Most variable names are considered to have the empty string as a value if you
have never set them. Several variables have built-in initial values that are
not empty, but you can set them in the usual ways (@pxref{Implicit Variables,
,Variables Used by Implicit Rules}). Several special variables are set
automatically to a new value while running the recipe for a rule; these are
called the @dfn{automatic} variables (@pxref{Automatic Variables}).
The shell assignment operator @samp{!=} can be used to execute a
shell script and set a variable to its output. This operator first
@ -6229,6 +6240,55 @@ var := $(shell find . -name "*.c")
As with the @code{shell} function, the exit status of the just-invoked
shell script is stored in the @code{.SHELLSTATUS} variable.
@node Conditionalizing
@section Conditionally Assigning to Variables
@cindex ?=
@cindex ?:=
@cindex ?::=
@cindex ?:::=
@cindex ?!=
Sometimes you want to set a variable but only if it's not already defined.
One way to do this is to test using the @code{origin} function (@pxref{Origin
Function}), like this:
@example
ifeq ($(origin FOO), undefined)
FOO = value
endif
@end example
However this is a lot to type, and read, and so GNU Make provides a way to
conditionally assign variables, only if they are not already defined.
To do this, prepend the conditional modifier @samp{?} to the assignment
operator. In this form @code{make} will first check to see if the variable is
defined; only if it is not will it proceed to execute the assignment (using
whichever operator you specified).
Instead of the above example, you get identical behavior by writing:
@example
FOO ?= value
@end example
And of course, you can also use:
@example
FOO ?:= value
@end example
@noindent
rather than writing:
@example
ifeq ($(origin FOO), undefined)
FOO := value
endif
@end example
The other assignment operators @samp{::=}, @samp{:::=}, and @samp{!=} can also
be used.
@node Appending
@section Appending More Text to Variables
@ -6625,13 +6685,13 @@ Multiple @var{target} values create a target-specific variable value for
each member of the target list individually.
The @var{variable-assignment} can be any valid form of assignment; recursive
(@samp{=}), simple (@samp{:=} or @samp{::=}), immediate (@samp{::=}),
appending (@samp{+=}), or conditional (@samp{?=}). All variables that appear
within the @var{variable-assignment} are evaluated within the context of the
target: thus, any previously-defined target-specific variable values will be
in effect. Note that this variable is actually distinct from any ``global''
value: the two variables do not have to have the same flavor (recursive vs.@:
simple).
(@samp{=}), simple (@samp{:=} or @samp{::=}), immediate (@samp{::=}), shell
(@samp{!=}), or appending (@samp{+=}), and may be preceded by a conditional
modifier (@samp{?}). The variable that appears within the
@var{variable-assignment} is evaluated within the context of the target: thus,
any previously-defined target-specific variable values will be in effect.
Note that this variable is actually distinct from any ``global'' value: the
two variables do not have to have the same flavor (recursive vs.@: simple).
Target-specific variables have the same priority as any other makefile
variable. Variables provided on the command line (and in the
@ -13069,8 +13129,13 @@ Here is a summary of the directives GNU @code{make} recognizes:
@itemx define @var{variable} :=
@itemx define @var{variable} ::=
@itemx define @var{variable} :::=
@itemx define @var{variable} +=
@itemx define @var{variable} !=
@itemx define @var{variable} ?=
@itemx define @var{variable} ?:=
@itemx define @var{variable} ?::=
@itemx define @var{variable} ?:::=
@itemx define @var{variable} ?!=
@itemx define @var{variable} +=
@itemx endef
Define multi-line variables.@*
@xref{Multi-Line}.

View File

@ -230,7 +230,7 @@ load_file (const floc *flocp, struct file *file, int noerror)
/* If the load didn't fail, add the file to the .LOADED variable. */
if (r)
do_variable_definition(flocp, ".LOADED", ldname, o_file, f_append_value, 0);
do_variable_definition(flocp, ".LOADED", ldname, o_file, f_append_value, 0, 0);
return r;
}

View File

@ -413,7 +413,7 @@ eval_makefile (const char *filename, unsigned short flags)
/* Add this makefile to the list. */
do_variable_definition (&ebuf.floc, "MAKEFILE_LIST", filename, o_file,
f_append_value, 0);
f_append_value, 0, 0);
/* Evaluate the makefile */
@ -733,11 +733,11 @@ eval (struct ebuffer *ebuf, int set_default)
record_waiting_files ();
if (vmod.undefine_v)
{
do_undefine (p, origin, ebuf);
continue;
}
else if (vmod.define_v)
{
do_undefine (p, origin, ebuf);
continue;
}
if (vmod.define_v)
v = do_define (p, origin, ebuf);
else
v = try_variable_definition (fstart, p, origin, 0);
@ -1395,8 +1395,11 @@ do_define (char *name, enum variable_origin origin, struct ebuffer *ebuf)
p = parse_variable_definition (name, &var);
if (p == NULL)
/* No assignment token, so assume recursive. */
var.flavor = f_recursive;
{
/* No assignment token, so assume recursive. */
var.flavor = f_recursive;
var.conditional = 0;
}
else
{
if (var.value[0] != '\0')
@ -1480,8 +1483,8 @@ do_define (char *name, enum variable_origin origin, struct ebuffer *ebuf)
else
definition[idx - 1] = '\0';
v = do_variable_definition (&defstart, name,
definition, origin, var.flavor, 0);
v = do_variable_definition (&defstart, name, definition,
origin, var.flavor, var.conditional, 0);
free (definition);
free (n);
return (v);
@ -2954,10 +2957,10 @@ construct_include_path (const char **arg_dirs)
/* Now add each dir to the .INCLUDE_DIRS variable. */
do_variable_definition (NILF, ".INCLUDE_DIRS", "", o_default, f_simple, 0);
do_variable_definition (NILF, ".INCLUDE_DIRS", "", o_default, f_simple, 0, 0);
for (cpp = dirs; *cpp != 0; ++cpp)
do_variable_definition (NILF, ".INCLUDE_DIRS", *cpp,
o_default, f_append, 0);
o_default, f_append, 0, 0);
free ((void *) include_directories);
include_directories = dirs;

View File

@ -708,7 +708,7 @@ initialize_file_variables (struct file *file, int reading)
v = do_variable_definition (
&p->variable.fileinfo, p->variable.name,
p->variable.value, p->variable.origin,
p->variable.flavor, 1);
p->variable.flavor, p->variable.conditional, 1);
}
/* Also mark it as a per-target and copy export status. */
@ -1370,15 +1370,22 @@ shell_result (const char *p)
See the try_variable_definition() function for details on the parameters. */
struct variable *
do_variable_definition (const floc *flocp, const char *varname,
const char *value, enum variable_origin origin,
enum variable_flavor flavor, int target_var)
do_variable_definition (const floc *flocp, const char *varname, const char *value,
enum variable_origin origin, enum variable_flavor flavor,
int conditional, int target_var)
{
const char *newval;
char *alloc_value = NULL;
struct variable *v;
int append = 0;
int conditional = 0;
/* Conditional variable definition: only set if the var is not defined. */
if (conditional)
{
v = lookup_variable (varname, strlen (varname));
if (v)
return v;
}
/* Calculate the variable's new value in VALUE. */
@ -1421,16 +1428,6 @@ do_variable_definition (const floc *flocp, const char *varname,
newval = alloc_value;
break;
}
case f_conditional:
/* A conditional variable definition "var ?= value".
The value is set IFF the variable is not defined yet. */
v = lookup_variable (varname, strlen (varname));
if (v)
goto done;
conditional = 1;
flavor = f_recursive;
/* FALLTHROUGH */
case f_recursive:
/* A recursive variable definition "var = value".
The value is used verbatim. */
@ -1680,10 +1677,11 @@ do_variable_definition (const floc *flocp, const char *varname,
If it is a variable definition, return a pointer to the char after the
assignment token and set the following fields (only) of *VAR:
name : name of the variable (ALWAYS SET) (NOT NUL-TERMINATED!)
length : length of the variable name
value : value of the variable (nul-terminated)
flavor : flavor of the variable
name : name of the variable (ALWAYS SET) (NOT NUL-TERMINATED!)
length : length of the variable name
value : value of the variable (nul-terminated)
flavor : flavor of the variable
conditional : whether it's a conditional assignment
Other values in *VAR are unchanged.
*/
@ -1696,11 +1694,13 @@ parse_variable_definition (const char *str, struct variable *var)
NEXT_TOKEN (p);
var->name = (char *)p;
var->length = 0;
var->conditional = 0;
/* Walk through STR until we find a valid assignment operator. Each time
through this loop P points to the next character to consider. */
while (1)
{
const char *start;
int c = *p++;
/* If we find a comment or EOS, it's not a variable definition. */
@ -1718,26 +1718,36 @@ parse_variable_definition (const char *str, struct variable *var)
continue;
}
/* This is the start of a token. */
start = p - 1;
/* If we see a ? then it could be a conditional assignment. */
if (c == '?')
{
var->conditional = 1;
c = *p++;
}
/* If we found = we're done! */
if (c == '=')
{
if (!end)
end = p - 1;
var->flavor = f_recursive;
end = start;
var->flavor = f_recursive; /* = */
break;
}
if (c == ':')
{
if (!end)
end = p - 1;
end = start;
/* We need to distinguish :=, ::=, and :::=, versus : outside of an
assignment (which means this is not a variable definition). */
c = *p++;
if (c == '=')
{
var->flavor = f_simple;
var->flavor = f_simple; /* := */
break;
}
if (c == ':')
@ -1745,12 +1755,12 @@ parse_variable_definition (const char *str, struct variable *var)
c = *p++;
if (c == '=')
{
var->flavor = f_simple;
var->flavor = f_simple; /* ::= */
break;
}
if (c == ':' && *p++ == '=')
{
var->flavor = f_expand;
var->flavor = f_expand; /* :::= */
break;
}
}
@ -1763,20 +1773,17 @@ parse_variable_definition (const char *str, struct variable *var)
switch (c)
{
case '+':
var->flavor = f_append;
break;
case '?':
var->flavor = f_conditional;
var->flavor = f_append; /* += */
break;
case '!':
var->flavor = f_shell;
var->flavor = f_shell; /* != */
break;
default:
goto other;
}
if (!end)
end = p - 1;
end = start;
++p;
break;
}
@ -1790,12 +1797,15 @@ parse_variable_definition (const char *str, struct variable *var)
if (c == '$')
p = skip_reference (p);
var->conditional = 0;
}
/* We found a valid variable assignment: END points to the char after the
end of the variable name and P points to the char after the =. */
var->length = (unsigned int) (end - var->name);
var->value = next_token (p);
return (char *)p;
}
@ -1854,7 +1864,7 @@ try_variable_definition (const floc *flocp, const char *line,
return 0;
vp = do_variable_definition (flocp, v.name, v.value,
origin, v.flavor, target_var);
origin, v.flavor, v.conditional, target_var);
free (v.name);

View File

@ -39,7 +39,6 @@ enum variable_flavor
f_recursive, /* Recursive definition (=) */
f_expand, /* POSIX :::= assignment */
f_append, /* Appending definition (+=) */
f_conditional, /* Conditional definition (?=) */
f_shell, /* Shell assignment (!=) */
f_append_value /* Append unexpanded value */
};
@ -172,7 +171,7 @@ struct variable *do_variable_definition (const floc *flocp,
const char *name, const char *value,
enum variable_origin origin,
enum variable_flavor flavor,
int target_var);
int conditional, int target_var);
char *parse_variable_definition (const char *line,
struct variable *v);
struct variable *assign_variable_definition (struct variable *v, const char *line);

View File

@ -0,0 +1,137 @@
# -*-perl-*-
$description = "Test various flavors of conditional variable setting.";
$details = "";
# Test ?=
run_make_test(q!
x = bar
y = baz
foo ?= $(x)
biz?=$(y)
x = 10
y = 20
all:;@: $(info foo=$(foo) biz=$(biz))
!,
'', "foo=10 biz=20");
run_make_test(q!
foo=1
biz=2
x = bar
y = baz
foo ?= $(x)
biz?=$(y)
x = 10
y = 20
all:;@: $(info foo=$(foo) biz=$(biz))
!,
'', "foo=1 biz=2");
# Test ?:=
run_make_test(q!
x = bar
y = baz
foo ?:= $(x)
biz?:=$(y)
x = 10
y = 20
all:;@: $(info foo=$(foo) biz=$(biz))
!,
'', "foo=bar biz=baz");
run_make_test(q!
foo=1
biz=2
x = bar
y = baz
foo ?:= $(x)$(info expanded)
biz?:=$(y)$(info expanded)
x = 10
y = 20
all:;@: $(info foo=$(foo) biz=$(biz))
!,
'', "foo=1 biz=2");
# Test ?::=
run_make_test(q!
x = bar
y = baz
foo ?::= $(x)
biz?::=$(y)
x = 10
y = 20
all:;@: $(info foo=$(foo) biz=$(biz))
!,
'', "foo=bar biz=baz");
run_make_test(q!
foo=1
biz=2
x = bar
y = baz
foo ?::= $(x)$(info expanded)
biz?::=$(y)$(info expanded)
x = 10
y = 20
all:;@: $(info foo=$(foo) biz=$(biz))
!,
'', "foo=1 biz=2");
# Test ?:::=
run_make_test(q!
x = bar
y = baz
foo ?:::= $(x)
biz?:::=$(y)
x = 10
y = 20
all:;@: $(info foo=$(foo) biz=$(biz))
!,
'', "foo=bar biz=baz");
run_make_test(q!
foo=1
biz=2
x = bar
y = baz
foo ?:::= $(x)$(info expanded)
biz?:::=$(y)$(info expanded)
x = 10
y = 20
all:;@: $(info foo=$(foo) biz=$(biz))
!,
'', "foo=1 biz=2");
# Test ?!=
run_make_test(q/
x = bar
y = baz
foo ?!= echo $(x)
biz?!=echo $(y)
x = 10
y = 20
all:;@: $(info foo=$(foo) biz=$(biz))
/,
'', "foo=bar biz=baz");
run_make_test(q/
foo=1
biz=2
x = bar
y = baz
foo ?!= echo $(x)$(info expanded)
biz?!=echo $(y)$(info expanded)
x = 10
y = 20
all:;@: $(info foo=$(foo) biz=$(biz))
/,
'', "foo=1 biz=2");
1;