From 744e8215261fbf81b9348d115d4916a9c88b52cc Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Tue, 20 Sep 2022 23:00:27 +0300 Subject: Add support for 'while' loop in script --- .../build/script/parser+command-if.test.testscript | 2 +- .../build/script/parser+while.test.testscript | 133 +++++++++++ libbuild2/build/script/parser.cxx | 141 ++++++++--- libbuild2/build/script/parser.hxx | 21 +- libbuild2/build/script/parser.test.cxx | 57 +++-- libbuild2/build/script/runner.cxx | 13 +- libbuild2/build/script/runner.hxx | 26 +- libbuild2/build/script/script.hxx | 3 +- libbuild2/script/parser.cxx | 201 ++++++++++------ libbuild2/script/parser.hxx | 12 +- libbuild2/script/run.cxx | 57 +++-- libbuild2/script/run.hxx | 12 +- libbuild2/script/script.cxx | 21 +- libbuild2/script/script.hxx | 10 + .../test/script/parser+command-if.test.testscript | 6 +- .../test/script/parser+description.test.testscript | 4 +- libbuild2/test/script/parser+while.test.testscript | 265 +++++++++++++++++++++ libbuild2/test/script/parser.cxx | 254 +++++++++++++------- libbuild2/test/script/parser.hxx | 13 +- libbuild2/test/script/parser.test.cxx | 52 +++- libbuild2/test/script/runner.cxx | 14 +- libbuild2/test/script/runner.hxx | 14 +- libbuild2/test/script/script.hxx | 7 +- tests/recipe/buildscript/testscript | 54 +++-- tests/test/script/runner/pipe.testscript | 8 +- tests/test/script/runner/redirect.testscript | 4 +- tests/test/script/runner/while.testscript | 16 ++ 27 files changed, 1102 insertions(+), 318 deletions(-) create mode 100644 libbuild2/build/script/parser+while.test.testscript create mode 100644 libbuild2/test/script/parser+while.test.testscript create mode 100644 tests/test/script/runner/while.testscript diff --git a/libbuild2/build/script/parser+command-if.test.testscript b/libbuild2/build/script/parser+command-if.test.testscript index a18a885..8b19186 100644 --- a/libbuild2/build/script/parser+command-if.test.testscript +++ b/libbuild2/build/script/parser+command-if.test.testscript @@ -279,7 +279,7 @@ cmd end EOI - buildfile:12:1: error: 'end' without preceding 'if' + buildfile:12:1: error: 'end' without preceding 'if', 'for', or 'while' EOE : before diff --git a/libbuild2/build/script/parser+while.test.testscript b/libbuild2/build/script/parser+while.test.testscript new file mode 100644 index 0000000..5587291 --- /dev/null +++ b/libbuild2/build/script/parser+while.test.testscript @@ -0,0 +1,133 @@ +# file : libbuild2/build/script/parser+while.test.testscript +# license : MIT; see accompanying LICENSE file + +: while +: +{ + : true + : + $* <>EOO + while ($v != "aa") + cmd "$v" + v = "$(v)a" + end + EOI + ? true + cmd '' + ? true + cmd a + ? false + EOO + + : false + : + $* <>EOO + while ($v == "aa") + cmd "$v" + v = "$(v)a" + end + EOI + ? false + EOO + + : without-command + : + $* <>EOE != 0 + while + cmd + end + EOI + buildfile:11:6: error: missing program + EOE +} + +: end +: +{ + : without-end + : + $* <>EOE != 0 + while true + cmd + EOI + buildfile:13:1: error: expected closing 'end' + EOE +} + +: elif +: +{ + : without-if + : + $* <>EOE != 0 + while false + elif true + cmd + end + end + EOI + buildfile:12:3: error: 'elif' without preceding 'if' + EOE +} + +: nested +: +{ + $* -l -r <>EOO + while ($v != "aa") # 1 + cmd1 "$v" # 2 + if ($v == "a") # 3 + cmd2 # 4 + while ($v2 != "$v") # 5 + cmd3 # 6 + v2=$v + end + else + cmd4 # 7 + end + cmd5 # 8 + v = "$(v)a" + end + EOI + ? true # 1 i1 + cmd1 '' # 2 i1 + ? false # 3 i1 + cmd4 # 7 i1 + cmd5 # 8 i1 + ? true # 1 i2 + cmd1 a # 2 i2 + ? true # 3 i2 + cmd2 # 4 i2 + ? true # 5 i2 i1 + cmd3 # 6 i2 i1 + ? false # 5 i2 i2 + cmd5 # 8 i2 + ? false # 1 i3 + EOO +} + +: contained +: +{ + : eos + : + $* <>EOE != 0 + while + EOI + buildfile:12:1: error: expected closing 'end' + EOE +} + +: var +: +$* <>EOO +while ($v1 != "a") + v1 = "$(v1)a" + v2 = "$v1" +end +cmd $v1 +EOI +? true +? false +cmd a +EOO diff --git a/libbuild2/build/script/parser.cxx b/libbuild2/build/script/parser.cxx index 0614f20..1a7f4e1 100644 --- a/libbuild2/build/script/parser.cxx +++ b/libbuild2/build/script/parser.cxx @@ -193,9 +193,22 @@ namespace build2 } } + // Parse a logical line, handling the flow control constructs + // recursively. + // + // If the flow control construct type is specified, then this line is + // assumed to belong to such a construct. + // void parser:: - pre_parse_line (token& t, type& tt, bool if_line) + pre_parse_line (token& t, type& tt, optional fct) { + // enter: next token is peeked at (type in tt) + // leave: newline + + assert (!fct || + *fct == line_type::cmd_if || + *fct == line_type::cmd_while); + // Determine the line type/start token. // line_type lt ( @@ -235,19 +248,25 @@ namespace build2 case line_type::cmd_elif: case line_type::cmd_elifn: case line_type::cmd_else: - case line_type::cmd_end: { - if (!if_line) - { + if (!fct || *fct != line_type::cmd_if) fail (t) << lt << " without preceding 'if'"; - } + } + // Fall through. + case line_type::cmd_end: + { + if (!fct) + fail (t) << lt << " without preceding 'if', 'for', or 'while'"; } // Fall through. case line_type::cmd_if: case line_type::cmd_ifn: + case line_type::cmd_while: next (t, tt); // Skip to start of command. - if (lt == line_type::cmd_if || lt == line_type::cmd_ifn) + if (lt == line_type::cmd_if || + lt == line_type::cmd_ifn || + lt == line_type::cmd_while) ++level_; else if (lt == line_type::cmd_end) --level_; @@ -287,6 +306,50 @@ namespace build2 pre_parse_if_else (t, tt); } + else if (lt == line_type::cmd_while) + { + tt = peek (lexer_mode::first_token); + + pre_parse_while (t, tt); + } + } + + // Pre-parse the flow control construct block line. + // + void parser:: + pre_parse_block_line (token& t, type& tt, line_type bt) + { + // enter: peeked first token of the line (type in tt) + // leave: newline + + const location ll (get_location (peeked ())); + + if (tt == type::eos) + fail (ll) << "expected closing 'end'"; + + line_type fct; // Flow control type the block type relates to. + + switch (bt) + { + case line_type::cmd_if: + case line_type::cmd_ifn: + case line_type::cmd_elif: + case line_type::cmd_elifn: + case line_type::cmd_else: + { + fct = line_type::cmd_if; + break; + } + case line_type::cmd_while: + { + fct = line_type::cmd_while; + break; + } + default: assert(false); + } + + pre_parse_line (t, tt, fct); + assert (tt == type::newline); } void parser:: @@ -295,8 +358,7 @@ namespace build2 // enter: peeked first token of next line (type in tt) // leave: newline - // Parse lines until we see closing 'end'. Nested if-else blocks are - // handled recursively. + // Parse lines until we see closing 'end'. // for (line_type bt (line_type::cmd_if); // Current block. ; @@ -304,25 +366,21 @@ namespace build2 { const location ll (get_location (peeked ())); - if (tt == type::eos) - fail (ll) << "expected closing 'end'"; - // Parse one line. Note that this one line can still be multiple - // lines in case of if-else. In this case we want to view it as - // cmd_if, not cmd_end. Thus remember the start position of the - // next logical line. + // lines in case of a flow control construct. In this case we want + // to view it as cmd_if, not cmd_end. Thus remember the start + // position of the next logical line. // size_t i (script_->body.size ()); - pre_parse_line (t, tt, true /* if_line */); - assert (tt == type::newline); + pre_parse_block_line (t, tt, bt); line_type lt (script_->body[i].type); // First take care of 'end'. // if (lt == line_type::cmd_end) - return; + break; // Check if-else block sequencing. // @@ -346,6 +404,25 @@ namespace build2 } } + void parser:: + pre_parse_while (token& t, type& tt) + { + // enter: peeked first token of next line (type in tt) + // leave: newline + + // Parse lines until we see closing 'end'. + // + for (;; tt = peek (lexer_mode::first_token)) + { + size_t i (script_->body.size ()); + + pre_parse_block_line (t, tt, line_type::cmd_while); + + if (script_->body[i].type == line_type::cmd_end) + break; + } + } + command_expr parser:: parse_command_line (token& t, type& tt) { @@ -946,7 +1023,7 @@ namespace build2 // Note that we rely on "small function object" optimization here. // auto exec_cmd = [this] (token& t, build2::script::token_type& tt, - size_t li, + const iteration_index* ii, size_t li, bool single, const location& ll) { @@ -958,7 +1035,7 @@ namespace build2 command_expr ce ( parse_command_line (t, static_cast (tt))); - runner_->run (*environment_, ce, li, ll); + runner_->run (*environment_, ce, ii, li, ll); }; exec_lines (s.body, exec_cmd); @@ -1009,13 +1086,13 @@ namespace build2 auto exec_cmd = [this, &data] (token& t, build2::script::token_type& tt, - size_t li, + const iteration_index* ii, size_t li, bool /* single */, const location& ll) { // Note that we never reset the line index to zero (as we do in - // execute_body()) assuming that there are some script body - // commands to follow. + // execute_body()) assuming that there are some script body commands + // to follow. // if (tt == type::word && t.value == "depdb") { @@ -1128,7 +1205,7 @@ namespace build2 info (rt[0].location ()) << "depdb preamble ends here"; } - runner_->run (*environment_, ce, li, ll); + runner_->run (*environment_, ce, ii, li, ll); } }; @@ -1191,20 +1268,22 @@ namespace build2 apply_value_attributes (&var, lhs, move (rhs), kind); }; - auto exec_if = [this] (token& t, build2::script::token_type& tt, - size_t li, - const location& ll) + auto exec_cond = [this] (token& t, build2::script::token_type& tt, + const iteration_index* ii, size_t li, + const location& ll) { command_expr ce ( parse_command_line (t, static_cast (tt))); - // Assume if-else always involves multiple commands. + // Assume a flow control construct always involves multiple + // commands. // - return runner_->run_if (*environment_, ce, li, ll); + return runner_->run_cond (*environment_, ce, ii, li, ll); }; build2::script::parser::exec_lines (begin, end, - exec_set, exec_cmd, exec_if, + exec_set, exec_cmd, exec_cond, + nullptr /* iteration_index */, environment_->exec_line, &environment_->var_pool); } @@ -2145,10 +2224,12 @@ namespace build2 istringstream iss; if (prog) { + // Note: depdb is disallowed inside flow control constructs. + // string s; build2::script::run (*environment_, cmd, - li, + nullptr /* iteration_index */, li, ll, !file ? &s : nullptr); diff --git a/libbuild2/build/script/parser.hxx b/libbuild2/build/script/parser.hxx index 932cbad..987ed1f 100644 --- a/libbuild2/build/script/parser.hxx +++ b/libbuild2/build/script/parser.hxx @@ -65,11 +65,18 @@ namespace build2 pre_parse_script (); void - pre_parse_line (token&, token_type&, bool if_line = false); + pre_parse_line (token&, token_type&, + optional flow_control_type = nullopt); + + void + pre_parse_block_line (token&, token_type&, line_type block_type); void pre_parse_if_else (token&, token_type&); + void + pre_parse_while (token&, token_type&); + command_expr parse_command_line (token&, token_type&); @@ -367,13 +374,13 @@ namespace build2 // line* save_line_; - // The if-else nesting level (and in the future for other flow - // control constructs). + // The flow control constructs nesting level. // - // Maintained during pre-parsing and is incremented when the cmd_if or - // cmd_ifn lines are encountered, which in particular means that it is - // already incremented by the time the if-condition expression is - // pre-parsed. Decremented when the cmd_end line is encountered. + // Maintained during pre-parsing and is incremented when flow control + // construct condition lines are encountered, which in particular + // means that it is already incremented by the time the condition + // expression is pre-parsed. Decremented when the cmd_end line is + // encountered. // size_t level_ = 0; diff --git a/libbuild2/build/script/parser.test.cxx b/libbuild2/build/script/parser.test.cxx index 5808015..f8c2696 100644 --- a/libbuild2/build/script/parser.test.cxx +++ b/libbuild2/build/script/parser.test.cxx @@ -29,7 +29,9 @@ namespace build2 class print_runner: public runner { public: - print_runner (bool line): line_ (line) {} + print_runner (bool line, bool iterations): + line_ (line), + iterations_ (iterations) {} virtual void enter (environment&, const location&) override {} @@ -37,27 +39,27 @@ namespace build2 virtual void run (environment&, const command_expr& e, - size_t i, + const iteration_index* ii, size_t i, const location&) override { cout << e; - if (line_) - cout << " # " << i; + if (line_ || iterations_) + print_line_info (ii, i); cout << endl; } virtual bool - run_if (environment&, - const command_expr& e, - size_t i, - const location&) override + run_cond (environment&, + const command_expr& e, + const iteration_index* ii, size_t i, + const location&) override { cout << "? " << e; - if (line_) - cout << " # " << i; + if (line_ || iterations_) + print_line_info (ii, i); cout << endl; @@ -68,12 +70,32 @@ namespace build2 leave (environment&, const location&) override {} private: + void + print_line_info (const iteration_index* ii, size_t i) const + { + cout << " #"; + + if (line_) + cout << ' ' << i; + + if (iterations_ && ii != nullptr) + { + string s; + for (const iteration_index* i (ii); i != nullptr; i = i->prev) + s.insert (0, " i" + to_string (i->index)); + + cout << s; + } + } + + private: bool line_; + bool iterations_; }; // Usages: // - // argv[0] [-l] + // argv[0] [-l] [-r] // argv[0] -b [-t] // argv[0] -d [-t] // argv[0] -q @@ -99,6 +121,9 @@ namespace build2 // -l // Print the script line number for each executed expression. // + // -r + // Print the loop iteration numbers for each executed expression. + // // -b // Dump the parsed script body to stdout. // @@ -136,6 +161,7 @@ namespace build2 } m (mode::run); bool print_line (false); + bool print_iterations (false); optional diag_name; bool temp_dir (false); @@ -145,6 +171,8 @@ namespace build2 if (a == "-l") print_line = true; + else if (a == "-r") + print_iterations = true; else if (a == "-b") m = mode::body; else if (a == "-d") @@ -170,8 +198,9 @@ namespace build2 } } - assert (!print_line || m == mode::run); - assert (!diag_name || m == mode::diag); + assert (!print_line || m == mode::run); + assert (!print_iterations || m == mode::run); + assert (!diag_name || m == mode::diag); // Fake build system driver, default verbosity. // @@ -223,7 +252,7 @@ namespace build2 case mode::run: { environment e (perform_update_id, tt, s.body_temp_dir); - print_runner r (print_line); + print_runner r (print_line, print_iterations); p.execute_body (ctx.global_scope, ctx.global_scope, e, s, r); break; } diff --git a/libbuild2/build/script/runner.cxx b/libbuild2/build/script/runner.cxx index 51139d4..157fc60 100644 --- a/libbuild2/build/script/runner.cxx +++ b/libbuild2/build/script/runner.cxx @@ -96,7 +96,7 @@ namespace build2 void default_runner:: run (environment& env, const command_expr& expr, - size_t li, + const iteration_index* ii, size_t li, const location& ll) { if (verb >= 3) @@ -115,20 +115,21 @@ namespace build2 (p.recall.string () == "set" || p.recall.string () == "exit"); }) != expr.end ()) - build2::script::run (env, expr, li, ll); + build2::script::run (env, expr, ii, li, ll); else if (verb >= 2) text << expr; } bool default_runner:: - run_if (environment& env, - const command_expr& expr, - size_t li, const location& ll) + run_cond (environment& env, + const command_expr& expr, + const iteration_index* ii, size_t li, + const location& ll) { if (verb >= 3) text << ": ?" << expr; - return build2::script::run_if (env, expr, li, ll); + return build2::script::run_cond (env, expr, ii, li, ll); } } } diff --git a/libbuild2/build/script/runner.hxx b/libbuild2/build/script/runner.hxx index 558de9b..0652396 100644 --- a/libbuild2/build/script/runner.hxx +++ b/libbuild2/build/script/runner.hxx @@ -35,14 +35,14 @@ namespace build2 virtual void run (environment&, const command_expr&, - size_t index, + const iteration_index*, size_t index, const location&) = 0; virtual bool - run_if (environment&, - const command_expr&, - size_t, - const location&) = 0; + run_cond (environment&, + const command_expr&, + const iteration_index*, size_t, + const location&) = 0; // Location is the script end location (for diagnostics, etc). // @@ -52,9 +52,9 @@ namespace build2 // Run command expressions. // - // In dry-run mode don't run the expressions unless they are if- - // conditions or execute the set or exit builtins, but print them at - // verbosity level 2 and up. + // In dry-run mode don't run the expressions unless they are flow + // control construct conditions or execute the set or exit builtins, but + // print them at verbosity level 2 and up. // class default_runner: public runner { @@ -65,14 +65,14 @@ namespace build2 virtual void run (environment&, const command_expr&, - size_t, + const iteration_index*, size_t, const location&) override; virtual bool - run_if (environment&, - const command_expr&, - size_t, - const location&) override; + run_cond (environment&, + const command_expr&, + const iteration_index*, size_t, + const location&) override; virtual void leave (environment&, const location&) override; diff --git a/libbuild2/build/script/script.hxx b/libbuild2/build/script/script.hxx index d0ab139..f8df204 100644 --- a/libbuild2/build/script/script.hxx +++ b/libbuild2/build/script/script.hxx @@ -20,12 +20,13 @@ namespace build2 namespace script { using build2::script::line; - using build2::script::lines; using build2::script::line_type; + using build2::script::lines; using build2::script::redirect; using build2::script::redirect_type; using build2::script::expr_term; using build2::script::command_expr; + using build2::script::iteration_index; using build2::script::deadline; using build2::script::timeout; diff --git a/libbuild2/script/parser.cxx b/libbuild2/script/parser.cxx index c199c0e..c371e5b 100644 --- a/libbuild2/script/parser.cxx +++ b/libbuild2/script/parser.cxx @@ -2061,6 +2061,7 @@ namespace build2 else if (n == "elif") r = line_type::cmd_elif; else if (n == "elif!") r = line_type::cmd_elifn; else if (n == "else") r = line_type::cmd_else; + else if (n == "while") r = line_type::cmd_while; else if (n == "end") r = line_type::cmd_end; else { @@ -2091,8 +2092,8 @@ namespace build2 exec_lines (lines::const_iterator i, lines::const_iterator e, const function& exec_set, const function& exec_cmd, - const function& exec_if, - size_t& li, + const function& exec_cond, + const iteration_index* ii, size_t& li, variable_pool* var_pool) { try @@ -2116,6 +2117,69 @@ namespace build2 next (t, tt); const location ll (get_location (t)); + // If end is true, then find the flow control construct's end ('end' + // line). Otherwise, find the flow control construct's block end + // ('end', 'else', etc). If skip is true then increment the command + // line index. + // + auto fcend = [e, &li] (lines::const_iterator j, + bool end, + bool skip) -> lines::const_iterator + { + // We need to be aware of nested flow control constructs. + // + size_t n (0); + + for (++j; j != e; ++j) + { + line_type lt (j->type); + + if (lt == line_type::cmd_if || + lt == line_type::cmd_ifn || + lt == line_type::cmd_while) + ++n; + + // If we are nested then we just wait until we get back + // to the surface. + // + if (n == 0) + { + switch (lt) + { + case line_type::cmd_elif: + case line_type::cmd_elifn: + case line_type::cmd_else: + if (end) break; + // Fall through. + case line_type::cmd_end: return j; + default: break; + } + } + + if (lt == line_type::cmd_end) + --n; + + if (skip) + { + // Note that we don't count else and end as commands. + // + switch (lt) + { + case line_type::cmd: + case line_type::cmd_if: + case line_type::cmd_ifn: + case line_type::cmd_elif: + case line_type::cmd_elifn: + case line_type::cmd_while: ++li; break; + default: break; + } + } + } + + assert (false); // Missing end. + return e; + }; + switch (lt) { case line_type::var: @@ -2151,7 +2215,7 @@ namespace build2 single = true; } - exec_cmd (t, tt, li++, single, ll); + exec_cmd (t, tt, ii, li++, single, ll); replay_stop (); break; @@ -2167,7 +2231,7 @@ namespace build2 bool take; if (lt != line_type::cmd_else) { - take = exec_if (t, tt, li++, ll); + take = exec_cond (t, tt, ii, li++, ll); if (lt == line_type::cmd_ifn || lt == line_type::cmd_elifn) take = !take; @@ -2180,94 +2244,89 @@ namespace build2 replay_stop (); - // If end is true, then find the 'end' line. Otherwise, find - // the next if-else line. If skip is true then increment the - // command line index. - // - auto next = [e, &li] (lines::const_iterator j, - bool end, - bool skip) -> lines::const_iterator - { - // We need to be aware of nested if-else chains. - // - size_t n (0); - - for (++j; j != e; ++j) - { - line_type lt (j->type); - - if (lt == line_type::cmd_if || lt == line_type::cmd_ifn) - ++n; - - // If we are nested then we just wait until we get back - // to the surface. - // - if (n == 0) - { - switch (lt) - { - case line_type::cmd_elif: - case line_type::cmd_elifn: - case line_type::cmd_else: - if (end) break; - // Fall through. - case line_type::cmd_end: return j; - default: break; - } - } - - if (lt == line_type::cmd_end) - --n; - - if (skip) - { - // Note that we don't count else and end as commands. - // - switch (lt) - { - case line_type::cmd: - case line_type::cmd_if: - case line_type::cmd_ifn: - case line_type::cmd_elif: - case line_type::cmd_elifn: ++li; break; - default: break; - } - } - } - - assert (false); // Missing end. - return e; - }; - // If we are taking this branch then we need to parse all the - // lines until the next if-else line and then skip all the - // lines until the end (unless next is already end). + // lines until the next if-else line and then skip all the lines + // until the end (unless we are already at the end). // // Otherwise, we need to skip all the lines until the next // if-else line and then continue parsing. // if (take) { - // Next if-else. + // Find block end. // - lines::const_iterator j (next (i, false, false)); + lines::const_iterator j (fcend (i, false, false)); + if (!exec_lines (i + 1, j, - exec_set, exec_cmd, exec_if, - li, + exec_set, exec_cmd, exec_cond, + ii, li, var_pool)) return false; - i = j->type == line_type::cmd_end ? j : next (j, true, true); + // Find construct end. + // + i = j->type == line_type::cmd_end ? j : fcend (j, true, true); } else { - i = next (i, false, true); + // Find block end. + // + i = fcend (i, false, true); + if (i->type != line_type::cmd_end) --i; // Continue with this line (e.g., elif or else). } break; } + case line_type::cmd_while: + { + size_t wli (li); + + for (iteration_index wi {1, ii};; wi.index++) + { + next (t, tt); // Skip to start of command. + + bool exec (exec_cond (t, tt, &wi, li++, ll)); + + replay_stop (); + + // If the condition evaluates to true, then we need to parse + // all the lines until the end line, prepare for the condition + // reevaluation, and re-iterate. + // + // Otherwise, we need to skip all the lines until the end + // line, bail out from the loop, and continue parsing. + // + if (exec) + { + // Find construct end. + // + lines::const_iterator j (fcend (i, true, false)); + + if (!exec_lines (i + 1, j, + exec_set, exec_cmd, exec_cond, + &wi, li, + var_pool)) + return false; + + // Prepare for the condition reevaluation. + // + replay_data (replay_tokens (ln.tokens)); + next (t, tt); + li = wli; + } + else + { + // Find construct end. + // + i = fcend (i, true, true); + break; // Bail out from the while-loop. + } + } + + break; + } case line_type::cmd_end: { assert (false); diff --git a/libbuild2/script/parser.hxx b/libbuild2/script/parser.hxx index d8e5dbf..9edb6ca 100644 --- a/libbuild2/script/parser.hxx +++ b/libbuild2/script/parser.hxx @@ -166,13 +166,13 @@ namespace build2 const location&); using exec_cmd_function = void (token&, token_type&, - size_t li, + const iteration_index*, size_t li, bool single, const location&); - using exec_if_function = bool (token&, token_type&, - size_t li, - const location&); + using exec_cond_function = bool (token&, token_type&, + const iteration_index*, size_t li, + const location&); // If a parser implementation doesn't pre-enter variables into a pool // during the pre-parsing phase, then they are entered during the @@ -183,8 +183,8 @@ namespace build2 exec_lines (lines::const_iterator b, lines::const_iterator e, const function&, const function&, - const function&, - size_t& li, + const function&, + const iteration_index*, size_t& li, variable_pool* = nullptr); // Customization hooks. diff --git a/libbuild2/script/run.cxx b/libbuild2/script/run.cxx index 5b45afd..51a1f92 100644 --- a/libbuild2/script/run.cxx +++ b/libbuild2/script/run.cxx @@ -809,7 +809,7 @@ namespace build2 // regex to file for troubleshooting regardless of whether we print // the diagnostics or not. We, however, register it for cleanup in the // later case (the expression may still succeed, we can be evaluating - // the if condition, etc). + // the flow control construct condition, etc). // optional rp; if (env.temp_dir_keep) @@ -1239,7 +1239,8 @@ namespace build2 command_pipe::const_iterator bc, command_pipe::const_iterator ec, auto_fd ifd, - size_t ci, size_t li, const location& ll, + const iteration_index* ii, size_t li, size_t ci, + const location& ll, bool diag, string* output, optional dl = nullopt, @@ -1444,19 +1445,28 @@ namespace build2 // Create a unique path for a command standard stream cache file. // - auto std_path = [&env, &ci, &li, &ll] (const char* n) -> path + auto std_path = [&env, ii, &li, &ci, &ll] (const char* nm) -> path { using std::to_string; - path p (n); + string s (nm); + size_t n (s.size ()); + + if (ii != nullptr) + { + // Note: reverse order (outermost to innermost). + // + for (const iteration_index* i (ii); i != nullptr; i = i->prev) + s.insert (n, "-i" + to_string (i->index)); + } // 0 if belongs to a single-line script, otherwise is the command line // number (start from one) in the script. // - if (li > 0) + if (li != 0) { - p += '-'; - p += to_string (li); + s += "-n"; + s += to_string (li); } // 0 if belongs to a single-command expression, otherwise is the @@ -1466,13 +1476,13 @@ namespace build2 // single-line script or to N-th single-command line of multi-line // script. These cases are mutually exclusive and so are unambiguous. // - if (ci > 0) + if (ci != 0) { - p += '-'; - p += to_string (ci); + s += "-c"; + s += to_string (ci); } - return normalize (move (p), temp_dir (env), ll); + return normalize (path (move (s)), temp_dir (env), ll); }; // If this is the first pipeline command, then open stdin descriptor @@ -2206,7 +2216,7 @@ namespace build2 success = run_pipe (env, nc, ec, move (ofd.in), - ci + 1, li, ll, diag, + ii, li, ci + 1, ll, diag, output, dl, dl_cmd, &pc); @@ -2333,7 +2343,7 @@ namespace build2 success = run_pipe (env, nc, ec, move (ofd.in), - ci + 1, li, ll, diag, + ii, li, ci + 1, ll, diag, output, dl, dl_cmd, &pc); @@ -2471,7 +2481,8 @@ namespace build2 static bool run_expr (environment& env, const command_expr& expr, - size_t li, const location& ll, + const iteration_index* ii, size_t li, + const location& ll, bool diag, string* output) { @@ -2517,7 +2528,7 @@ namespace build2 r = run_pipe (env, p.begin (), p.end (), auto_fd (), - ci, li, ll, print, + ii, li, ci, ll, print, output); } @@ -2530,26 +2541,28 @@ namespace build2 void run (environment& env, const command_expr& expr, - size_t li, const location& ll, + const iteration_index* ii, size_t li, + const location& ll, string* output) { // Note that we don't print the expression at any verbosity level // assuming that the caller does this, potentially providing some // additional information (command type, etc). // - if (!run_expr (env, expr, li, ll, true /* diag */, output)) + if (!run_expr (env, expr, ii, li, ll, true /* diag */, output)) throw failed (); // Assume diagnostics is already printed. } bool - run_if (environment& env, - const command_expr& expr, - size_t li, const location& ll, - string* output) + run_cond (environment& env, + const command_expr& expr, + const iteration_index* ii, size_t li, + const location& ll, + string* output) { // Note that we don't print the expression here (see above). // - return run_expr (env, expr, li, ll, false /* diag */, output); + return run_expr (env, expr, ii, li, ll, false /* diag */, output); } void diff --git a/libbuild2/script/run.hxx b/libbuild2/script/run.hxx index 8bc246c..01b010c 100644 --- a/libbuild2/script/run.hxx +++ b/libbuild2/script/run.hxx @@ -44,16 +44,16 @@ namespace build2 void run (environment&, const command_expr&, - size_t index, + const iteration_index*, size_t index, const location&, string* output = nullptr); bool - run_if (environment&, - const command_expr&, - size_t index, - const location&, - string* output = nullptr); + run_cond (environment&, + const command_expr&, + const iteration_index*, size_t index, + const location&, + string* output = nullptr); // Perform the registered special file cleanups in the direct order and // then the regular cleanups in the reverse order. diff --git a/libbuild2/script/script.cxx b/libbuild2/script/script.cxx index 9e6eeed..d4096cf 100644 --- a/libbuild2/script/script.cxx +++ b/libbuild2/script/script.cxx @@ -27,6 +27,7 @@ namespace build2 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_while: s = "'while'"; break; case line_type::cmd_end: s = "'end'"; break; } @@ -186,14 +187,14 @@ namespace build2 void dump (ostream& os, const string& ind, const lines& ls) { - // Additionally indent the if-branch lines. + // Additionally indent the flow control construct block lines. // - string if_ind; + string fc_ind; for (const line& l: ls) { - // Before printing indentation, decrease it if the else or end line is - // reached. + // Before printing indentation, decrease it if the else, end, etc line + // is reached. // switch (l.type) { @@ -202,9 +203,9 @@ namespace build2 case line_type::cmd_else: case line_type::cmd_end: { - size_t n (if_ind.size ()); + size_t n (fc_ind.size ()); assert (n >= 2); - if_ind.resize (n - 2); + fc_ind.resize (n - 2); break; } default: break; @@ -212,9 +213,10 @@ namespace build2 // Print indentations. // - os << ind << if_ind; + os << ind << fc_ind; - // After printing indentation, increase it for if/else branch. + // After printing indentation, increase it for the flow control + // construct block lines. // switch (l.type) { @@ -222,7 +224,8 @@ namespace build2 case line_type::cmd_ifn: case line_type::cmd_elif: case line_type::cmd_elifn: - case line_type::cmd_else: if_ind += " "; break; + case line_type::cmd_else: + case line_type::cmd_while: fc_ind += " "; break; default: break; } diff --git a/libbuild2/script/script.hxx b/libbuild2/script/script.hxx index 5a39659..d6018f0 100644 --- a/libbuild2/script/script.hxx +++ b/libbuild2/script/script.hxx @@ -27,6 +27,7 @@ namespace build2 cmd_elif, cmd_elifn, cmd_else, + cmd_while, cmd_end }; @@ -380,6 +381,15 @@ namespace build2 ostream& operator<< (ostream&, const command_expr&); + // Stack-allocated linked list of iteration indexes of the nested loops. + // + struct iteration_index + { + size_t index; // 1-based. + + const iteration_index* prev; // NULL for the top-most loop. + }; + struct timeout { duration value; diff --git a/libbuild2/test/script/parser+command-if.test.testscript b/libbuild2/test/script/parser+command-if.test.testscript index 0b72b4a..9e223dd 100644 --- a/libbuild2/test/script/parser+command-if.test.testscript +++ b/libbuild2/test/script/parser+command-if.test.testscript @@ -315,6 +315,7 @@ } : end +: { : without-if : @@ -322,7 +323,7 @@ cmd end EOI - testscript:2:1: error: 'end' without preceding 'if' + testscript:2:1: error: 'end' without preceding 'if', 'for', or 'while' EOE : without-if-semi @@ -331,10 +332,11 @@ cmd; end EOI - testscript:2:1: error: 'end' without preceding 'if' + testscript:2:1: error: 'end' without preceding 'if', 'for', or 'while' EOE : before + : { : semi : diff --git a/libbuild2/test/script/parser+description.test.testscript b/libbuild2/test/script/parser+description.test.testscript index cee540f..f656b7d 100644 --- a/libbuild2/test/script/parser+description.test.testscript +++ b/libbuild2/test/script/parser+description.test.testscript @@ -313,7 +313,7 @@ x = y end EOI - testscript:2:1: error: description before/after setup/teardown variable-if + testscript:2:1: error: description before/after setup/teardown variable-only 'if' EOE : var-if-after @@ -323,7 +323,7 @@ x = y end : foo EOI - testscript:1:1: error: description before/after setup/teardown variable-if + testscript:1:1: error: description before/after setup/teardown variable-only 'if' EOE : test diff --git a/libbuild2/test/script/parser+while.test.testscript b/libbuild2/test/script/parser+while.test.testscript new file mode 100644 index 0000000..b1a2b44 --- /dev/null +++ b/libbuild2/test/script/parser+while.test.testscript @@ -0,0 +1,265 @@ +# file : libbuild2/test/script/parser+while.test.testscript +# license : MIT; see accompanying LICENSE file + +: while +: +{ + : true + : + $* <>EOO + while ($v != "aa") + cmd "$v" + v = "$(v)a" + end + EOI + ? true + cmd '' + ? true + cmd a + ? false + EOO + + : false + : + $* <>EOO + while ($v == "aa") + cmd "$v" + v = "$(v)a" + end + EOI + ? false + EOO + + : without-command + : + $* <>EOE != 0 + while + cmd + end + EOI + testscript:1:6: error: missing program + EOE + + : after-semi + : + $* -s <>EOO + cmd1; + while ($v != "aa") + cmd2 "$v" + v = "$(v)a" + end + EOI + { + { + cmd1 + ? true + cmd2 '' + ? true + cmd2 a + ? false + } + } + EOO + + : setup + : + $* -s <>EOO + +while ($v != "aa") + cmd2 "$v" + v = "$(v)a" + end + EOI + { + ? true + +cmd2 '' + ? true + +cmd2 a + ? false + } + EOO + + : tdown + : + $* -s <>EOO + -while ($v != "aa") + cmd2 "$v" + v = "$(v)a" + end + EOI + { + ? true + -cmd2 '' + ? true + -cmd2 a + ? false + } + EOO +} + +: end +: +{ + : without-end + : + $* <>EOE != 0 + while true + cmd + EOI + testscript:3:1: error: expected closing 'end' + EOE +} + +: elif +: +{ + : without-if + : + $* <>EOE != 0 + while false + elif true + cmd + end + end + EOI + testscript:2:3: error: 'elif' without preceding 'if' + EOE +} + +: nested +: +{ + $* -l -r <>EOO + while ($v != "aa") # 1 + cmd1 "$v" # 2 + if ($v == "a") # 3 + cmd2 # 4 + while ($v2 != "$v") # 5 + cmd3 # 6 + v2=$v + end + else + cmd4 # 7 + end + cmd5 # 8 + v = "$(v)a" + end; + cmd6 + EOI + ? true # 1 i1 + cmd1 '' # 2 i1 + ? false # 3 i1 + cmd4 # 7 i1 + cmd5 # 8 i1 + ? true # 1 i2 + cmd1 a # 2 i2 + ? true # 3 i2 + cmd2 # 4 i2 + ? true # 5 i2 i1 + cmd3 # 6 i2 i1 + ? false # 5 i2 i2 + cmd5 # 8 i2 + ? false # 1 i3 + cmd6 # 9 + EOO +} + +: contained +: +{ + : semi + : + $* <>EOE != 0 + while + cmd; + cmd + end + EOI + testscript:2:3: error: ';' inside 'while' + EOE + + : colon-leading + : + $* <>EOE != 0 + while + : foo + cmd + end + EOI + testscript:2:3: error: description inside 'while' + EOE + + : colon-trailing + : + $* <>EOE != 0 + while + cmd : foo + end + EOI + testscript:2:3: error: description inside 'while' + EOE + + : eos + : + $* <>EOE != 0 + while + EOI + testscript:2:1: error: expected closing 'end' + EOE + + : scope + : + $* <>EOE != 0 + while + cmd + { + } + end + EOI + testscript:3:3: error: expected closing 'end' + EOE + + : setup + : + $* <>EOE != 0 + while + +cmd + end + EOI + testscript:2:3: error: setup command inside 'while' + EOE + + : tdown + : + $* <>EOE != 0 + while + -cmd + end + EOI + testscript:2:3: error: teardown command inside 'while' + EOE +} + +: var +: +$* <>EOO +while ($v1 != "a") + v1 = "$(v1)a" + v2 = "$v1" +end +cmd $v1 +EOI +? true +? false +cmd a +EOO + +: leading-and-trailing-description +: +$* <>EOE != 0 +: foo +while false + cmd +end : bar +EOI +testscript:4:1: error: both leading and trailing descriptions +EOE diff --git a/libbuild2/test/script/parser.cxx b/libbuild2/test/script/parser.cxx index eb7b140..a99f80a 100644 --- a/libbuild2/test/script/parser.cxx +++ b/libbuild2/test/script/parser.cxx @@ -293,22 +293,28 @@ namespace build2 } // Parse a logical line (as well as scope-if since the only way to - // recognize it is to parse the if line). + // recognize it is to parse the if line), handling the flow control + // constructs recursively. // // If one is true then only parse one line returning an indication of - // whether the line ended with a semicolon. If if_line is true then this - // line can be an if-else construct flow control line (else, end, etc). + // whether the line ended with a semicolon. If the flow control + // construct type is specified, then this line is assumed to belong to + // such construct. // bool parser:: pre_parse_line (token& t, type& tt, optional& d, lines* ls, bool one, - bool if_line) + optional fct) { // enter: next token is peeked at (type in tt) // leave: newline + assert (!fct || + *fct == line_type::cmd_if || + *fct == line_type::cmd_while); + // Note: token is only peeked at. // const location ll (get_location (peeked ())); @@ -364,8 +370,9 @@ namespace build2 { const string& n (t.value); - if (n == "if") lt = line_type::cmd_if; - else if (n == "if!") lt = line_type::cmd_ifn; + if (n == "if") lt = line_type::cmd_if; + else if (n == "if!") lt = line_type::cmd_ifn; + else if (n == "while") lt = line_type::cmd_while; } break; @@ -420,16 +427,20 @@ namespace build2 case line_type::cmd_elif: case line_type::cmd_elifn: case line_type::cmd_else: - case line_type::cmd_end: { - if (!if_line) - { + if (!fct || *fct != line_type::cmd_if) fail (t) << lt << " without preceding 'if'"; - } + } + // Fall through. + case line_type::cmd_end: + { + if (!fct) + fail (t) << lt << " without preceding 'if', 'for', or 'while'"; } // Fall through. case line_type::cmd_if: case line_type::cmd_ifn: + case line_type::cmd_while: next (t, tt); // Skip to start of command. // Fall through. case line_type::cmd: @@ -440,8 +451,9 @@ namespace build2 p = parse_command_expr (t, tt, lexer::redirect_aliases); // Colon and semicolon are only valid in test command lines and - // after 'end' in if-else. Note that we still recognize them - // lexically, they are just not valid tokens per the grammar. + // after 'end' in a flow control construct. Note that we still + // recognize them lexically, they are just not valid tokens per + // the grammar. // if (tt != type::newline) { @@ -504,14 +516,17 @@ namespace build2 if (ls->empty ()) return semi; } + else if (lt == line_type::cmd_while) + semi = pre_parse_while (t, tt, d, *ls); // Unless we were told where to put it, decide where it actually goes. // if (ls == &ls_data) { - // First pre-check variable and variable-if: by themselves (i.e., - // without a trailing semicolon) they are treated as either setup or - // teardown without plus/minus. Also handle illegal line types. + // First pre-check variables and variable-only flow control + // constructs: by themselves (i.e., without a trailing semicolon) + // they are treated as either setup or teardown without + // plus/minus. Also handle illegal line types. // switch (lt) { @@ -524,8 +539,9 @@ namespace build2 } case line_type::cmd_if: case line_type::cmd_ifn: + case line_type::cmd_while: { - // See if this is a variable-only command-if. + // See if this is a variable-only flow control construct. // if (find_if (ls_data.begin (), ls_data.end (), [] (const line& l) { @@ -549,7 +565,7 @@ namespace build2 fail (ll) << "description before setup/teardown variable"; else fail (ll) << "description before/after setup/teardown " - << "variable-if"; + << "variable-only " << lt; } // If we don't have any nested scopes or teardown commands, @@ -793,7 +809,7 @@ namespace build2 td, &ls, true /* one */, - true /* if_line */)); + line_type::cmd_if)); assert (ls.size () == 1 && ls.back ().type == lt); assert (tt == type::newline); @@ -831,6 +847,97 @@ namespace build2 return false; // We never end with a semi. } + // Pre-parse the flow control construct block line. Fail if the line is + // unexpectedly followed with a semicolon or test description. + // + bool parser:: + pre_parse_block_line (token& t, type& tt, + line_type bt, + optional& d, + lines& ls) + { + // enter: peeked first token of the line (type in tt) + // leave: newline + + const location ll (get_location (peeked ())); + + switch (tt) + { + case type::colon: + fail (ll) << "description inside " << bt << endf; + case type::eos: + case type::rcbrace: + case type::lcbrace: + fail (ll) << "expected closing 'end'" << endf; + case type::plus: + fail (ll) << "setup command inside " << bt << endf; + case type::minus: + fail (ll) << "teardown command inside " << bt << endf; + } + + // Parse one line. Note that this one line can still be multiple lines + // in case of a flow control construct. In this case we want to view + // it as, for example, cmd_if, not cmd_end. Thus remember the start + // position of the next logical line. + // + size_t i (ls.size ()); + + line_type fct; // Flow control type the block type relates to. + + switch (bt) + { + case line_type::cmd_if: + case line_type::cmd_ifn: + case line_type::cmd_elif: + case line_type::cmd_elifn: + case line_type::cmd_else: + { + fct = line_type::cmd_if; + break; + } + case line_type::cmd_while: + { + fct = line_type::cmd_while; + break; + } + default: assert(false); + } + + optional td; + bool semi (pre_parse_line (t, tt, td, &ls, true /* one */, fct)); + + assert (tt == type::newline); + + line_type lt (ls[i].type); + + // First take care of 'end'. + // + if (lt == line_type::cmd_end) + { + if (td) + { + if (d) + fail (ll) << "both leading and trailing descriptions"; + + d = move (td); + } + + return semi; + } + + // For any other line trailing semi or description is illegal. + // + // @@ Not the exact location of semi/colon. + // + if (semi) + fail (ll) << "';' inside " << bt; + + if (td) + fail (ll) << "description inside " << bt; + + return false; + } + bool parser:: pre_parse_if_else_command (token& t, type& tt, optional& d, @@ -839,70 +946,23 @@ namespace build2 // enter: peeked first token of next line (type in tt) // leave: newline - // Parse lines until we see closing 'end'. Nested if-else blocks are - // handled recursively. + // Parse lines until we see closing 'end'. // for (line_type bt (line_type::cmd_if); // Current block. ; tt = peek (lexer_mode::first_token)) { const location ll (get_location (peeked ())); - - switch (tt) - { - case type::colon: - fail (ll) << "description inside " << bt << endf; - case type::eos: - case type::rcbrace: - case type::lcbrace: - fail (ll) << "expected closing 'end'" << endf; - case type::plus: - fail (ll) << "setup command inside " << bt << endf; - case type::minus: - fail (ll) << "teardown command inside " << bt << endf; - } - - // Parse one line. Note that this one line can still be multiple - // lines in case of if-else. In this case we want to view it as - // cmd_if, not cmd_end. Thus remember the start position of the - // next logical line. - // size_t i (ls.size ()); - optional td; - bool semi (pre_parse_line (t, tt, - td, - &ls, - true /* one */, - true /* if_line */)); - assert (tt == type::newline); + bool semi (pre_parse_block_line (t, tt, bt, d, ls)); line_type lt (ls[i].type); // First take care of 'end'. // if (lt == line_type::cmd_end) - { - if (td) - { - if (d) - fail (ll) << "both leading and trailing descriptions"; - - d = move (td); - } - return semi; - } - - // For any other line trailing semi or description is illegal. - // - // @@ Not the exact location of semi/colon. - // - if (semi) - fail (ll) << "';' inside " << bt; - - if (td) - fail (ll) << "description inside " << bt; // Check if-else block sequencing. // @@ -924,6 +984,36 @@ namespace build2 default: break; } } + + assert (false); // Can't be here. + return false; + } + + bool parser:: + pre_parse_while (token& t, type& tt, + optional& d, + lines& ls) + { + // enter: (previous line) + // leave: + + tt = peek (lexer_mode::first_token); + + // Parse lines until we see closing 'end'. + // + for (;; tt = peek (lexer_mode::first_token)) + { + size_t i (ls.size ()); + + bool semi ( + pre_parse_block_line (t, tt, line_type::cmd_while, d, ls)); + + if (ls[i].type == line_type::cmd_end) + return semi; + } + + assert (false); // Can't be here. + return false; } void parser:: @@ -1424,7 +1514,7 @@ namespace build2 command_type ct; auto exec_cmd = [&ct, this] (token& t, build2::script::token_type& tt, - size_t li, + const iteration_index* ii, size_t li, bool single, const location& ll) { @@ -1437,19 +1527,20 @@ namespace build2 command_expr ce ( parse_command_line (t, static_cast (tt))); - runner_->run (*scope_, ce, ct, li, ll); + runner_->run (*scope_, ce, ct, ii, li, ll); }; - auto exec_if = [this] (token& t, build2::script::token_type& tt, - size_t li, - const location& ll) + auto exec_cond = [this] (token& t, build2::script::token_type& tt, + const iteration_index* ii, size_t li, + const location& ll) { command_expr ce ( parse_command_line (t, static_cast (tt))); - // Assume if-else always involves multiple commands. + // Assume a flow control construct always involves multiple + // commands. // - return runner_->run_if (*scope_, ce, li, ll); + return runner_->run_cond (*scope_, ce, ii, li, ll); }; size_t li (1); @@ -1459,16 +1550,16 @@ namespace build2 ct = command_type::test; exec_lines (t->tests_.begin (), t->tests_.end (), - exec_set, exec_cmd, exec_if, - li); + exec_set, exec_cmd, exec_cond, + nullptr /* iteration_index */, li); } else if (group* g = dynamic_cast (scope_)) { ct = command_type::setup; bool exec_scope (exec_lines (g->setup_.begin (), g->setup_.end (), - exec_set, exec_cmd, exec_if, - li)); + exec_set, exec_cmd, exec_cond, + nullptr /* iteration_index */, li)); if (exec_scope) { @@ -1526,7 +1617,8 @@ namespace build2 try { - take = runner_->run_if (*scope_, ce, li++, ll); + take = runner_->run_cond ( + *scope_, ce, nullptr /* iteration_index */, li++, ll); } catch (const exit_scope& e) { @@ -1637,8 +1729,8 @@ namespace build2 ct = command_type::teardown; exec_lines (g->tdown_.begin (), g->tdown_.end (), - exec_set, exec_cmd, exec_if, - li); + exec_set, exec_cmd, exec_cond, + nullptr /* iteration_index */, li); } else assert (false); diff --git a/libbuild2/test/script/parser.hxx b/libbuild2/test/script/parser.hxx index 0d15580..31dd41d 100644 --- a/libbuild2/test/script/parser.hxx +++ b/libbuild2/test/script/parser.hxx @@ -62,7 +62,13 @@ namespace build2 optional&, lines* = nullptr, bool one = false, - bool if_line = false); + optional flow_control_type = nullopt); + + bool + pre_parse_block_line (token&, token_type&, + line_type block_type, + optional&, + lines&); bool pre_parse_if_else (token&, token_type&, @@ -79,6 +85,11 @@ namespace build2 optional&, lines&); + bool + pre_parse_while (token&, token_type&, + optional&, + lines&); + void pre_parse_directive (token&, token_type&); diff --git a/libbuild2/test/script/parser.test.cxx b/libbuild2/test/script/parser.test.cxx index ccd4104..ab0aee9 100644 --- a/libbuild2/test/script/parser.test.cxx +++ b/libbuild2/test/script/parser.test.cxx @@ -33,8 +33,11 @@ namespace build2 class print_runner: public runner { public: - print_runner (bool scope, bool id, bool line) - : scope_ (scope), id_ (id), line_ (line) {} + print_runner (bool scope, bool id, bool line, bool iterations) + : scope_ (scope), + id_ (id), + line_ (line), + iterations_ (iterations) {} virtual bool test (scope&) const override @@ -99,7 +102,7 @@ namespace build2 virtual void run (scope&, const command_expr& e, command_type t, - size_t i, + const iteration_index* ii, size_t i, const location&) override { const char* s (nullptr); @@ -113,22 +116,22 @@ namespace build2 cout << ind_ << s << e; - if (line_) - cout << " # " << i; + if (line_ || iterations_) + print_line_info (ii, i); cout << endl; } virtual bool - run_if (scope&, - const command_expr& e, - size_t i, - const location&) override + run_cond (scope&, + const command_expr& e, + const iteration_index* ii, size_t i, + const location&) override { cout << ind_ << "? " << e; - if (line_) - cout << " # " << i; + if (line_ || iterations_) + print_line_info (ii, i); cout << endl; @@ -146,13 +149,33 @@ namespace build2 } private: + void + print_line_info (const iteration_index* ii, size_t i) const + { + cout << " #"; + + if (line_) + cout << ' ' << i; + + if (iterations_ && ii != nullptr) + { + string s; + for (const iteration_index* i (ii); i != nullptr; i = i->prev) + s.insert (0, " i" + to_string (i->index)); + + cout << s; + } + } + + private: bool scope_; bool id_; bool line_; + bool iterations_; string ind_; }; - // Usage: argv[0] [-s] [-i] [-l] [] + // Usage: argv[0] [-s] [-i] [-l] [-r] [] // int main (int argc, char* argv[]) @@ -174,6 +197,7 @@ namespace build2 bool scope (false); bool id (false); bool line (false); + bool iterations (false); path name; for (int i (1); i != argc; ++i) @@ -186,6 +210,8 @@ namespace build2 id = true; else if (a == "-l") line = true; + else if (a == "-r") + iterations = true; else { name = path (move (a)); @@ -236,7 +262,7 @@ namespace build2 script s (tt, st, dir_path (work) /= "test-driver"); p.pre_parse (cin, s); - print_runner r (scope, id, line); + print_runner r (scope, id, line, iterations); p.execute (s, r); } catch (const failed&) diff --git a/libbuild2/test/script/runner.cxx b/libbuild2/test/script/runner.cxx index 8054c61..42eef04 100644 --- a/libbuild2/test/script/runner.cxx +++ b/libbuild2/test/script/runner.cxx @@ -142,7 +142,8 @@ namespace build2 void default_runner:: run (scope& sp, const command_expr& expr, command_type ct, - size_t li, const location& ll) + const iteration_index* ii, size_t li, + const location& ll) { // Noop for teardown commands if keeping tests output is requested. // @@ -175,13 +176,14 @@ namespace build2 dr << info << "test id: " << sp.id_path.posix_string (); }); - build2::script::run (sp, expr, li, ll); + build2::script::run (sp, expr, ii, li, ll); } bool default_runner:: - run_if (scope& sp, - const command_expr& expr, - size_t li, const location& ll) + run_cond (scope& sp, + const command_expr& expr, + const iteration_index* ii, size_t li, + const location& ll) { if (verb >= 3) text << ": ?" << expr; @@ -197,7 +199,7 @@ namespace build2 dr << info << "test id: " << sp.id_path.posix_string (); }); - return build2::script::run_if (sp, expr, li, ll); + return build2::script::run_cond (sp, expr, ii, li, ll); } } } diff --git a/libbuild2/test/script/runner.hxx b/libbuild2/test/script/runner.hxx index b6a038d..0309a35 100644 --- a/libbuild2/test/script/runner.hxx +++ b/libbuild2/test/script/runner.hxx @@ -51,11 +51,14 @@ namespace build2 virtual void run (scope&, const command_expr&, command_type, - size_t index, + const iteration_index*, size_t index, const location&) = 0; virtual bool - run_if (scope&, const command_expr&, size_t, const location&) = 0; + run_cond (scope&, + const command_expr&, + const iteration_index*, size_t, + const location&) = 0; // Location is the scope end location (for diagnostics, etc). // @@ -84,11 +87,14 @@ namespace build2 virtual void run (scope&, const command_expr&, command_type, - size_t, + const iteration_index*, size_t, const location&) override; virtual bool - run_if (scope&, const command_expr&, size_t, const location&) override; + run_cond (scope&, + const command_expr&, + const iteration_index*, size_t, + const location&) override; virtual void leave (scope&, const location&) override; diff --git a/libbuild2/test/script/script.hxx b/libbuild2/test/script/script.hxx index 22f6725..b75f68e 100644 --- a/libbuild2/test/script/script.hxx +++ b/libbuild2/test/script/script.hxx @@ -21,13 +21,14 @@ namespace build2 namespace script { using build2::script::line; + using build2::script::line_type; using build2::script::lines; using build2::script::redirect; using build2::script::redirect_type; - using build2::script::line_type; - using build2::script::command_expr; - using build2::script::expr_term; using build2::script::command; + using build2::script::expr_term; + using build2::script::command_expr; + using build2::script::iteration_index; using build2::script::environment_vars; using build2::script::deadline; using build2::script::timeout; diff --git a/tests/recipe/buildscript/testscript b/tests/recipe/buildscript/testscript index 0bf752e..54c3bbe 100644 --- a/tests/recipe/buildscript/testscript +++ b/tests/recipe/buildscript/testscript @@ -487,7 +487,7 @@ posix = ($cxx.target.class != 'windows') : normal : { - cat <=bar.h; + cat <=bar.h; bar EOI @@ -519,23 +519,23 @@ posix = ($cxx.target.class != 'windows') }} EOI - $* 2>>EOE; + $* 2>>EOE; gen h{baz.h} gen h{foo.h} EOE - cat foo.h >>EOO; - bar - baz - EOO + cat foo.h >>EOO; + bar + baz + EOO - $* clean 2>- + $* clean 2>- } : byproduct : { - cat <=bar.h; + cat <=bar.h; bar EOI @@ -562,17 +562,17 @@ posix = ($cxx.target.class != 'windows') }} EOI - $* 2>>EOE; + $* 2>>EOE; gen h{baz.h} gen h{foo.h} EOE - cat foo.h >>EOO; - bar - baz - EOO + cat foo.h >>EOO; + bar + baz + EOO - $* clean 2>- + $* clean 2>- } } } @@ -885,3 +885,29 @@ if $posix alias{bar} alias{far} EOE } + +: flow-control-construct +: +{ + : while + : + { + echo 'bar' >=bar; + + cat <=buildfile; + foo: bar + {{ + p = $path($>) + while test -f $p != 0 + cp $path($<) $p + end + }} + EOI + + $* 2>'cp file{foo}'; + + cat <<'bar'; + + $* clean 2>- + } +} diff --git a/tests/test/script/runner/pipe.testscript b/tests/test/script/runner/pipe.testscript index 205fd55..92ab33e 100644 --- a/tests/test/script/runner/pipe.testscript +++ b/tests/test/script/runner/pipe.testscript @@ -17,7 +17,7 @@ $c <'$* -o foo | cat >foo' && $b : process-to-builtin : $c <'$* -o foo -s 1 | $* -i 1 >foo -s 2' && $b 2>>/~%EOE% != 0 %testscript:1:1: error: .+ exited with code 2% - info: stdout: test/1/stdout-2 + info: stdout: test/1/stdout-c2 info: test id: 1 EOE @@ -25,9 +25,9 @@ $c <'$* -o foo | cat >foo' && $b : process-to-builtin : $c <'$* -o foo -e foo 2>bar | $* -i 2 2>baz' && $b 2>>/~%EOE% != 0 %testscript:1:1: error: .+ stderr doesn't match expected% - info: stderr: test/1/stderr-2 - info: expected stderr: test/1/stderr-2.orig - info: stderr diff: test/1/stderr-2.diff + info: stderr: test/1/stderr-c2 + info: expected stderr: test/1/stderr-c2.orig + info: stderr diff: test/1/stderr-c2.diff %.{3} -baz +foo diff --git a/tests/test/script/runner/redirect.testscript b/tests/test/script/runner/redirect.testscript index 0fe3aa3..209c4ce 100644 --- a/tests/test/script/runner/redirect.testscript +++ b/tests/test/script/runner/redirect.testscript @@ -654,9 +654,9 @@ psr = ($cxx.target.class != 'windows' ? '/' : '\\') # Path separator in regex. $* -o bar >?out EOI %testscript:2: error: ../../../../../driver(.exe)? stdout doesn't match expected% - info: stdout: test/1/stdout-2 + info: stdout: test/1/stdout-n2 info: expected stdout: test/1/out - info: stdout diff: test/1/stdout-2.diff + info: stdout diff: test/1/stdout-n2.diff %--- \.*% %\+\+\+ \.*% %@@ \.*% diff --git a/tests/test/script/runner/while.testscript b/tests/test/script/runner/while.testscript new file mode 100644 index 0000000..1c58827 --- /dev/null +++ b/tests/test/script/runner/while.testscript @@ -0,0 +1,16 @@ +# file : tests/test/script/runner/while.testscript +# license : MIT; see accompanying LICENSE file + +.include ../common.testscript + +: basics +: +$c <>EOO + while ($v != "aa") + echo "$v" >| + v = "$(v)a" + end + EOI + + a + EOO -- cgit v1.1