]> git.ipfire.org Git - thirdparty/make.git/commitdiff
Create $(let ...) providing lexically scoped variables
authorJouke Witteveen <j.witteveen@gmail.com>
Sun, 1 Nov 2020 21:48:53 +0000 (22:48 +0100)
committerPaul Smith <psmith@gnu.org>
Sun, 6 Dec 2020 23:30:58 +0000 (18:30 -0500)
Add a new function $(let ...) which allows lexically scoped variables.

* NEWS: Add information on this feature.
* doc/make.texi (Let Function): Document the 'let' function.
* src/function.c (func_let): Create the 'let' built-in function.
* tests/scripts/functions/let: Test the 'let' built-in function.

NEWS
doc/make.texi
src/function.c
tests/scripts/functions/let [new file with mode: 0644]

diff --git a/NEWS b/NEWS
index 6802a7ce563073c17563b0fc5cfa140bc0168b43..5d71488c4f76c579c6e37191ca10d8995b655193 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -34,6 +34,12 @@ https://sv.gnu.org/bugs/index.php?group=make&report_id=111&fix_release_id=109&se
   https://www.gnu.org/software/gnulib/manual/html_node/C99-features-assumed.html
   The configure script should verify the compiler has these features.
 
+* New feature: The $(let ...) function
+  This function allows user-defined functions to provide a lexically-scoped
+  set of variables: values can be assigned to these variables from within the
+  user-defined function and they will not impact global variable assignments.
+  Implementation provided by Jouke Witteveen <j.witteveen@gmail.com>
+
 * New debug option "print" will show the recipe to be run, even when silent
   mode is set.
 
index 62103efb5866ad7400cba0a733828e95002c154e..b9588124da7b34ef98374489c850b90ae8483ae0 100644 (file)
@@ -276,6 +276,7 @@ Functions for Transforming Text
 * Text Functions::              General-purpose text manipulation functions.
 * File Name Functions::         Functions for manipulating file names.
 * Conditional Functions::       Functions that implement conditions.
+* Let Function::                Lexically scoped variables.
 * Foreach Function::            Repeat some text with controlled variation.
 * File Function::               Write text to a file.
 * Call Function::               Expand a user-defined function.
@@ -7038,6 +7039,7 @@ be substituted.
 * Text Functions::              General-purpose text manipulation functions.
 * File Name Functions::         Functions for manipulating file names.
 * Conditional Functions::       Functions that implement conditions.
+* Let Function::                Lexically scoped variables.
 * Foreach Function::            Repeat some text with controlled variation.
 * File Function::               Write text to a file.
 * Call Function::               Expand a user-defined function.
@@ -7638,7 +7640,7 @@ the file names to refer to an existing file or directory.  Use the
 @code{wildcard} function to test for existence.
 @end table
 
-@node Conditional Functions, Foreach Function, File Name Functions, Functions
+@node Conditional Functions, Let Function, File Name Functions, Functions
 @section Functions for Conditionals
 @findex if
 @cindex conditional expansion
@@ -7691,14 +7693,84 @@ the result of the expansion is the expansion of the last argument.
 
 @end table
 
-@node Foreach Function, File Function, Conditional Functions, Functions
+@node Let Function, Foreach Function, Conditional Functions, Functions
+@section The @code{let} Function
+@findex let
+@cindex variables, lexically scoped
+
+The @code{let} function provides a means to limit the scope of a
+variable.  The assignment of the named variables in a @code{let}
+expression is in effect only within the text provided by the
+@code{let} expression, and this assignment doesn't impact that named
+variable in any outer scope.
+
+Additionally, the @code{let} function enables list unpacking by
+assigning all unassigned values to the last named variable.
+
+The syntax of the @code{let} function is:
+
+@example
+$(let @var{var} [@var{var} ...],[@var{list}],@var{text})
+@end example
+
+@noindent
+The first two arguments, @var{var} and @var{list}, are expanded before
+anything else is done; note that the last argument, @var{text}, is
+@strong{not} expanded at the same time.  Next, each word of the
+expanded value of @var{list} is bound to each of the variable names,
+@var{var}, in turn, with the final variable name being bound to the
+remainder of the expanded @var{list}.  In other words, the first word
+of @var{list} is bound to the first variable @var{var}, the second
+word to the second variable @var{var}, and so on.
+
+If there are more variable names in @var{var} than there are words in
+@var{list}, the remaining @var{var} variable names are set to the
+empty string.  If there are fewer @var{var}s than words in @var{list}
+then the last @var{var} is set to all remaining words in @var{list}.
+
+The variables in @var{var} are assigned as simply-expanded variables
+during the execution of @code{let}.  @xref{Flavors, ,The Two Flavors
+of Variables}.@refill
+
+After all variables are thus bound, @var{text} is expanded to provide
+the result of the @code{let} function.
+
+For example, this macro reverses the order of the words in the list
+that it is given as its first argument:
+
+@example
+reverse = $(let first rest,$1,$(if $(rest),$(call reverse,$(rest)) )$(first))
+
+all: ; @@echo $(call reverse,d c b a)
+@end example
+
+@noindent
+will print @code{a b c d}.  When first called, @code{let} will expand
+@var{$1} to @code{d c b a}.  It will then assign @var{first} to
+@code{d} and assign @var{rest} to @code{c b a}.  It will then expand
+the if-statement, where @code{$(rest)} is not empty so we recursively
+invoke the @var{reverse} function with the value of @var{rest} which
+is now @code{c b a}.  The recursive invocation of @code{let} assigns
+@var{first} to @code{c} and @var{rest} to @code{b a}.  The recursion
+continues until @code{let} is called with just a single value,
+@code{a}.  Here @var{first} is @code{a} and @var{rest} is empty, so we
+do not recurse but simply expand @code{$(first)} to @code{a} and
+return, which adds @code{ b}, etc.
+
+After the @var{reverse} call is complete, the @var{first} and
+@var{rest} variables are no longer set.  If variables by those names
+existed beforehand, they are not affected by the expansion of the
+@code{reverse} macro.
+
+@node Foreach Function, File Function, Let Function, Functions
 @section The @code{foreach} Function
 @findex foreach
 @cindex words, iterating over
 
