diff options
Diffstat (limited to 'libbuild2/test/script/script.cxx')
-rw-r--r-- | libbuild2/test/script/script.cxx | 741 |
1 files changed, 741 insertions, 0 deletions
diff --git a/libbuild2/test/script/script.cxx b/libbuild2/test/script/script.cxx new file mode 100644 index 0000000..b879eb4 --- /dev/null +++ b/libbuild2/test/script/script.cxx @@ -0,0 +1,741 @@ +// file : libbuild2/test/script/script.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <libbuild2/test/script/script.hxx> + +#include <sstream> + +#include <libbuild2/target.hxx> +#include <libbuild2/algorithm.hxx> + +using namespace std; + +namespace build2 +{ + namespace test + { + namespace script + { + ostream& + operator<< (ostream& o, line_type lt) + { + const char* s (nullptr); + + switch (lt) + { + case line_type::var: s = "variable"; break; + case line_type::cmd: s = "command"; break; + case line_type::cmd_if: s = "'if'"; break; + case line_type::cmd_ifn: s = "'if!'"; break; + case line_type::cmd_elif: s = "'elif'"; break; + case line_type::cmd_elifn: s = "'elif!'"; break; + case line_type::cmd_else: s = "'else'"; break; + case line_type::cmd_end: s = "'end'"; break; + } + + return o << s; + } + + // Quote if empty or contains spaces or any of the special characters. + // Note that we use single quotes since double quotes still allow + // expansion. + // + // @@ What if it contains single quotes? + // + static void + to_stream_q (ostream& o, const string& s) + { + if (s.empty () || s.find_first_of (" |&<>=\\\"") != string::npos) + o << '\'' << s << '\''; + else + o << s; + }; + + void + to_stream (ostream& o, const command& c, command_to_stream m) + { + auto print_path = [&o] (const path& p) + { + using build2::operator<<; + + ostringstream s; + stream_verb (s, stream_verb (o)); + s << p; + + to_stream_q (o, s.str ()); + }; + + auto print_redirect = + [&o, print_path] (const redirect& r, const char* prefix) + { + o << ' ' << prefix; + + size_t n (string::traits_type::length (prefix)); + assert (n > 0); + + char d (prefix[n - 1]); // Redirect direction. + + switch (r.type) + { + case redirect_type::none: assert (false); break; + case redirect_type::pass: o << '|'; break; + case redirect_type::null: o << '-'; break; + case redirect_type::trace: o << '!'; break; + case redirect_type::merge: o << '&' << r.fd; break; + + case redirect_type::here_str_literal: + case redirect_type::here_doc_literal: + { + bool doc (r.type == redirect_type::here_doc_literal); + + // For here-document add another '>' or '<'. Note that here end + // marker never needs to be quoted. + // + if (doc) + o << d; + + o << r.modifiers; + + if (doc) + o << r.end; + else + { + const string& v (r.str); + to_stream_q (o, + r.modifiers.find (':') == string::npos + ? string (v, 0, v.size () - 1) // Strip newline. + : v); + } + + break; + } + + case redirect_type::here_str_regex: + case redirect_type::here_doc_regex: + { + bool doc (r.type == redirect_type::here_doc_regex); + + // For here-document add another '>' or '<'. Note that here end + // marker never needs to be quoted. + // + if (doc) + o << d; + + o << r.modifiers; + + const regex_lines& re (r.regex); + + if (doc) + o << re.intro + r.end + re.intro + re.flags; + else + { + assert (!re.lines.empty ()); // Regex can't be empty. + + regex_line l (re.lines[0]); + to_stream_q (o, re.intro + l.value + re.intro + l.flags); + } + + break; + } + + case redirect_type::file: + { + // For stdin or stdout-comparison redirect add '>>' or '<<' (and + // so make it '<<<' or '>>>'). Otherwise add '+' or '=' (and so + // make it '>+' or '>='). + // + if (d == '<' || r.file.mode == redirect_fmode::compare) + o << d << d; + else + o << (r.file.mode == redirect_fmode::append ? '+' : '='); + + print_path (r.file.path); + break; + } + + case redirect_type::here_doc_ref: assert (false); break; + } + }; + + auto print_doc = [&o] (const redirect& r) + { + o << endl; + + if (r.type == redirect_type::here_doc_literal) + o << r.str; + else + { + assert (r.type == redirect_type::here_doc_regex); + + const regex_lines& rl (r.regex); + + for (auto b (rl.lines.cbegin ()), i (b), e (rl.lines.cend ()); + i != e; ++i) + { + if (i != b) + o << endl; + + const regex_line& l (*i); + + if (l.regex) // Regex (possibly empty), + o << rl.intro << l.value << rl.intro << l.flags; + else if (!l.special.empty ()) // Special literal. + o << rl.intro; + else // Textual literal. + o << l.value; + + o << l.special; + } + } + + o << (r.modifiers.find (':') == string::npos ? "" : "\n") << r.end; + }; + + if ((m & command_to_stream::header) == command_to_stream::header) + { + // Program. + // + to_stream_q (o, c.program.string ()); + + // Arguments. + // + for (const string& a: c.arguments) + { + o << ' '; + to_stream_q (o, a); + } + + // Redirects. + // + if (c.in.effective ().type != redirect_type::none) + print_redirect (c.in.effective (), "<"); + + if (c.out.effective ().type != redirect_type::none) + print_redirect (c.out.effective (), ">"); + + if (c.err.effective ().type != redirect_type::none) + print_redirect (c.err.effective (), "2>"); + + for (const auto& p: c.cleanups) + { + o << " &"; + + if (p.type != cleanup_type::always) + o << (p.type == cleanup_type::maybe ? '?' : '!'); + + print_path (p.path); + } + + if (c.exit.comparison != exit_comparison::eq || c.exit.code != 0) + { + switch (c.exit.comparison) + { + case exit_comparison::eq: o << " == "; break; + case exit_comparison::ne: o << " != "; break; + } + + o << static_cast<uint16_t> (c.exit.code); + } + } + + if ((m & command_to_stream::here_doc) == command_to_stream::here_doc) + { + // Here-documents. + // + if (c.in.type == redirect_type::here_doc_literal || + c.in.type == redirect_type::here_doc_regex) + print_doc (c.in); + + if (c.out.type == redirect_type::here_doc_literal || + c.out.type == redirect_type::here_doc_regex) + print_doc (c.out); + + if (c.err.type == redirect_type::here_doc_literal || + c.err.type == redirect_type::here_doc_regex) + print_doc (c.err); + } + } + + void + to_stream (ostream& o, const command_pipe& p, command_to_stream m) + { + if ((m & command_to_stream::header) == command_to_stream::header) + { + for (auto b (p.begin ()), i (b); i != p.end (); ++i) + { + if (i != b) + o << " | "; + + to_stream (o, *i, command_to_stream::header); + } + } + + if ((m & command_to_stream::here_doc) == command_to_stream::here_doc) + { + for (const command& c: p) + to_stream (o, c, command_to_stream::here_doc); + } + } + + void + to_stream (ostream& o, const command_expr& e, command_to_stream m) + { + if ((m & command_to_stream::header) == command_to_stream::header) + { + for (auto b (e.begin ()), i (b); i != e.end (); ++i) + { + if (i != b) + { + switch (i->op) + { + case expr_operator::log_or: o << " || "; break; + case expr_operator::log_and: o << " && "; break; + } + } + + to_stream (o, i->pipe, command_to_stream::header); + } + } + + if ((m & command_to_stream::here_doc) == command_to_stream::here_doc) + { + for (const expr_term& t: e) + to_stream (o, t.pipe, command_to_stream::here_doc); + } + } + + // redirect + // + redirect:: + redirect (redirect_type t) + : type (t) + { + switch (type) + { + case redirect_type::none: + case redirect_type::pass: + case redirect_type::null: + case redirect_type::trace: + case redirect_type::merge: break; + + case redirect_type::here_str_literal: + case redirect_type::here_doc_literal: new (&str) string (); break; + + case redirect_type::here_str_regex: + case redirect_type::here_doc_regex: + { + new (®ex) regex_lines (); + break; + } + + case redirect_type::file: new (&file) file_type (); break; + + case redirect_type::here_doc_ref: assert (false); break; + } + } + + redirect:: + redirect (redirect&& r) + : type (r.type), + modifiers (move (r.modifiers)), + end (move (r.end)), + end_line (r.end_line), + end_column (r.end_column) + { + switch (type) + { + case redirect_type::none: + case redirect_type::pass: + case redirect_type::null: + case redirect_type::trace: break; + + case redirect_type::merge: fd = r.fd; break; + + case redirect_type::here_str_literal: + case redirect_type::here_doc_literal: + { + new (&str) string (move (r.str)); + break; + } + case redirect_type::here_str_regex: + case redirect_type::here_doc_regex: + { + new (®ex) regex_lines (move (r.regex)); + break; + } + case redirect_type::file: + { + new (&file) file_type (move (r.file)); + break; + } + case redirect_type::here_doc_ref: + { + new (&ref) reference_wrapper<const redirect> (r.ref); + break; + } + } + } + + redirect:: + ~redirect () + { + switch (type) + { + case redirect_type::none: + case redirect_type::pass: + case redirect_type::null: + case redirect_type::trace: + case redirect_type::merge: break; + + case redirect_type::here_str_literal: + case redirect_type::here_doc_literal: str.~string (); break; + + case redirect_type::here_str_regex: + case redirect_type::here_doc_regex: regex.~regex_lines (); break; + + case redirect_type::file: file.~file_type (); break; + + case redirect_type::here_doc_ref: + { + ref.~reference_wrapper<const redirect> (); + break; + } + } + } + + redirect& redirect:: + operator= (redirect&& r) + { + if (this != &r) + { + this->~redirect (); + new (this) redirect (move (r)); // Assume noexcept move-constructor. + } + return *this; + } + + // scope + // + scope:: + scope (const string& id, scope* p, script* r) + : parent (p), + root (r), + vars (false /* global */), + id_path (cast<path> (assign (root->id_var) = path ())), + wd_path (cast<dir_path> (assign (root->wd_var) = dir_path ())) + + { + // Construct the id_path as a string to ensure POSIX form. In fact, + // the only reason we keep it as a path is to be able to easily get id + // by calling leaf(). + // + { + string s (p != nullptr ? p->id_path.string () : string ()); + + if (!s.empty () && !id.empty ()) + s += '/'; + + s += id; + const_cast<path&> (id_path) = path (move (s)); + } + + // Calculate the working directory path unless this is the root scope + // (handled in an ad hoc way). + // + if (p != nullptr) + const_cast<dir_path&> (wd_path) = dir_path (p->wd_path) /= id; + } + + void scope:: + clean (cleanup c, bool implicit) + { + using std::find; // Hidden by scope::find(). + + assert (!implicit || c.type == cleanup_type::always); + + const path& p (c.path); + if (!p.sub (root->wd_path)) + { + if (implicit) + return; + else + assert (false); // Error so should have been checked. + } + + auto pr = [&p] (const cleanup& v) -> bool {return v.path == p;}; + auto i (find_if (cleanups.begin (), cleanups.end (), pr)); + + if (i == cleanups.end ()) + cleanups.emplace_back (move (c)); + else if (!implicit) + i->type = c.type; + } + + void scope:: + clean_special (path p) + { + special_cleanups.emplace_back (move (p)); + } + + // script_base + // + script_base:: + script_base () + : // Enter the test.* variables with the same variable types as in + // buildfiles except for test: while in buildfiles it can be a + // target name, in testscripts it should be resolved to a path. + // + // Note: entering in a custom variable pool. + // + test_var (var_pool.insert<path> ("test")), + options_var (var_pool.insert<strings> ("test.options")), + arguments_var (var_pool.insert<strings> ("test.arguments")), + redirects_var (var_pool.insert<strings> ("test.redirects")), + cleanups_var (var_pool.insert<strings> ("test.cleanups")), + + wd_var (var_pool.insert<dir_path> ("~")), + id_var (var_pool.insert<path> ("@")), + cmd_var (var_pool.insert<strings> ("*")), + cmdN_var { + &var_pool.insert<path> ("0"), + &var_pool.insert<string> ("1"), + &var_pool.insert<string> ("2"), + &var_pool.insert<string> ("3"), + &var_pool.insert<string> ("4"), + &var_pool.insert<string> ("5"), + &var_pool.insert<string> ("6"), + &var_pool.insert<string> ("7"), + &var_pool.insert<string> ("8"), + &var_pool.insert<string> ("9")} {} + + // script + // + script:: + script (const target& tt, + const testscript& st, + const dir_path& rwd) + : group (st.name == "testscript" ? string () : st.name, this), + test_target (tt), + target_scope (tt.base_scope ()), + script_target (st) + { + // Set the script working dir ($~) to $out_base/test/<id> (id_path + // for root is just the id which is empty if st is 'testscript'). + // + const_cast<dir_path&> (wd_path) = dir_path (rwd) /= id_path.string (); + + // Set the test variable at the script level. We do it even if it's + // set in the buildfile since they use different types. + // + { + value& v (assign (test_var)); + + // Note that the test variable's visibility is target. + // + lookup l (find_in_buildfile ("test", false)); + + // Note that we have similar code for simple tests. + // + const target* t (nullptr); + + if (l.defined ()) + { + const name* n (cast_null<name> (l)); + + if (n == nullptr) + v = nullptr; + else if (n->empty ()) + v = path (); + else if (n->simple ()) + { + // Ignore the special 'true' value. + // + if (n->value != "true") + v = path (n->value); + else + t = &tt; + } + else if (n->directory ()) + v = path (n->dir); + else + { + // Must be a target name. + // + // @@ OUT: what if this is a @-qualified pair of names? + // + t = search_existing (*n, target_scope); + + if (t == nullptr) + fail << "unknown target '" << *n << "' in test variable"; + } + } + else + // By default we set it to the test target's path. + // + t = &tt; + + // If this is a path-based target, then we use the path. If this + // is an alias target (e.g., dir{}), then we use the directory + // path. Otherwise, we leave it NULL expecting the testscript to + // set it to something appropriate, if used. + // + if (t != nullptr) + { + if (auto* pt = t->is_a<path_target> ()) + { + // Do some sanity checks: the target better be up-to-date with + // an assigned path. + // + v = pt->path (); + + if (v.empty ()) + fail << "target " << *pt << " specified in the test variable " + << "is out of date" << + info << "consider specifying it as a prerequisite of " << tt; + } + else if (t->is_a<alias> ()) + v = path (t->dir); + else if (t != &tt) + fail << "target " << *t << " specified in the test variable " + << "is not path-based"; + } + } + + // Set the special $*, $N variables. + // + reset_special (); + } + + lookup scope:: + find (const variable& var) const + { + // Search script scopes until we hit the root. + // + const scope* s (this); + + do + { + auto p (s->vars.find (var)); + if (p.first != nullptr) + return lookup (*p.first, p.second, s->vars); + } + while ((s->parent != nullptr ? (s = s->parent) : nullptr) != nullptr); + + return find_in_buildfile (var.name); + } + + + lookup scope:: + find_in_buildfile (const string& n, bool target_only) const + { + // Switch to the corresponding buildfile variable. Note that we don't + // want to insert a new variable into the pool (we might be running + // in parallel). Plus, if there is no such variable, then we cannot + // possibly find any value. + // + const variable* pvar (build2::var_pool.find (n)); + + if (pvar == nullptr) + return lookup (); + + const script& s (static_cast<const script&> (*root)); + const variable& var (*pvar); + + // First check the target we are testing. + // + { + // Note that we skip applying the override if we did not find any + // value. In this case, presumably the override also affects the + // script target and we will pick it up there. A bit fuzzy. + // + auto p (s.test_target.find_original (var, target_only)); + + if (p.first) + { + if (var.overrides != nullptr) + p = s.target_scope.find_override (var, move (p), true); + + return p.first; + } + } + + // Then the script target followed by the scopes it is in. Note that + // while unlikely it is possible the test and script targets will be + // in different scopes which brings the question of which scopes we + // should search. + // + return s.script_target[var]; + } + + value& scope:: + append (const variable& var) + { + lookup l (find (var)); + + if (l.defined () && l.belongs (*this)) // Existing var in this scope. + return vars.modify (l); + + value& r (assign (var)); // NULL. + + if (l.defined ()) + r = *l; // Copy value (and type) from the outer scope. + + return r; + } + + void scope:: + reset_special () + { + // First assemble the $* value. + // + strings s; + + auto append = [&s] (const strings& v) + { + s.insert (s.end (), v.begin (), v.end ()); + }; + + if (lookup l = find (root->test_var)) + s.push_back (cast<path> (l).representation ()); + + if (lookup l = find (root->options_var)) + append (cast<strings> (l)); + + if (lookup l = find (root->arguments_var)) + append (cast<strings> (l)); + + // Keep redirects/cleanups out of $N. + // + size_t n (s.size ()); + + if (lookup l = find (root->redirects_var)) + append (cast<strings> (l)); + + if (lookup l = find (root->cleanups_var)) + append (cast<strings> (l)); + + // Set the $N values if present. + // + for (size_t i (0); i <= 9; ++i) + { + value& v (assign (*root->cmdN_var[i])); + + if (i < n) + { + if (i == 0) + v = path (s[i]); + else + v = s[i]; + } + else + v = nullptr; // Clear any old values. + } + + // Set $*. + // + assign (root->cmd_var) = move (s); + } + } + } +} |