diff options
-rw-r--r-- | doc/testscript.cli | 46 | ||||
-rw-r--r-- | libbuild2/buildfile | 2 | ||||
-rw-r--r-- | libbuild2/script/builtin-options.cxx | 297 | ||||
-rw-r--r-- | libbuild2/script/builtin-options.hxx | 118 | ||||
-rw-r--r-- | libbuild2/script/builtin-options.ixx | 111 | ||||
-rw-r--r-- | libbuild2/script/builtin.cli | 8 | ||||
-rw-r--r-- | libbuild2/script/parser.cxx | 24 | ||||
-rw-r--r-- | libbuild2/script/run.cxx | 100 | ||||
-rw-r--r-- | libbuild2/script/script.cxx | 94 | ||||
-rw-r--r-- | libbuild2/script/script.hxx | 60 | ||||
-rw-r--r-- | libbuild2/test/script/parser+env.test.testscript | 2 | ||||
-rw-r--r-- | libbuild2/test/script/script.cxx | 8 | ||||
-rw-r--r-- | libbuild2/test/script/script.hxx | 7 | ||||
-rw-r--r-- | tests/recipe/buildscript/testscript | 37 | ||||
-rw-r--r-- | tests/test/script/runner/driver.cxx | 4 | ||||
-rw-r--r-- | tests/test/script/runner/export.testscript | 133 |
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 +} |