-The @code{foreach} function is very different from other functions.  It
-causes one piece of text to be used repeatedly, each time with a different
-substitution performed on it.  It resembles the @code{for} command in the
+The @code{foreach} function is similar to the @code{let} function, but very
+different from other functions.  It causes one piece of text to be used
+repeatedly, each time with a different substitution performed on it.  The
+@code{foreach} function resembles the @code{for} command in the
 shell @code{sh} and the @code{foreach} command in the C-shell @code{csh}.
 
 The syntax of the @code{foreach} function is:
@@ -7757,13 +7829,14 @@ actual function call to be re-expanded under the control of @code{foreach};
 a simply-expanded variable would not do, since @code{wildcard} would be
 called only once at the time of defining @code{find_files}.
 
-The @code{foreach} function has no permanent effect on the variable
-@var{var}; its value and flavor after the @code{foreach} function call are
-the same as they were beforehand.  The other values which are taken from
-@var{list} are in effect only temporarily, during the execution of
-@code{foreach}.  The variable @var{var} is a simply-expanded variable
-during the execution of @code{foreach}.  If @var{var} was undefined
-before the @code{foreach} function call, it is undefined after the call.
+Like the @code{let} function, the @code{foreach} function has no permanent
+effect on the variable @var{var}; its value and flavor after the
+@code{foreach} function call are the same as they were beforehand.  The
+other values which are taken from @var{list} are in effect only
+temporarily, during the execution of @code{foreach}.  The variable
+@var{var} is a simply-expanded variable during the execution of
+@code{foreach}.  If @var{var} was undefined before the @code{foreach}
+function call, it is undefined after the call.
 @xref{Flavors, ,The Two Flavors of Variables}.@refill
 
 You must take care when using complex variable expressions that result in
@@ -12409,6 +12482,11 @@ Return a string describing the flavor of the @code{make} variable
 @var{variable}.@*
 @xref{Flavor Function, , The @code{flavor} Function}.
 
+@item $(let @var{var} [@var{var} ...],@var{words},@var{text})
+Evaluate @var{text} with the @var{var}s bound to the words in
+@var{words}.@*
+@xref{Let Function, ,The @code{let} Function}.
+
 @item $(foreach @var{var},@var{words},@var{text})
 Evaluate @var{text} with @var{var} bound to each word in @var{words},
 and concatenate the results.@*
index 87f2a8b3667aa71fe6ee1cbe7d3bb01ddf97c7f0..d36b7951cb27f8faba6f5d7c3a1aebf48e000c2a 100644 (file)
@@ -908,6 +908,56 @@ func_foreach (char *o, char **argv, const char *funcname UNUSED)
   return o;
 }
 
