]> git.ipfire.org Git - thirdparty/make.git/commitdiff
Support conditional modifiers on all assignment operators
authorPaul Smith <psmith@gnu.org>
Thu, 11 Jan 2024 15:05:27 +0000 (10:05 -0500)
committerPaul Smith <psmith@gnu.org>
Sun, 28 Jan 2024 19:20:47 +0000 (14:20 -0500)
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.

NEWS
doc/make.texi
src/load.c
src/read.c
src/variable.c
src/variable.h
tests/scripts/variables/conditional [new file with mode: 0644]

diff --git a/NEWS b/NEWS
index 7d296ac74db1998d8b35ebd64f05a17aa46f5e07..676794d6c6177957f0c02f17f5f20a64784d73f1 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -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
index 21bee9873565dac4dbd633d8cd1e0d6eb880fbf6..b2e0141ccb641a2f11170316434389ef6af2d0d9 100644 (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}.
index b76ddb0138e9f30057f4c1a87d8690bff4cdfef9..626c34f0665f1960a7b81a87da1bdb2e53c857e9 100644 (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;
 }
index a6ee0450645b6474276aada1364a9672178822be..069f9c3032361c0ae6120b20f112539d285a15fa 100644 (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;
index 8b63df2d82850b1dfd2b22508738fcba2f263f2a..5cd28bf73a0955bc923a5e84b425fc81896d1a73 100644 (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;
 }
 \f
@@ -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);
 
index a82b512c51c120e149b9e8d082a4d6d273124f6f..0b84c2e5a563f8b54ce563d2274e538f42c1af9b 100644 (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);
diff --git a/tests/scripts/variables/conditional b/tests/scripts/variables/conditional
new file mode 100644 (file)
index 0000000..eb3424c
--- /dev/null
@@ -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;