From cb558e4bd2b817bc72275c2bbd90dfe9fe380af9 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Tue, 8 Dec 2020 22:40:54 +0300 Subject: Add export script pseudo-builtin --- libbuild2/script/builtin-options.cxx | 297 +++++++++++++++++++++++++++++++++++ libbuild2/script/builtin-options.hxx | 118 +++++++++++++- libbuild2/script/builtin-options.ixx | 111 +++++++++++++ libbuild2/script/builtin.cli | 8 + libbuild2/script/parser.cxx | 24 +-- libbuild2/script/run.cxx | 100 ++++++++++-- libbuild2/script/script.cxx | 94 +++++++++++ libbuild2/script/script.hxx | 60 ++++++- 8 files changed, 773 insertions(+), 39 deletions(-) (limited to 'libbuild2/script') 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 + _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, &export_options::unset_, + &export_options::unset_specified_ >; + _cli_export_options_map_["-u"] = + &::build2::script::cli::thunk< export_options, vector, &export_options::unset_, + &export_options::unset_specified_ >; + _cli_export_options_map_["--clear"] = + &::build2::script::cli::thunk< export_options, vector, &export_options::clear_, + &export_options::clear_specified_ >; + _cli_export_options_map_["-c"] = + &::build2::script::cli::thunk< export_options, vector, &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 (co.c_str ()), + const_cast (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 + 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& + unset () const; + + vector& + unset (); + + void + unset (const vector&); + + bool + unset_specified () const; + + void + unset_specified (bool); + + const vector& + clear () const; + + vector& + clear (); + + void + clear (const vector&); + + 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 unset_; + bool unset_specified_; + vector 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& export_options:: + unset () const + { + return this->unset_; + } + + inline vector& export_options:: + unset () + { + return this->unset_; + } + + inline void export_options:: + unset (const vector& 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& export_options:: + clear () const + { + return this->clear_; + } + + inline vector& export_options:: + clear () + { + return this->clear_; + } + + inline void export_options:: + clear (const vector& 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 ; + // 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 --unset|-u; + vector --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 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 ]... [-u|--unset ]... [=]... + // + 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 [-s|--success] // 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& 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; + struct environment_vars: small_vector + { + // 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&); } } -- cgit v1.1