+static char *
+func_let (char *o, char **argv, const char *funcname UNUSED)
+{
+  /* expand only the first two.  */
+  char *varnames = expand_argument (argv[0], NULL);
+  char *list = expand_argument (argv[1], NULL);
+  const char *body = argv[2];
+
+  const char *vp;
+  const char *vp_next = varnames;
+  const char *list_iterator = list;
+  char *p;
+  size_t len;
+  size_t vlen;
+
+  push_new_variable_scope ();
+
+  /* loop through LIST for all but the last VARNAME */
+  vp = find_next_token (&vp_next, &vlen);
+  NEXT_TOKEN (vp_next);
+  while (*vp_next != '\0')
+    {
+      p = find_next_token (&list_iterator, &len);
+      if (*list_iterator != '\0')
+        {
+          ++list_iterator;
+          p[len] = '\0';
+        }
+      define_variable (vp, vlen, p ? p : "", o_automatic, 0);
+
+      vp = find_next_token (&vp_next, &vlen);
+      NEXT_TOKEN (vp_next);
+    }
+
+  /* set the last VARNAME to the remainder of LIST */
+  if (vp)
+    define_variable (vp, vlen, next_token (list_iterator), o_automatic, 0);
+
+  /* Expand the body in the context of the arguments, adding the result to
+     the variable buffer.  */
+
+  o = variable_expand_string (o, body, SIZE_MAX);
+
+  pop_variable_scope ();
+  free (varnames);
+  free (list);
+
+  return o + strlen (o);
+}
+
 struct a_word
 {
   struct a_word *chain;
@@ -2344,7 +2394,8 @@ func_abspath (char *o, char **argv, const char *funcname UNUSED)
    comma-separated values are treated as arguments.
 
    EXPAND_ARGS means that all arguments should be expanded before invocation.
-   Functions that do namespace tricks (foreach) don't automatically expand.  */
+   Functions that do namespace tricks (foreach, let) don't automatically
+   expand.  */
 
 static char *func_call (char *o, char **argv, const char *funcname);
 
@@ -2380,6 +2431,7 @@ static struct function_table_entry function_table_init[] =
   FT_ENTRY ("words",         0,  1,  1,  func_words),
   FT_ENTRY ("origin",        0,  1,  1,  func_origin),
   FT_ENTRY ("foreach",       3,  3,  0,  func_foreach),
+  FT_ENTRY ("let",           3,  3,  0,  func_let),
   FT_ENTRY ("call",          1,  0,  1,  func_call),
   FT_ENTRY ("info",          0,  1,  1,  func_error),
   FT_ENTRY ("error",         0,  1,  1,  func_error),
diff --git a/tests/scripts/functions/let b/tests/scripts/functions/let
new file mode 100644 (file)
index 0000000..f1012e5
--- /dev/null
@@ -0,0 +1,110 @@
+#                                                                    -*-perl-*-
+# $Id$
+
+$description = "Test the let function.";
+
+$details = "This is a test of the let function in gnu make.
+This function destructures a list of values and binds each
+value to a variable name in a list of variable names.
+Superfluous variable names are assigned the empty string and
+the remaining values are assigned to the last variable name.
+The binding holds for the duration of the evaluation of the
+given text and no longer.  The general form of the command
+is $(let \$vars,\$list,\$text).  Several types of let
+assignments are tested\n";
+
+# check for mismatched var and list word counts
+run_make_test(q!
+a = bad
+b = news
+x = $(let a b,1 2,$a $b)
+y = $(let a,1 2,$a)
+z = $(let a b,1,$a $b)
+all:;@echo 'a=,$a,' 'b=,$b,' 'x=,$x,' 'y=,$y,' 'z=,$z,'
+!,
+    '', "a=,bad, b=,news, x=,1 2, y=,1 2, z=,1 ,\n");
+
+# check for whitespace
+run_make_test(q!
+a = bad
+b = news
+x = $(let a b,   1   2   ,+$a+$b+)
+y = $(let a,   1  2   ,+$a+)
+z = $(let a b,  1   ,+$a+$b+)
+all:;@echo 'a=,$a,' 'b=,$b,' 'x=,$x,' 'y=,$y,' 'z=,$z,'
+!,
+    '', "a=,bad, b=,news, x=,+1+2   +, y=,+1  2   +, z=,+1++,\n");
+
+# Allow empty variable names and empty value list.
+# We still expand the list and body.
+run_make_test('
+null =
+x = $(let $(null),$(info side-effect),abc)
+y = $(let y,,$ydef)
+
+all: ; @echo $x$y',
+              '', "side-effect\nabcdef\n");
+
+# The example macro from the manual.
+run_make_test('
+reverse = $(let first rest,$1,$(if $(rest),$(call reverse,$(rest)) )$(first))
+
+all: ; @echo $(call reverse, \
+                 moe   miny  meeny eeny \
+              )',
+              '', "eeny meeny miny moe\n");
+
+
+# Set an environment variable that we can test in the makefile.
+$ENV{FOOFOO} = 'foo foo';
+
+# Verify masking: expansion outside the scope of let is unaffected.
+run_make_test('
+auto_var = \
+  udef \
+  CC \
+  FOOFOO \
+  MAKE \
+  foo \
+  CFLAGS \
+  WHITE \
+  @ \
+  <
+av = $(foreach var, $(auto_var), $(origin $(var)) )
+foo = bletch null @ garf
+override WHITE := BLACK
+
+define mktarget
+target: foo := $(foo)
+target: ; @echo $(AR)_$(foo)_
+endef
+
+all: auto target
+auto: ; @echo $(let $(auto_var),,$(av)) $(av)
+$(let AR foo,bar foo ,$(eval $(value mktarget)))',
+              '-e WHITE=WHITE CFLAGS=',
+              "automatic automatic automatic automatic automatic automatic automatic automatic automatic undefined default environment default file command line override automatic automatic
+ar_foo _
+");
+
+
+# Check some error conditions.
+run_make_test('
+x = $(let )
+y = $x
+
+all: ; @echo $y',
+              '',
+              "#MAKEFILE#:2: *** insufficient number of arguments (1) to function 'let'.  Stop.",
+              512);
+
+run_make_test('
+x = $(let x,y)
+y := $x
+
+all: ; @echo $y',
+              '',
+              "#MAKEFILE#:2: *** insufficient number of arguments (2) to function 'let'.  Stop.",
+              512);
+
+1;