aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/testscript.cli46
-rw-r--r--libbuild2/buildfile2
-rw-r--r--libbuild2/script/builtin-options.cxx297
-rw-r--r--libbuild2/script/builtin-options.hxx118
-rw-r--r--libbuild2/script/builtin-options.ixx111
-rw-r--r--libbuild2/script/builtin.cli8
-rw-r--r--libbuild2/script/parser.cxx24
-rw-r--r--libbuild2/script/run.cxx100
-rw-r--r--libbuild2/script/script.cxx94
-rw-r--r--libbuild2/script/script.hxx60
-rw-r--r--libbuild2/test/script/parser+env.test.testscript2
-rw-r--r--libbuild2/test/script/script.cxx8
-rw-r--r--libbuild2/test/script/script.hxx7
-rw-r--r--tests/recipe/buildscript/testscript37
-rw-r--r--tests/test/script/runner/driver.cxx4
-rw-r--r--tests/test/script/runner/export.testscript133
16 files changed, 1000 insertions, 51 deletions
diff --git a/doc/testscript.cli b/doc/testscript.cli
index badbe97..4721a88 100644
--- a/doc/testscript.cli
+++ b/doc/testscript.cli
@@ -2461,12 +2461,14 @@ env - --unset=FOO -- $*
\li|\n\c{-t|--timeout <sec>}
Terminate the command if it fails to complete within the specified number
- of seconds. See also \l{#builtins-timeout \c{timeout}} builtin.|
+ of seconds. See also the \l{#builtins-timeout \c{timeout}} builtin.|
\li|\n\c{-u|--unset <name>}
Remove the specified variable from the environment.||
+See also the \l{#builtins-export \c{export}} builtin.
+
\h#builtins-exit|\c{exit}|
@@ -2488,6 +2490,38 @@ the outer scopes unsuccessfully, as if the \c{exit} command failed. In this
case the argument must be the diagnostics string describing the error.
+\h#builtins-export|\c{export}|
+
+\
+export [-c <name>]... [-u <name>]... [<name>=<value>]...
+\
+
+Add/remove the variables to/from the current scope commands execution
+environment and/or clear the previous additions/removals.
+
+Note that \c{export} is a \i{pseudo-builtin}. In particular, it must be the
+only command in the pipe expression, it either succeeds or terminates
+abnormally, and its standard streams cannot be redirected.
+
+The environment variables can be added and removed on multiple levels: with
+the \c{export} builtin in the nested test group scopes and the test scope and
+with the \l{#builtins-env \c{env}} builtin for individual commands. Before
+executing a command, all the variable additions and removals from its
+environment hierarchy are merged so that those specified in the inner levels
+override those specified in the outer levels.
+
+\dl|
+
+\li|\n\c{-c|--clear <name>}
+
+ Clear the previous variable addition/removal to/from the environment, if
+ exists.|
+
+\li|\n\c{-u|--unset <name>}
+
+ Remove the specified variable from the environment.||
+
+
\h#builtins-false|\c{false}|
\
@@ -2866,11 +2900,11 @@ The timeouts can be set on multiple levels: via the \c{config.test.timeout}
variable on the (potentially nested) project root scopes (see
\l{build2#module-test \c{test}} module for details), with the \c{timeout}
builtin in the nested test group scopes and the test scope, and with the
-\c{env} builtin for individual commands. Each command must complete before the
-nearest timeout from its timeout hierarchy. Failed that, a command is
-terminated forcibly causing the entire \c{test} operation to fail unless the
-expired timeout was specified with the \c{--success} option, in which case the
-timed out command is assumed to have succeeded.
+\l{#builtins-env \c{env}} builtin for individual commands. Each command must
+complete before the nearest timeout from its timeout hierarchy. Failed that, a
+command is terminated forcibly causing the entire \c{test} operation to fail
+unless the expired timeout was specified with the \c{--success} option, in
+which case the timed out command is assumed to have succeeded.
\dl|
diff --git a/libbuild2/buildfile b/libbuild2/buildfile
index 831a9b9..28adbdd 100644
--- a/libbuild2/buildfile
+++ b/libbuild2/buildfile
@@ -220,7 +220,7 @@ script/
cli.options += --std c++11 -I $src_root --include-with-brackets \
--include-prefix libbuild2/script --guard-prefix LIBBUILD2_SCRIPT \
--cli-namespace build2::script::cli --generate-vector-scanner \
---generate-specifier --suppress-usage
+--generate-modifier --generate-specifier --suppress-usage
cli.cxx{*}:
{
diff --git a/libbuild2/script/builtin-options.cxx b/libbuild2/script/builtin-options.cxx
index 9b91bd2..c27f266 100644
--- a/libbuild2/script/builtin-options.cxx
+++ b/libbuild2/script/builtin-options.cxx
@@ -923,6 +923,303 @@ namespace build2
return r;
}
+
+ // export_options
+ //
+
+ export_options::
+ export_options ()
+ : unset_ (),
+ unset_specified_ (false),
+ clear_ (),
+ clear_specified_ (false)
+ {
+ }
+
+ export_options::
+ export_options (int& argc,
+ char** argv,
+ bool erase,
+ ::build2::script::cli::unknown_mode opt,
+ ::build2::script::cli::unknown_mode arg)
+ : unset_ (),
+ unset_specified_ (false),
+ clear_ (),
+ clear_specified_ (false)
+ {
+ ::build2::script::cli::argv_scanner s (argc, argv, erase);
+ _parse (s, opt, arg);
+ }
+
+ export_options::
+ export_options (int start,
+ int& argc,
+ char** argv,
+ bool erase,
+ ::build2::script::cli::unknown_mode opt,
+ ::build2::script::cli::unknown_mode arg)
+ : unset_ (),
+ unset_specified_ (false),
+ clear_ (),
+ clear_specified_ (false)
+ {
+ ::build2::script::cli::argv_scanner s (start, argc, argv, erase);
+ _parse (s, opt, arg);
+ }
+
+ export_options::
+ export_options (int& argc,
+ char** argv,
+ int& end,
+ bool erase,
+ ::build2::script::cli::unknown_mode opt,
+ ::build2::script::cli::unknown_mode arg)
+ : unset_ (),
+ unset_specified_ (false),
+ clear_ (),
+ clear_specified_ (false)
+ {
+ ::build2::script::cli::argv_scanner s (argc, argv, erase);
+ _parse (s, opt, arg);
+ end = s.end ();
+ }
+
+ export_options::
+ export_options (int start,
+ int& argc,
+ char** argv,
+ int& end,
+ bool erase,
+ ::build2::script::cli::unknown_mode opt,
+ ::build2::script::cli::unknown_mode arg)
+ : unset_ (),
+ unset_specified_ (false),
+ clear_ (),
+ clear_specified_ (false)
+ {
+ ::build2::script::cli::argv_scanner s (start, argc, argv, erase);
+ _parse (s, opt, arg);
+ end = s.end ();
+ }
+
+ export_options::
+ export_options (::build2::script::cli::scanner& s,
+ ::build2::script::cli::unknown_mode opt,
+ ::build2::script::cli::unknown_mode arg)
+ : unset_ (),
+ unset_specified_ (false),
+ clear_ (),
+ clear_specified_ (false)
+ {
+ _parse (s, opt, arg);
+ }
+
+ typedef
+ std::map<std::string, void (*) (export_options&, ::build2::script::cli::scanner&)>
+ _cli_export_options_map;
+
+ static _cli_export_options_map _cli_export_options_map_;
+
+ struct _cli_export_options_map_init
+ {
+ _cli_export_options_map_init ()
+ {
+ _cli_export_options_map_["--unset"] =
+ &::build2::script::cli::thunk< export_options, vector<string>, &export_options::unset_,
+ &export_options::unset_specified_ >;
+ _cli_export_options_map_["-u"] =
+ &::build2::script::cli::thunk< export_options, vector<string>, &export_options::unset_,
+ &export_options::unset_specified_ >;
+ _cli_export_options_map_["--clear"] =
+ &::build2::script::cli::thunk< export_options, vector<string>, &export_options::clear_,
+ &export_options::clear_specified_ >;
+ _cli_export_options_map_["-c"] =
+ &::build2::script::cli::thunk< export_options, vector<string>, &export_options::clear_,
+ &export_options::clear_specified_ >;
+ }
+ };
+
+ static _cli_export_options_map_init _cli_export_options_map_init_;
+
+ bool export_options::
+ _parse (const char* o, ::build2::script::cli::scanner& s)
+ {
+ _cli_export_options_map::const_iterator i (_cli_export_options_map_.find (o));
+
+ if (i != _cli_export_options_map_.end ())
+ {
+ (*(i->second)) (*this, s);
+ return true;
+ }
+
+ return false;
+ }
+
+ bool export_options::
+ _parse (::build2::script::cli::scanner& s,
+ ::build2::script::cli::unknown_mode opt_mode,
+ ::build2::script::cli::unknown_mode arg_mode)
+ {
+ // Can't skip combined flags (--no-combined-flags).
+ //
+ assert (opt_mode != ::build2::script::cli::unknown_mode::skip);
+
+ bool r = false;
+ bool opt = true;
+
+ while (s.more ())
+ {
+ const char* o = s.peek ();
+
+ if (std::strcmp (o, "--") == 0)
+ {
+ opt = false;
+ s.skip ();
+ r = true;
+ continue;
+ }
+
+ if (opt)
+ {
+ if (_parse (o, s))
+ {
+ r = true;
+ continue;
+ }
+
+ if (std::strncmp (o, "-", 1) == 0 && o[1] != '\0')
+ {
+ // Handle combined option values.
+ //
+ std::string co;
+ if (const char* v = std::strchr (o, '='))
+ {
+ co.assign (o, 0, v - o);
+ ++v;
+
+ int ac (2);
+ char* av[] =
+ {
+ const_cast<char*> (co.c_str ()),
+ const_cast<char*> (v)
+ };
+
+ ::build2::script::cli::argv_scanner ns (0, ac, av);
+
+ if (_parse (co.c_str (), ns))
+ {
+ // Parsed the option but not its value?
+ //
+ if (ns.end () != 2)
+ throw ::build2::script::cli::invalid_value (co, v);
+
+ s.next ();
+ r = true;
+ continue;
+ }
+ else
+ {
+ // Set the unknown option and fall through.
+ //
+ o = co.c_str ();
+ }
+ }
+
+ // Handle combined flags.
+ //
+ char cf[3];
+ {
+ const char* p = o + 1;
+ for (; *p != '\0'; ++p)
+ {
+ if (!((*p >= 'a' && *p <= 'z') ||
+ (*p >= 'A' && *p <= 'Z') ||
+ (*p >= '0' && *p <= '9')))
+ break;
+ }
+
+ if (*p == '\0')
+ {
+ for (p = o + 1; *p != '\0'; ++p)
+ {
+ std::strcpy (cf, "-");
+ cf[1] = *p;
+ cf[2] = '\0';
+
+ int ac (1);
+ char* av[] =
+ {
+ cf
+ };
+
+ ::build2::script::cli::argv_scanner ns (0, ac, av);
+
+ if (!_parse (cf, ns))
+ break;
+ }
+
+ if (*p == '\0')
+ {
+ // All handled.
+ //
+ s.next ();
+ r = true;
+ continue;
+ }
+ else
+ {
+ // Set the unknown option and fall through.
+ //
+ o = cf;
+ }
+ }
+ }
+
+ switch (opt_mode)
+ {
+ case ::build2::script::cli::unknown_mode::skip:
+ {
+ s.skip ();
+ r = true;
+ continue;
+ }
+ case ::build2::script::cli::unknown_mode::stop:
+ {
+ break;
+ }
+ case ::build2::script::cli::unknown_mode::fail:
+ {
+ throw ::build2::script::cli::unknown_option (o);
+ }
+ }
+
+ break;
+ }
+ }
+
+ switch (arg_mode)
+ {
+ case ::build2::script::cli::unknown_mode::skip:
+ {
+ s.skip ();
+ r = true;
+ continue;
+ }
+ case ::build2::script::cli::unknown_mode::stop:
+ {
+ break;
+ }
+ case ::build2::script::cli::unknown_mode::fail:
+ {
+ throw ::build2::script::cli::unknown_argument (o);
+ }
+ }
+
+ break;
+ }
+
+ return r;
+ }
}
}
diff --git a/libbuild2/script/builtin-options.hxx b/libbuild2/script/builtin-options.hxx
index d0d3c31..f6544cf 100644
--- a/libbuild2/script/builtin-options.hxx
+++ b/libbuild2/script/builtin-options.hxx
@@ -257,6 +257,8 @@ namespace build2
}
}
+#include <libbuild2/types.hxx>
+
namespace build2
{
namespace script
@@ -298,17 +300,35 @@ namespace build2
::build2::script::cli::unknown_mode option = ::build2::script::cli::unknown_mode::fail,
::build2::script::cli::unknown_mode argument = ::build2::script::cli::unknown_mode::stop);
- // Option accessors.
+ // Option accessors and modifiers.
//
const bool&
exact () const;
+ bool&
+ exact ();
+
+ void
+ exact (const bool&);
+
const bool&
newline () const;
+ bool&
+ newline ();
+
+ void
+ newline (const bool&);
+
const bool&
whitespace () const;
+ bool&
+ whitespace ();
+
+ void
+ whitespace (const bool&);
+
// Implementation details.
//
protected:
@@ -364,11 +384,17 @@ namespace build2
::build2::script::cli::unknown_mode option = ::build2::script::cli::unknown_mode::fail,
::build2::script::cli::unknown_mode argument = ::build2::script::cli::unknown_mode::stop);
- // Option accessors.
+ // Option accessors and modifiers.
//
const bool&
success () const;
+ bool&
+ success ();
+
+ void
+ success (const bool&);
+
// Implementation details.
//
protected:
@@ -384,6 +410,94 @@ namespace build2
public:
bool success_;
};
+
+ class export_options
+ {
+ public:
+ export_options ();
+
+ export_options (int& argc,
+ char** argv,
+ bool erase = false,
+ ::build2::script::cli::unknown_mode option = ::build2::script::cli::unknown_mode::fail,
+ ::build2::script::cli::unknown_mode argument = ::build2::script::cli::unknown_mode::stop);
+
+ export_options (int start,
+ int& argc,
+ char** argv,
+ bool erase = false,
+ ::build2::script::cli::unknown_mode option = ::build2::script::cli::unknown_mode::fail,
+ ::build2::script::cli::unknown_mode argument = ::build2::script::cli::unknown_mode::stop);
+
+ export_options (int& argc,
+ char** argv,
+ int& end,
+ bool erase = false,
+ ::build2::script::cli::unknown_mode option = ::build2::script::cli::unknown_mode::fail,
+ ::build2::script::cli::unknown_mode argument = ::build2::script::cli::unknown_mode::stop);
+
+ export_options (int start,
+ int& argc,
+ char** argv,
+ int& end,
+ bool erase = false,
+ ::build2::script::cli::unknown_mode option = ::build2::script::cli::unknown_mode::fail,
+ ::build2::script::cli::unknown_mode argument = ::build2::script::cli::unknown_mode::stop);
+
+ export_options (::build2::script::cli::scanner&,
+ ::build2::script::cli::unknown_mode option = ::build2::script::cli::unknown_mode::fail,
+ ::build2::script::cli::unknown_mode argument = ::build2::script::cli::unknown_mode::stop);
+
+ // Option accessors and modifiers.
+ //
+ const vector<string>&
+ unset () const;
+
+ vector<string>&
+ unset ();
+
+ void
+ unset (const vector<string>&);
+
+ bool
+ unset_specified () const;
+
+ void
+ unset_specified (bool);
+
+ const vector<string>&
+ clear () const;
+
+ vector<string>&
+ clear ();
+
+ void
+ clear (const vector<string>&);
+
+ bool
+ clear_specified () const;
+
+ void
+ clear_specified (bool);
+
+ // Implementation details.
+ //
+ protected:
+ bool
+ _parse (const char*, ::build2::script::cli::scanner&);
+
+ private:
+ bool
+ _parse (::build2::script::cli::scanner&,
+ ::build2::script::cli::unknown_mode option,
+ ::build2::script::cli::unknown_mode argument);
+
+ public:
+ vector<string> unset_;
+ bool unset_specified_;
+ vector<string> clear_;
+ bool clear_specified_;
+ };
}
}
diff --git a/libbuild2/script/builtin-options.ixx b/libbuild2/script/builtin-options.ixx
index 5edf31a..bbd12b1 100644
--- a/libbuild2/script/builtin-options.ixx
+++ b/libbuild2/script/builtin-options.ixx
@@ -162,18 +162,54 @@ namespace build2
return this->exact_;
}
+ inline bool& set_options::
+ exact ()
+ {
+ return this->exact_;
+ }
+
+ inline void set_options::
+ exact (const bool& x)
+ {
+ this->exact_ = x;
+ }
+
inline const bool& set_options::
newline () const
{
return this->newline_;
}
+ inline bool& set_options::
+ newline ()
+ {
+ return this->newline_;
+ }
+
+ inline void set_options::
+ newline (const bool& x)
+ {
+ this->newline_ = x;
+ }
+
inline const bool& set_options::
whitespace () const
{
return this->whitespace_;
}
+ inline bool& set_options::
+ whitespace ()
+ {
+ return this->whitespace_;
+ }
+
+ inline void set_options::
+ whitespace (const bool& x)
+ {
+ this->whitespace_ = x;
+ }
+
// timeout_options
//
@@ -182,6 +218,81 @@ namespace build2
{
return this->success_;
}
+
+ inline bool& timeout_options::
+ success ()
+ {
+ return this->success_;
+ }
+
+ inline void timeout_options::
+ success (const bool& x)
+ {
+ this->success_ = x;
+ }
+
+ // export_options
+ //
+
+ inline const vector<string>& export_options::
+ unset () const
+ {
+ return this->unset_;
+ }
+
+ inline vector<string>& export_options::
+ unset ()
+ {
+ return this->unset_;
+ }
+
+ inline void export_options::
+ unset (const vector<string>& x)
+ {
+ this->unset_ = x;
+ }
+
+ inline bool export_options::
+ unset_specified () const
+ {
+ return this->unset_specified_;
+ }
+
+ inline void export_options::
+ unset_specified (bool x)
+ {
+ this->unset_specified_ = x;
+ }
+
+ inline const vector<string>& export_options::
+ clear () const
+ {
+ return this->clear_;
+ }
+
+ inline vector<string>& export_options::
+ clear ()
+ {
+ return this->clear_;
+ }
+
+ inline void export_options::
+ clear (const vector<string>& x)
+ {
+ this->clear_ = x;
+ }
+
+ inline bool export_options::
+ clear_specified () const
+ {
+ return this->clear_specified_;
+ }
+
+ inline void export_options::
+ clear_specified (bool x)
+ {
+ this->clear_specified_ = x;
+ }
}
}
diff --git a/libbuild2/script/builtin.cli b/libbuild2/script/builtin.cli
index 1a6f523..1e3fb45 100644
--- a/libbuild2/script/builtin.cli
+++ b/libbuild2/script/builtin.cli
@@ -1,6 +1,8 @@
// file : libbuild2/script/builtin.cli
// license : MIT; see accompanying LICENSE file
+include <libbuild2/types.hxx>;
+
// Note that options in this file are undocumented because we generate neither
// the usage printing code nor man pages. Instead, they are documented in the
// Testscript Language Manual's builtin descriptions.
@@ -22,5 +24,11 @@ namespace build2
{
bool --success|-s;
};
+
+ class export_options
+ {
+ vector<string> --unset|-u;
+ vector<string> --clear|-c;
+ };
}
}
diff --git a/libbuild2/script/parser.cxx b/libbuild2/script/parser.cxx
index b4184ea..41d3092 100644
--- a/libbuild2/script/parser.cxx
+++ b/libbuild2/script/parser.cxx
@@ -1437,36 +1437,22 @@ namespace build2
}
else if (optional<string> v = str ("--unset", "-u"))
{
- if (v->find ('=') != string::npos)
- fail (i->second) << "env: invalid value '" << *v << "' for "
- << "option '" << o << "': contains '='";
+ verify_environment_var_name (*v, o.c_str (), "env: ", i->second);
- r.variables.push_back (move (*v));
+ r.variables.add (move (*v));
}
else
break;
}
- // Parse the variable sets (from arguments).
+ // Parse arguments (variable sets).
//
for (; i != e; ++i)
{
string& a (i->first);
+ verify_environment_var_assignment (a, "env: ", i->second);
- // Validate the variable assignment.
- //
- size_t p (a.find ('='));
-
- if (p == string::npos)
- fail (i->second)
- << "env: expected variable assignment instead of '" << a << "'";
-
- if (p == 0)
- fail (i->second) << "env: empty variable name";
-
- // Add the variable set to the resulting list.
- //
- r.variables.push_back (move (a));
+ r.variables.add (move (a));
}
return r;
diff --git a/libbuild2/script/run.cxx b/libbuild2/script/run.cxx
index b1bc888..58ba23d 100644
--- a/libbuild2/script/run.cxx
+++ b/libbuild2/script/run.cxx
@@ -764,10 +764,66 @@ namespace build2
return false;
}
+ // The export pseudo-builtin: add/remove the variables to/from the script
+ // commands execution environment and/or clear the previous additions/
+ // removals.
+ //
+ // export [-c|--clear <name>]... [-u|--unset <name>]... [<name>=<value>]...
+ //
+ static void
+ export_builtin (environment& env, const strings& args, const location& ll)
+ {
+ try
+ {
+ cli::vector_scanner scan (args);
+ export_options ops (scan);
+
+ // Validate a variable name.
+ //
+ auto verify_name = [&ll] (const string& name, const char* opt)
+ {
+ verify_environment_var_name (name, opt, "export: ", ll);
+ };
+
+ // Parse options (variable set/unset cleanups and unsets).
+ //
+ for (const string& v: ops.clear ())
+ {
+ verify_name (v, "-c|--clear");
+
+ environment_vars::iterator i (env.exported_vars.find (v));
+
+ if (i != env.exported_vars.end ())
+ env.exported_vars.erase (i);
+ }
+
+ for (string& v: ops.unset ())
+ {
+ verify_name (v, "-u|--unset");
+
+ env.exported_vars.add (move (v));
+ }
+
+ // Parse arguments (variable sets).
+ //
+ while (scan.more ())
+ {
+ string a (scan.next ());
+ verify_environment_var_assignment (a, "export: ", ll);
+
+ env.exported_vars.add (move (a));
+ }
+ }
+ catch (const cli::exception& e)
+ {
+ fail (ll) << "export: " << e;
+ }
+ }
+
// The timeout pseudo-builtin: set the script timeout. See the script-
// specific set_timeout() implementations for the exact semantics.
//
- // timeout [--success|-s] <timeout>
+ // timeout [-s|--success] <timeout>
//
static void
timeout_builtin (environment& env,
@@ -1180,17 +1236,19 @@ namespace build2
};
// Prior to opening file descriptors for command input/output redirects
- // let's check if the command is the timeout or exit builtin. Being a
- // builtin syntactically they differ from the regular ones in a number
- // of ways. They don't communicate with standard streams, so redirecting
- // them is meaningless. They may appear only as a single command in a
- // pipeline. They don't return any value, so checking their exit status
- // is meaningless as well. That all means we can short-circuit here
- // calling the builtin and bailing out right after that. Checking that
- // the user didn't specify any variables, timeout, redirects, or exit
- // code check sounds like a right thing to do.
+ // let's check if the command is the exit, export, or timeout
+ // builtin. Being a builtin syntactically they differ from the regular
+ // ones in a number of ways. They don't communicate with standard
+ // streams, so redirecting them is meaningless. They may appear only as
+ // a single command in a pipeline. They don't return any value, so
+ // checking their exit status is meaningless as well. That all means we
+ // can short-circuit here calling the builtin and bailing out right
+ // after that. Checking that the user didn't specify any variables,
+ // timeout, redirects, or exit code check sounds like a right thing to
+ // do.
//
- if (resolve && (program == "timeout" || program == "exit"))
+ if (resolve &&
+ (program == "exit" || program == "export" || program == "timeout"))
{
// In case the builtin is erroneously pipelined from the other
// command, we will close stdin gracefully (reading out the stream
@@ -1233,13 +1291,20 @@ namespace build2
if (verb >= 2)
print_process (process_args ());
- if (program == "timeout")
+ if (program == "exit")
+ {
+ exit_builtin (c.arguments, ll); // Throws exit exception.
+ }
+ else if (program == "export")
+ {
+ export_builtin (env, c.arguments, ll);
+ return true;
+ }
+ else if (program == "timeout")
{
timeout_builtin (env, c.arguments, ll);
return true;
}
- else if (program == "exit")
- exit_builtin (c.arguments, ll); // Throws exit exception.
else
assert (false);
}
@@ -2056,9 +2121,14 @@ namespace build2
? process::path_search (args[0])
: process_path ());
+ environment_vars vss;
+ const environment_vars& vs (
+ env.merge_exported_variables (c.variables, vss));
+
// Note that CWD and builtin-escaping character '^' are not printed.
//
- process_env pe (resolve ? pp : c.program, c.variables);
+ const small_vector<string, 4>& evs (vs);
+ process_env pe (resolve ? pp : c.program, evs);
if (verb >= 2)
print_process (pe, args);
diff --git a/libbuild2/script/script.cxx b/libbuild2/script/script.cxx
index f540687..db53418 100644
--- a/libbuild2/script/script.cxx
+++ b/libbuild2/script/script.cxx
@@ -609,6 +609,34 @@ namespace build2
}
}
+ // environment_vars
+ //
+ environment_vars::iterator environment_vars::
+ find (const string& var)
+ {
+ size_t n (var.find ('='));
+ if (n == string::npos)
+ n = var.size ();
+
+ return find_if (begin (), end (),
+ [&var, n] (const string& v)
+ {
+ return v.compare (0, n, var, 0, n) == 0 &&
+ (v[n] == '=' || v[n] == '\0');
+ });
+ }
+
+ void environment_vars::
+ add (string var)
+ {
+ iterator i (find (var));
+
+ if (i != end ())
+ *i = move (var);
+ else
+ push_back (move (var));
+ }
+
// redirect
//
redirect::
@@ -751,5 +779,71 @@ namespace build2
{
special_cleanups.emplace_back (move (p));
}
+
+ const environment_vars& environment::
+ exported_variables (environment_vars&)
+ {
+ return exported_vars;
+ }
+
+ const environment_vars& environment::
+ merge_exported_variables (const environment_vars& vars,
+ environment_vars& storage)
+ {
+ const environment_vars& own (exported_variables (storage));
+
+ // If both, the own and the specified variable (un)sets are present,
+ // then merge them. Otherwise, return the own (un)sets, if present, or
+ // the specified (un)sets otherwise.
+ //
+ if (!own.empty () && !vars.empty ())
+ {
+ // Copy the own (un)sets into the storage, if they are not there yet.
+ //
+ if (&storage != &own)
+ storage = own;
+
+ for (const string& v: vars)
+ storage.add (v);
+
+ return storage;
+ }
+ else if (!own.empty ())
+ return own;
+ else
+ return vars;
+ }
+
+ // Helpers.
+ //
+ void
+ verify_environment_var_name (const string& name,
+ const char* opt,
+ const char* prefix,
+ const location& l)
+ {
+ if (name.empty ())
+ fail (l) << prefix << "empty value for option " << opt;
+
+ if (name.find ('=') != string::npos)
+ fail (l) << prefix << "invalid value '" << name << "' for option "
+ << opt << ": contains '='";
+ }
+
+
+ void
+ verify_environment_var_assignment (const string& var,
+ const char* prefix,
+ const location& l)
+ {
+ size_t p (var.find ('='));
+
+ if (p == 0)
+ fail (l) << prefix << "empty variable name";
+
+ if (p == string::npos)
+ fail (l) << prefix << "expected variable assignment instead of '"
+ << var << "'";
+ }
}
}
diff --git a/libbuild2/script/script.hxx b/libbuild2/script/script.hxx
index ecd2c2b..b4cb7fc 100644
--- a/libbuild2/script/script.hxx
+++ b/libbuild2/script/script.hxx
@@ -295,10 +295,25 @@ namespace build2
// command
//
- // Align with butl::process_env, assuming it is not very common to (un)set
- // more than two variables.
+ // Assume it is not very common to (un)set more than a few environment
+ // variables in the script.
//
- using environment_vars = small_vector<string, 2>;
+ struct environment_vars: small_vector<string, 4>
+ {
+ // Find a variable (un)set.
+ //
+ // Note that only the variable name is considered for both arguments. In
+ // other words, passing a variable set as a first argument can result
+ // with a variable unset being found and vice versa.
+ //
+ environment_vars::iterator
+ find (const string&);
+
+ // Add or overwrite an existing variable (un)set.
+ //
+ void
+ add (string);
+ };
struct command
{
@@ -492,6 +507,29 @@ namespace build2
void
clean_special (path);
+ // Command execution environment variables.
+ //
+ public:
+ // Environment variable (un)sets from the export builtin call.
+ //
+ // Each variable in the list can only be present once.
+ //
+ environment_vars exported_vars;
+
+ // Return the environment variable (un)sets which can potentially rely
+ // on factors besides the export builtin call sequence (scoping,
+ // etc). The default implementation returns exported_vars.
+ //
+ virtual const environment_vars&
+ exported_variables (environment_vars& storage);
+
+ // Merge the own environment variable (un)sets with the specified ones,
+ // overriding the former with the latter.
+ //
+ const environment_vars&
+ merge_exported_variables (const environment_vars&,
+ environment_vars& storage);
+
public:
// Set variable value with optional (non-empty) attributes.
//
@@ -528,6 +566,22 @@ namespace build2
virtual
~environment () = default;
};
+
+ // Helpers.
+ //
+ // Issue diagnostics with the specified prefix and fail if the string is
+ // not a valid variable name or assignment (empty, etc).
+ //
+ void
+ verify_environment_var_name (const string&,
+ const char* opt,
+ const char* prefix,
+ const location&);
+
+ void
+ verify_environment_var_assignment (const string&,
+ const char* prefix,
+ const location&);
}
}
diff --git a/libbuild2/test/script/parser+env.test.testscript b/libbuild2/test/script/parser+env.test.testscript
index b6fb305..efa5dec 100644
--- a/libbuild2/test/script/parser+env.test.testscript
+++ b/libbuild2/test/script/parser+env.test.testscript
@@ -31,7 +31,7 @@
: invalid-val
:
$* <'env --unset=a=b -- cmd' 2>>EOE != 0
- testscript:1:5: error: env: invalid value 'a=b' for option '--unset': contains '='
+ testscript:1:5: error: env: invalid value 'a=b' for option --unset: contains '='
EOE
: no-sep
diff --git a/libbuild2/test/script/script.cxx b/libbuild2/test/script/script.cxx
index 165b9b7..3a8ceac 100644
--- a/libbuild2/test/script/script.cxx
+++ b/libbuild2/test/script/script.cxx
@@ -173,6 +173,14 @@ namespace build2
reset_special ();
}
+ const environment_vars& scope::
+ exported_variables (environment_vars& storage)
+ {
+ return parent != nullptr
+ ? parent->merge_exported_variables (exported_vars, storage)
+ : exported_vars;
+ }
+
// script_base
//
script_base::
diff --git a/libbuild2/test/script/script.hxx b/libbuild2/test/script/script.hxx
index ea1f579..7dae78c 100644
--- a/libbuild2/test/script/script.hxx
+++ b/libbuild2/test/script/script.hxx
@@ -30,6 +30,7 @@ namespace build2
using build2::script::command_expr;
using build2::script::expr_term;
using build2::script::command;
+ using build2::script::environment_vars;
using build2::script::deadline;
using build2::script::timeout;
@@ -110,6 +111,12 @@ namespace build2
const string& attrs,
const location&) override;
+ // Merge the command execution environment variable (un)sets from this
+ // and outer scopes.
+ //
+ virtual const environment_vars&
+ exported_variables (environment_vars& storage) override;
+
// Noop since the temporary directory is a working directory and so
// is created before the scope commands execution.
//
diff --git a/tests/recipe/buildscript/testscript b/tests/recipe/buildscript/testscript
index 6bdbd32..14036dd 100644
--- a/tests/recipe/buildscript/testscript
+++ b/tests/recipe/buildscript/testscript
@@ -1,6 +1,8 @@
# file : tests/recipe/buildscript/testscript
# license : MIT; see accompanying LICENSE file
+posix = ($cxx.target.class != 'windows')
+
+mkdir build
+cat <<EOI >=build/bootstrap.build
project = test
@@ -129,6 +131,37 @@
$* clean 2>-
}
+ : export
+ :
+ if $posix
+ {
+ cat <<EOI >=bar;
+ #!/bin/sh
+ echo "$message"
+ EOI
+
+ cat <<EOI >=buildfile;
+ exe{foo}: bar
+ {{
+ cp $path($<) $path($>)
+ }}
+ % test
+ {{
+ diag test $>
+ export message=text1
+ $> >>>?'text1'
+ env message=text2 -- $> >>>?'text2'
+ }}
+ EOI
+
+ $* test 2>>EOE;
+ cp exe{foo}
+ test exe{foo.}
+ EOE
+
+ $* clean 2>-
+ }
+
: depdb
:
{
@@ -442,7 +475,7 @@
: runner
:
- if ($cxx.target.class != 'windows')
+ if $posix
{
echo 'bar' >=bar;
@@ -512,7 +545,7 @@
: timeout
:
-if ($cxx.target.class != 'windows')
+if $posix
{
: update
:
diff --git a/tests/test/script/runner/driver.cxx b/tests/test/script/runner/driver.cxx
index 935541d..f081714 100644
--- a/tests/test/script/runner/driver.cxx
+++ b/tests/test/script/runner/driver.cxx
@@ -66,8 +66,8 @@ main (int argc, char* argv[])
// if required.
//
// -v <name>
- // If the specified variable is set the print its value to stdout and the
- // string '<none>' otherwise.
+ // If the specified variable is set then print its value to stdout and
+ // the string '<none>' otherwise.
//
// -l <sec>
// Sleep the specified number of seconds.
diff --git a/tests/test/script/runner/export.testscript b/tests/test/script/runner/export.testscript
new file mode 100644
index 0000000..f965005
--- /dev/null
+++ b/tests/test/script/runner/export.testscript
@@ -0,0 +1,133 @@
+# file : tests/test/script/runner/export.testscript
+# license : MIT; see accompanying LICENSE file
+
+.include ../common.testscript
+
+: group
+:
+{
+ : add
+ :
+ $c <<EOI && $b
+ {
+ +export foo=bar
+
+ $* -v foo >'bar'
+ }
+ EOI
+
+ : change
+ :
+ $c <<EOI && $b
+ {
+ +export foo=bar
+ +export foo=baz
+
+ $* -v foo >'baz'
+ }
+ EOI
+
+ : remove
+ :
+ $c <<EOI && $b
+ {
+ +export foo=bar
+ +export --unset foo
+
+ $* -v foo >'<none>'
+ }
+ EOI
+
+ : clear
+ :
+ {
+ : added
+ :
+ $c <<EOI && $b
+ {
+ +export foo=bar
+ +export --clear foo
+
+ $* -v foo >'<none>'
+ }
+ EOI
+
+ : removed
+ :
+ $c <<EOI && $b
+ {
+ +export foo=bar
+ +export --unset foo
+ +export --clear foo
+
+ $* -v foo >'<none>'
+ }
+ EOI
+
+ : non-existent
+ :
+ $c <<EOI && $b
+ {
+ +export --clear foo
+
+ $* -v foo >'<none>'
+ }
+ EOI
+ }
+
+ : override
+ :
+ $c <<EOI && $b
+ +export foo=bar
+
+ {
+ +export --unset foo
+
+ export foo=baz;
+ $* -v foo >'baz'
+
+ -$* -v foo >'<none>'
+ }
+
+ -$* -v foo >'bar'
+ EOI
+}
+
+: test
+:
+{
+ : override
+ :
+ $c <<EOI && $b
+ {
+ export foo=bar;
+ env foo=baz -- $* -v foo >'baz';
+ $* -v foo >'bar'
+ }
+ EOI
+}
+
+: invalid
+:
+{
+ : set
+ :
+ $c <'export foo' && $b 2>>~%EOE% != 0
+ testscript:1:1: error: export: expected variable assignment instead of 'foo'
+ %.+
+ EOE
+
+ : unset
+ :
+ $c <'export --unset foo=abc' && $b 2>>~%EOE% != 0
+ testscript:1:1: error: export: invalid value 'foo=abc' for option -u|--unset: contains '='
+ %.+
+ EOE
+
+ : clear
+ :
+ $c <'export --clear foo=abc' && $b 2>>~%EOE% != 0
+ testscript:1:1: error: export: invalid value 'foo=abc' for option -c|--clear: contains '='
+ %.+
+ EOE
+}