diff options
Diffstat (limited to 'libbuild2/build/script')
30 files changed, 3937 insertions, 0 deletions
diff --git a/libbuild2/build/script/lexer+command-line.test.testscript b/libbuild2/build/script/lexer+command-line.test.testscript new file mode 100644 index 0000000..3eceae8 --- /dev/null +++ b/libbuild2/build/script/lexer+command-line.test.testscript @@ -0,0 +1,164 @@ +# file : libbuild2/build/script/lexer+command-line.test.testscript +# license : MIT; see accompanying LICENSE file + +test.arguments = command-line + +: redirect +: +{ + : pass + : + $* <"cmd <| 1>|" >>EOO + 'cmd' + <| + '1' + >| + <newline> + EOO + + : null + : + $* <"cmd <- 1>-" >>EOO + 'cmd' + <- + '1' + >- + <newline> + EOO + + : trace + : + $* <"cmd 1>!" >>EOO + 'cmd' + '1' + >! + <newline> + EOO + + : merge + : + $* <"cmd 1>&2" >>EOO + 'cmd' + '1' + >& + '2' + <newline> + EOO + + : str + : + $* <"cmd <<<=a 1>>>?b" >>EOO + 'cmd' + <<<= + 'a' + '1' + >>>? + 'b' + <newline> + EOO + + : str-nn + : + $* <"cmd <<<=:a 1>>>?:b" >>EOO + 'cmd' + <<<=: + 'a' + '1' + >>>?: + 'b' + <newline> + EOO + + : str-nn-alias + : + $* <"cmd <<<:a 1>>>?:b" >>EOO + 'cmd' + <<<: + 'a' + '1' + >>>?: + 'b' + <newline> + EOO + + : doc + : + $* <"cmd <<EOI 1>>EOO" >>EOO + 'cmd' + << + 'EOI' + '1' + >> + 'EOO' + <newline> + EOO + + : doc-nn + : + $* <"cmd <<:EOI 1>>?:EOO" >>EOO + 'cmd' + <<: + 'EOI' + '1' + >>?: + 'EOO' + <newline> + EOO + + : file-cmp + : + $* <"cmd <=in >?out 2>?err" >>EOO + 'cmd' + <= + 'in' + >? + 'out' + '2' + >? + 'err' + <newline> + EOO + + : file-write + : + $* <"cmd >=out 2>+err" >>EOO + 'cmd' + >= + 'out' + '2' + >+ + 'err' + <newline> + EOO +} + +: cleanup +: +{ + : always + : + $* <"cmd &file" >>EOO + 'cmd' + & + 'file' + <newline> + EOO + + : maybe + : + $* <"cmd &?file" >>EOO + 'cmd' + &? + 'file' + <newline> + EOO + + : never + : + $* <"cmd &!file" >>EOO + 'cmd' + &! + 'file' + <newline> + EOO +} diff --git a/libbuild2/build/script/lexer+first-token.test.testscript b/libbuild2/build/script/lexer+first-token.test.testscript new file mode 100644 index 0000000..6709e60 --- /dev/null +++ b/libbuild2/build/script/lexer+first-token.test.testscript @@ -0,0 +1,30 @@ +# file : libbuild2/build/script/lexer+first-token.test.testscript +# license : MIT; see accompanying LICENSE file + +# Note: this mode auto-expires after each token. +# +test.arguments = first-token + +: assign +: +$* <"foo=" >>EOO +'foo' +'=' +<newline> +EOO + +: append +: +$* <"foo+=" >>EOO +'foo' +'+=' +<newline> +EOO + +: prepend +: +$* <"foo=+" >>EOO +'foo' +'=+' +<newline> +EOO diff --git a/libbuild2/build/script/lexer+second-token.test.testscript b/libbuild2/build/script/lexer+second-token.test.testscript new file mode 100644 index 0000000..d5f3329 --- /dev/null +++ b/libbuild2/build/script/lexer+second-token.test.testscript @@ -0,0 +1,53 @@ +# file : libbuild2/build/script/lexer+second-token.test.testscript +# license : MIT; see accompanying LICENSE file + +# Note: this mode auto-expires after each token. +# +test.arguments = second-token + +: assign +: +$* <"=foo" >>EOO += +'foo' +<newline> +EOO + +: append +: +$* <"+= foo" >>EOO ++= +'foo' +<newline> +EOO + +: prepend +: +$* <" =+ foo" >>EOO +=+ +'foo' +<newline> +EOO + +: assign-leading +: +$* <"foo=bar" >>EOO +'foo=bar' +<newline> +EOO + +: append-leading +: +$* <"foo+= bar" >>EOO +'foo+=' +'bar' +<newline> +EOO + +: prepend-leading +: +$* <"foo =+bar" >>EOO +'foo' +'=+bar' +<newline> +EOO diff --git a/libbuild2/build/script/lexer+variable-line.test.testscript b/libbuild2/build/script/lexer+variable-line.test.testscript new file mode 100644 index 0000000..e4b5adb --- /dev/null +++ b/libbuild2/build/script/lexer+variable-line.test.testscript @@ -0,0 +1,12 @@ +# file : libbuild2/build/script/lexer+variable-line.test.testscript +# license : MIT; see accompanying LICENSE file + +test.arguments = variable-line + +: basic +: +$* <"a 'b c'" >>EOO +'a' +'b c' +<newline> +EOO diff --git a/libbuild2/build/script/lexer+variable.test.testscript b/libbuild2/build/script/lexer+variable.test.testscript new file mode 100644 index 0000000..54b0a30 --- /dev/null +++ b/libbuild2/build/script/lexer+variable.test.testscript @@ -0,0 +1,25 @@ +# file : libbuild2/build/script/lexer+variable.test.testscript +# license : MIT; see accompanying LICENSE file + +# Test handling custom variable names ($*, $~, $NN). +# +test.arguments = variable + +: primary-target +: +{ + : only + : + $* <">" >>EOO + '>' + <newline> + EOO + + : followed + : + $* <">abc" >>EOO + '>' + 'abc' + <newline> + EOO +} diff --git a/libbuild2/build/script/lexer.cxx b/libbuild2/build/script/lexer.cxx new file mode 100644 index 0000000..7b8bdd4 --- /dev/null +++ b/libbuild2/build/script/lexer.cxx @@ -0,0 +1,270 @@ +// file : libbuild2/build/script/lexer.cxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#include <libbuild2/build/script/lexer.hxx> + +using namespace std; + +namespace build2 +{ + namespace build + { + namespace script + { + using type = token_type; + + build2::script::redirect_aliases lexer::redirect_aliases { + type (type::in_file), + type (type::in_doc), + type (type::in_str), + type (type::out_file_ovr), + type (type::out_file_app), + nullopt}; + + void lexer:: + mode (build2::lexer_mode m, + char ps, + optional<const char*> esc, + uintptr_t data) + { + bool a (false); // attributes + + const char* s1 (nullptr); + const char* s2 (nullptr); + + bool s (true); // space + bool n (true); // newline + bool q (true); // quotes + + if (!esc) + { + assert (!state_.empty ()); + esc = state_.top ().escapes; + } + + switch (m) + { + case lexer_mode::command_line: + { + s1 = "=!|&<> $(#\t\n"; + s2 = "== "; + break; + } + case lexer_mode::first_token: + { + // First token on the script line. Like command_line but + // recognizes variable assignments as separators. + // + s1 = "=+!|&<> $(#\t\n"; + s2 = " == "; + break; + } + case lexer_mode::second_token: + { + // Second token on the script line. Like command_line but + // recognizes leading variable assignments. + // + // Note that to recognize only leading assignments we shouldn't + // add them to the separator strings (so this is identical to + // command_line). + // + s1 = "=!|&<> $(#\t\n"; + s2 = "== "; + break; + } + case lexer_mode::variable_line: + { + // Like value except we don't recognize '{'. + // + s1 = " $(#\t\n"; + s2 = " "; + break; + } + default: + { + base_lexer::mode (m, ps, esc); + return; + } + } + + assert (ps == '\0'); + state_.push (state {m, data, nullopt, a, ps, s, n, q, *esc, s1, s2}); + } + + token lexer:: + next () + { + token r; + + switch (state_.top ().mode) + { + case lexer_mode::command_line: + case lexer_mode::first_token: + case lexer_mode::second_token: + case lexer_mode::variable_line: + r = next_line (); + break; + default: return base_lexer::next (); + } + + if (r.qtype != quote_type::unquoted) + ++quoted_; + + return r; + } + + token lexer:: + next_line () + { + bool sep (skip_spaces ().first); + + xchar c (get ()); + uint64_t ln (c.line), cn (c.column); + + state st (state_.top ()); // Make copy (see first/second_token). + lexer_mode m (st.mode); + + auto make_token = [&sep, ln, cn] (type t) + { + return token (t, sep, ln, cn, token_printer); + }; + + // Handle attributes (do it first to make sure the flag is cleared + // regardless of what we return). + // + if (st.attributes) + { + assert (m == lexer_mode::variable_line); + + state_.top ().attributes = false; + + if (c == '[') + return make_token (type::lsbrace); + } + + if (eos (c)) + return make_token (type::eos); + + // Expire certain modes at the end of the token. Do it early in case + // we push any new mode (e.g., double quote). + // + if (m == lexer_mode::first_token || m == lexer_mode::second_token) + state_.pop (); + + // NOTE: remember to update mode() if adding new special characters. + + switch (c) + { + case '\n': + { + // Expire variable value mode at the end of the line. + // + if (m == lexer_mode::variable_line) + state_.pop (); + + sep = true; // Treat newline as always separated. + return make_token (type::newline); + } + + // Variable expansion, function call, and evaluation context. + // + case '$': return make_token (type::dollar); + case '(': return make_token (type::lparen); + } + + // Command line operator/separators. + // + if (m == lexer_mode::command_line || + m == lexer_mode::first_token || + m == lexer_mode::second_token) + { + switch (c) + { + // Comparison (==, !=). + // + case '=': + case '!': + { + if (peek () == '=') + { + get (); + return make_token (c == '=' ? type::equal : type::not_equal); + } + } + } + } + + // Command operators. + // + if (m == lexer_mode::command_line || + m == lexer_mode::first_token || + m == lexer_mode::second_token) + { + if (optional<token> t = next_cmd_op (c, sep)) + return move (*t); + } + + // Variable assignment (=, +=, =+). + // + if (m == lexer_mode::second_token) + { + switch (c) + { + case '=': + { + if (peek () == '+') + { + get (); + return make_token (type::prepend); + } + else + return make_token (type::assign); + } + case '+': + { + if (peek () == '=') + { + get (); + return make_token (type::append); + } + } + } + } + + // Otherwise it is a word. + // + unget (c); + return word (st, sep); + } + + token lexer:: + word (state st, bool sep) + { + lexer_mode m (st.mode); + + // Customized implementation that handles special variable names ($>, + // $<, $~). + // + // @@ TODO: $(<), $(>): feels like this will have to somehow be + // handled at the top-level lexer level. Maybe provide a + // string of one-char special variable names as state::data? + // + if (m != lexer_mode::variable) + return base_lexer::word (st, sep); + + xchar c (peek ()); + + if (c != '>' && c != '<' && c != '~') + return base_lexer::word (st, sep); + + get (); + + state_.pop (); // Expire the variable mode. + return token (string (1, c), + sep, + quote_type::unquoted, false, + c.line, c.column); + } + } + } +} diff --git a/libbuild2/build/script/lexer.hxx b/libbuild2/build/script/lexer.hxx new file mode 100644 index 0000000..7d919e5 --- /dev/null +++ b/libbuild2/build/script/lexer.hxx @@ -0,0 +1,80 @@ +// file : libbuild2/build/script/lexer.hxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#ifndef LIBBUILD2_BUILD_SCRIPT_LEXER_HXX +#define LIBBUILD2_BUILD_SCRIPT_LEXER_HXX + +#include <libbuild2/types.hxx> +#include <libbuild2/utility.hxx> + +#include <libbuild2/script/lexer.hxx> + +#include <libbuild2/build/script/token.hxx> + +namespace build2 +{ + namespace build + { + namespace script + { + struct lexer_mode: build2::script::lexer_mode + { + using base_type = build2::script::lexer_mode; + + enum + { + command_line = base_type::value_next, + first_token, // Expires at the end of the token. + second_token, // Expires at the end of the token. + variable_line // Expires at the end of the line. + }; + + lexer_mode () = default; + lexer_mode (value_type v): base_type (v) {} + lexer_mode (build2::lexer_mode v): base_type (v) {} + }; + + class lexer: public build2::script::lexer + { + public: + using base_lexer = build2::script::lexer; + + // Note that neither the name nor escape arguments are copied. + // + lexer (istream& is, + const path_name& name, + uint64_t line, // Start line in the stream. + lexer_mode m, + const char* escapes = nullptr) + : base_lexer (is, name, line, + nullptr /* escapes */, + false /* set_mode */, + redirect_aliases) + { + mode (m, '\0', escapes); + } + + virtual void + mode (build2::lexer_mode, + char = '\0', + optional<const char*> = nullopt, + uintptr_t = 0) override; + + virtual token + next () override; + + public: + static redirect_aliases_type redirect_aliases; + + private: + token + next_line (); + + virtual token + word (state, bool) override; + }; + } + } +} + +#endif // LIBBUILD2_BUILD_SCRIPT_LEXER_HXX diff --git a/libbuild2/build/script/lexer.test.cxx b/libbuild2/build/script/lexer.test.cxx new file mode 100644 index 0000000..1c47442 --- /dev/null +++ b/libbuild2/build/script/lexer.test.cxx @@ -0,0 +1,77 @@ +// file : libbuild2/build/script/lexer.test.cxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#include <cassert> +#include <iostream> + +#include <libbuild2/types.hxx> +#include <libbuild2/utility.hxx> + +#include <libbuild2/build/script/token.hxx> +#include <libbuild2/build/script/lexer.hxx> + +using namespace std; + +namespace build2 +{ + namespace build + { + namespace script + { + // Usage: argv[0] <lexer-mode> + // + int + main (int argc, char* argv[]) + { + lexer_mode m; + { + assert (argc == 2); + string s (argv[1]); + + if (s == "command-line") m = lexer_mode::command_line; + else if (s == "first-token") m = lexer_mode::first_token; + else if (s == "second-token") m = lexer_mode::second_token; + else if (s == "variable-line") m = lexer_mode::variable_line; + else if (s == "variable") m = lexer_mode::variable; + else assert (false); + } + + try + { + cin.exceptions (istream::failbit | istream::badbit); + + // Some modes auto-expire so we need something underneath. + // + bool u (m != lexer_mode::command_line); + + path_name in ("<stdin>"); + lexer l (cin, in, 1 /* line */, lexer_mode::command_line); + if (u) + l.mode (m); + + // No use printing eos since we will either get it or loop forever. + // + for (token t (l.next ()); t.type != token_type::eos; t = l.next ()) + { + // Print each token on a separate line without quoting operators. + // + t.printer (cout, t, print_mode::normal); + cout << endl; + } + } + catch (const failed&) + { + return 1; + } + + return 0; + } + } + } +} + +int +main (int argc, char* argv[]) +{ + return build2::build::script::main (argc, argv); +} diff --git a/libbuild2/build/script/parser+cleanup.test.testscript b/libbuild2/build/script/parser+cleanup.test.testscript new file mode 100644 index 0000000..9a5af3d --- /dev/null +++ b/libbuild2/build/script/parser+cleanup.test.testscript @@ -0,0 +1,57 @@ +# file : libbuild2/build/script/parser+cleanup.test.testscript +# license : MIT; see accompanying LICENSE file + +: always +: +$* <<EOI >>EOO +cmd &file +EOI +cmd &file +EOO + +: maybe +: +$* <<EOI >>EOO +cmd &?file +EOI +cmd &?file +EOO + +: never +: +$* <<EOI >>EOO +cmd &!file +EOI +cmd &!file +EOO + +: empty +: +$* <<EOI 2>>EOE != 0 +cmd &"" +EOI +buildfile:11:6: error: empty cleanup path +EOE + +: missed-before +: +{ + : token + : + : Path missed before command next token + : + $* <<EOI 2>>EOE != 0 + cmd & >file + EOI + buildfile:11:7: error: missing cleanup path + EOE + + : end + : Test path missed before end of command + : + $* <<EOI 2>>EOE != 0 + cmd & + EOI + buildfile:11:6: error: missing cleanup path + EOE +} diff --git a/libbuild2/build/script/parser+command-if.test.testscript b/libbuild2/build/script/parser+command-if.test.testscript new file mode 100644 index 0000000..a18a885 --- /dev/null +++ b/libbuild2/build/script/parser+command-if.test.testscript @@ -0,0 +1,395 @@ +# file : libbuild2/build/script/parser+command-if.test.testscript +# license : MIT; see accompanying LICENSE file + +: if +: +{ + : true + : + $* <<EOI >>EOO + if true foo + cmd1 + cmd2 + end + EOI + ? true foo + cmd1 + cmd2 + EOO + + : false + : + $* <<EOI >>EOO + if false foo + cmd1 + cmd2 + end + EOI + ? false foo + EOO + + : not-true + : + $* <<EOI >>EOO + if! true foo + cmd1 + cmd2 + end + EOI + ? true foo + EOO + + : not-false + : + $* <<EOI >>EOO + if! false foo + cmd1 + cmd2 + end + EOI + ? false foo + cmd1 + cmd2 + EOO + + : without-command + : + $* <<EOI 2>>EOE != 0 + if + cmd + end + EOI + buildfile:11:3: error: missing program + EOE +} + +: elif +: +{ + : true + : + $* <<EOI >>EOO + if false + cmd1 + cmd2 + elif true + cmd3 + cmd4 + end + EOI + ? false + ? true + cmd3 + cmd4 + EOO + + : false + : + $* <<EOI >>EOO + if false + cmd1 + cmd2 + elif false + cmd3 + cmd4 + end + EOI + ? false + ? false + EOO + + : not-true + : + $* <<EOI >>EOO + if false + cmd1 + cmd2 + elif! true + cmd3 + cmd4 + end + EOI + ? false + ? true + EOO + + : not-false + : + $* <<EOI >>EOO + if false + cmd1 + cmd2 + elif! false + cmd3 + cmd4 + end + EOI + ? false + ? false + cmd3 + cmd4 + EOO + + : without-if + : + $* <<EOI 2>>EOE != 0 + cmd + elif true + cmd + end + EOI + buildfile:12:1: error: 'elif' without preceding 'if' + EOE + + : not-without-if + : + $* <<EOI 2>>EOE != 0 + cmd + elif! true + cmd + end + EOI + buildfile:12:1: error: 'elif!' without preceding 'if' + EOE + + : after-else + : + $* <<EOI 2>>EOE != 0 + if false + cmd + else + cmd + elif true + cmd + end + EOI + buildfile:15:1: error: 'elif' after 'else' + EOE +} + +: else +: +{ + : true + : + $* <<EOI >>EOO + if false + cmd1 + cmd2 + else + cmd3 + cmd4 + end + EOI + ? false + cmd3 + cmd4 + EOO + + : false + : + $* <<EOI >>EOO + if true + cmd1 + cmd2 + else + cmd3 + cmd4 + end + EOI + ? true + cmd1 + cmd2 + EOO + + : chain + : + $* <<EOI >>EOO + if false + cmd + cmd + elif false + cmd + cmd + elif false + cmd + cmd + elif true + cmd1 + cmd2 + elif false + cmd + cmd + else + cmd + cmd + end + EOI + ? false + ? false + ? false + ? true + cmd1 + cmd2 + EOO + + : command-after + : + $* <<EOI 2>>EOE != 0 + if true + cmd + else cmd + cmd + end + EOI + buildfile:13:6: error: expected newline instead of 'cmd' + EOE + + : without-if + : + $* <<EOI 2>>EOE != 0 + cmd + else + cmd + end + EOI + buildfile:12:1: error: 'else' without preceding 'if' + EOE + + : after-else + : + $* <<EOI 2>>EOE != 0 + if false + cmd + else + cmd + else + cmd + end + EOI + buildfile:15:1: error: 'else' after 'else' + EOE +} + +: end +{ + : without-if + : + $* <<EOI 2>>EOE != 0 + cmd + end + EOI + buildfile:12:1: error: 'end' without preceding 'if' + EOE + + : before + { + : command + : + $* <<EOI 2>>EOE != 0 + if true + cmd + end cmd + EOI + buildfile:13:5: error: expected newline instead of 'cmd' + EOE + } +} + +: nested +: +{ + : take + : + $* <<EOI >>EOO + if true + cmd1 + if false + cmd + elif false + if true + cmd + end + else + cmd2 + end + cmd3 + end + EOI + ? true + cmd1 + ? false + ? false + cmd2 + cmd3 + EOO + + : skip + : + $* <<EOI >>EOO + if false + cmd1 + if false + cmd + elif false + if true + cmd + end + else + cmd2 + end + cmd3 + else + cmd + end + EOI + ? false + cmd + EOO +} + +: contained +: +{ + : eos + : + $* <<EOI 2>>EOE != 0 + if + EOI + buildfile:12:1: error: expected closing 'end' + EOE +} + +: line-index +: +$* -l <<EOI >>EOO +if false + cmd + if true + cmd + end + cmd +elif false + cmd +else + cmd +end +EOI +? false # 1 +? false # 6 +cmd # 8 +EOO + +: var +: +$* <<EOI >>EOO +if true + x = foo +else + x = bar +end +cmd $x +EOI +? true +cmd foo +EOO diff --git a/libbuild2/build/script/parser+command-re-parse.test.testscript b/libbuild2/build/script/parser+command-re-parse.test.testscript new file mode 100644 index 0000000..a59b49c --- /dev/null +++ b/libbuild2/build/script/parser+command-re-parse.test.testscript @@ -0,0 +1,11 @@ +# file : libbuild2/build/script/parser+command-re-parse.test.testscript +# license : MIT; see accompanying LICENSE file + +: double-quote +: +$* <<EOI >>EOO +x = cmd \">-\" "'<-'" +$x +EOI +cmd '>-' '<-' +EOO diff --git a/libbuild2/build/script/parser+exit.test.testscript b/libbuild2/build/script/parser+exit.test.testscript new file mode 100644 index 0000000..53ee1b9 --- /dev/null +++ b/libbuild2/build/script/parser+exit.test.testscript @@ -0,0 +1,26 @@ +# file : libbuild2/build/script/parser+exit.test.testscript +# license : MIT; see accompanying LICENSE file + +: eq +: +$* <<EOI >>EOO +cmd == 1 +EOI +cmd == 1 +EOO + +: ne +: +$* <<EOI >>EOO +cmd!=1 +EOI +cmd != 1 +EOO + +: end +: +$* <<EOI 2>>EOE != 0 +cmd != 1 <"foo" +EOI +buildfile:11:10: error: expected newline instead of '<' +EOE diff --git a/libbuild2/build/script/parser+expansion.test.testscript b/libbuild2/build/script/parser+expansion.test.testscript new file mode 100644 index 0000000..9f1e774 --- /dev/null +++ b/libbuild2/build/script/parser+expansion.test.testscript @@ -0,0 +1,35 @@ +# file : libbuild2/build/script/parser+expansion.test.testscript +# license : MIT; see accompanying LICENSE file + +: quote +: +: Make sure everything expanded as strings. +: +$* <<EOI >>EOO +x = dir/ proj% proj%name proj%proj%dir/type{name name {name}} +cmd dir/ proj% proj%name proj%proj%dir/type{name name {name}} +cmd $x +EOI +cmd dir/ proj% proj%name proj%proj%dir/type{name name {name}} +cmd dir/ proj% proj%name proj%proj%dir/type{name name {name}} +EOO + +: unterm-quoted-seq +: +$* <<EOI 2>>EOE != 0 +x = "'a bc" +cmd xy$x +EOI +<string>:1:8: error: unterminated single-quoted sequence + buildfile:12:5: info: while parsing string 'xy'a bc' +EOE + +: invalid-redirect +: +$* <<EOI 2>>EOE != 0 +x = "1>&a" +cmd $x +EOI +<string>:1:4: error: stdout merge redirect file descriptor must be 2 + buildfile:12:5: info: while parsing string '1>&a' +EOE diff --git a/libbuild2/build/script/parser+here-document.test.testscript b/libbuild2/build/script/parser+here-document.test.testscript new file mode 100644 index 0000000..f56a5e1 --- /dev/null +++ b/libbuild2/build/script/parser+here-document.test.testscript @@ -0,0 +1,272 @@ +# file : libbuild2/build/script/parser+here-document.test.testscript +# license : MIT; see accompanying LICENSE file + +: end-marker +: +{ + : missing-newline + : + $* <'cmd <<=' 2>>EOE != 0 + buildfile:11:8: error: expected here-document end marker + EOE + + : missing-newline-alias + : + $* <'cmd <<' 2>>EOE != 0 + buildfile:11:7: error: expected here-document end marker + EOE + + : missing-exit + : + $* <'cmd <<= != 0' 2>>EOE != 0 + buildfile:11:9: error: expected here-document end marker + EOE + + : missing-exit-alias + : + $* <'cmd << != 0' 2>>EOE != 0 + buildfile:11:8: error: expected here-document end marker + EOE + + : missing-empty + : + $* <'cmd <<=""' 2>>EOE != 0 + buildfile:11:8: error: expected here-document end marker + EOE + + : missing-empty-alias + : + $* <'cmd <<""' 2>>EOE != 0 + buildfile:11:7: error: expected here-document end marker + EOE + + : unseparated-expansion + : + $* <'cmd <<=FOO$foo' 2>>EOE != 0 + buildfile:11:11: error: here-document end marker must be literal + EOE + + : unseparated-expansion-alias + : + $* <'cmd <<FOO$foo' 2>>EOE != 0 + buildfile:11:10: error: here-document end marker must be literal + EOE + + : quoted-single-partial + : + $* <"cmd <<=F'O'O" 2>>EOE != 0 + buildfile:11:8: error: partially-quoted here-document end marker + EOE + + : quoted-double-partial + : + $* <'cmd <<="FO"O' 2>>EOE != 0 + buildfile:11:8: error: partially-quoted here-document end marker + EOE + + : quoted-mixed + : + $* <"cmd <<=\"FO\"'O'" 2>>EOE != 0 + buildfile:11:8: error: partially-quoted here-document end marker + EOE + + : unseparated + : + $* <<EOI >>EOO + cmd <<=EOF!=0 + foo + EOF + EOI + cmd <<=EOF != 0 + foo + EOF + EOO + + : unseparated-alias + : + $* <<EOI >>EOO + cmd <<EOF!=0 + foo + EOF + EOI + cmd <<EOF != 0 + foo + EOF + EOO + + : quoted-single + : + $* <<EOI >>EOO + cmd <<='EOF' + foo + EOF + EOI + cmd <<=EOF + foo + EOF + EOO + + : quoted-single-alias + : + $* <<EOI >>EOO + cmd <<'EOF' + foo + EOF + EOI + cmd <<EOF + foo + EOF + EOO + + : quoted-double + : + $* <<EOI >>EOO + cmd <<="EOF" + foo + EOF + EOI + cmd <<=EOF + foo + EOF + EOO + + : quoted-double-alias + : + $* <<EOI >>EOO + cmd <<"EOF" + foo + EOF + EOI + cmd <<EOF + foo + EOF + EOO +} + +: indent +: +{ + : basic + : + $* <<EOI >>EOO + cmd <<=EOF + foo + bar + baz + EOF + EOI + cmd <<=EOF + foo + bar + baz + EOF + EOO + + : blank + : + $* <<EOI >>EOO + cmd <<=EOF + foo + + + bar + EOF + EOI + cmd <<=EOF + foo + + + bar + EOF + EOO + + : non-ws-prefix + : + $* <<EOI >>EOO + cmd <<=EOF + x EOF + EOF + EOI + cmd <<=EOF + x EOF + EOF + EOO + + : whole-token + : Test the case where the indentation is a whole token + : + $* <<EOI >>EOO + x = foo bar + cmd <<="EOF" + $x + EOF + EOI + cmd <<=EOF + foo bar + EOF + EOO + + : long-line + : Test the case where the line contains multiple tokens + : + $* <<EOI >>EOO + x = foo + cmd <<="EOF" + $x bar $x + EOF + EOI + cmd <<=EOF + foo bar foo + EOF + EOO + + : unindented + : + $* <<EOI 2>>EOE != 0 + cmd <<=EOF + bar + EOF + EOI + buildfile:12:1: error: unindented here-document line + EOE +} + +: blank +: +$* <<EOI >>EOO +cmd <<=EOF + +foo + +bar + +EOF +EOI +cmd <<=EOF + +foo + +bar + +EOF +EOO + +: quote +: +: Note: they are still recognized in eval contexts. +: +$* <<EOI >>EOO +cmd <<="EOF" +'single' +"double" +b'o't"h" +('single' "double") +EOF +EOI +cmd <<=EOF +'single' +"double" +b'o't"h" +single double +EOF +EOO diff --git a/libbuild2/build/script/parser+here-string.test.testscript b/libbuild2/build/script/parser+here-string.test.testscript new file mode 100644 index 0000000..f857c57 --- /dev/null +++ b/libbuild2/build/script/parser+here-string.test.testscript @@ -0,0 +1,34 @@ +# file : libbuild2/build/script/parser+here-string.test.testscript +# license : MIT; see accompanying LICENSE file + +: empty +: +$* <<EOI >>EOO +cmd <<<="" +EOI +cmd <<<='' +EOO + +: empty-nn +: +$* <<EOI >>EOO +cmd <<<=:"" +EOI +cmd <<<=:'' +EOO + +: empty-alias +: +$* <<EOI >>EOO +cmd <<<"" +EOI +cmd <<<'' +EOO + +: empty-nn-alias +: +$* <<EOI >>EOO +cmd <<<:"" +EOI +cmd <<<:'' +EOO diff --git a/libbuild2/build/script/parser+line.test.testscript b/libbuild2/build/script/parser+line.test.testscript new file mode 100644 index 0000000..6401d91 --- /dev/null +++ b/libbuild2/build/script/parser+line.test.testscript @@ -0,0 +1,72 @@ +# file : libbuild2/build/script/parser+line.test.testscript +# license : MIT; see accompanying LICENSE file + +test.options += -d + +: command +: +$* <<EOF >>EOF + foo >| 2>- &a &?b + foo >=c 2>~/error:.*/ &!c + foo >>:/~%EOS% + %.* + abc + %xyz.*% + EOS + EOF + +: if-else +: +$* <<EOF >>EOF + if foo + bar + elif fox + if fix + baz + end + biz + end + if! foo + bar + elif! fox + baz + end + EOF + +: quoting +: +$* <<EOI >>EOO + foo 'bar' "baz" '' "" + "$foo" + "foo$" + "fo"o + "foo"\" + "foo\\" + "foo\"<" + fo\"o + fo\\o + fo\<o + "fo<o" + 'fo\"o' + f"oo" "ba"r + f"oo" 'ba'r + "fo"'o' + 'foo b"ar baz' + EOI + foo 'bar' "baz" '' "" + "$foo" + "foo$" + "foo" + "foo\"" + "foo\\" + "foo\"<" + fo\"o + fo\\o + fo\<o + "fo<o" + 'fo\"o' + "foo bar" + "foo" 'bar' + "foo" + 'foo b"ar baz' + EOO diff --git a/libbuild2/build/script/parser+pipe-expr.test.testscript b/libbuild2/build/script/parser+pipe-expr.test.testscript new file mode 100644 index 0000000..a6ca12e --- /dev/null +++ b/libbuild2/build/script/parser+pipe-expr.test.testscript @@ -0,0 +1,132 @@ +# file : libbuild2/build/script/parser+pipe-expr.test.testscript +# license : MIT; see accompanying LICENSE file + +: pipe +: +$* <<EOI >>EOO +cmd1 | cmd2|cmd3 +EOI +cmd1 | cmd2 | cmd3 +EOO + +: log +: +$* <<EOI >>EOO +cmd1 || cmd2&&cmd3 +EOI +cmd1 || cmd2 && cmd3 +EOO + +: pipe-log +: +$* <<EOI >>EOO +cmd1 | cmd2 && cmd3 | cmd4 +EOI +cmd1 | cmd2 && cmd3 | cmd4 +EOO + +: exit +: +$* <<EOI >>EOO +cmd1|cmd2==1&&cmd3!=0|cmd4 +EOI +cmd1 | cmd2 == 1 && cmd3 != 0 | cmd4 +EOO + +: here-doc +: +$* <<EOI >>EOO +cmd1 <<=EOI1 | cmd2 >>?EOO2 && cmd3 <<=EOI3 2>&1 | cmd4 2>>?EOE4 >>?EOO4 +input +one +EOI1 +ouput +two +EOO2 +input +three +EOI3 +error +four +EOE4 +output +four +EOO4 +EOI +cmd1 <<=EOI1 | cmd2 >>?EOO2 && cmd3 <<=EOI3 2>&1 | cmd4 >>?EOO4 2>>?EOE4 +input +one +EOI1 +ouput +two +EOO2 +input +three +EOI3 +output +four +EOO4 +error +four +EOE4 +EOO + +: leading +: +$* <<EOI 2>>EOE != 0 +| cmd +EOI +buildfile:11:1: error: missing program +EOE + +: trailing +: +$* <<EOI 2>>EOE != 0 +cmd && +EOI +buildfile:11:7: error: missing program +EOE + +: redirected +: +{ + : input + : + { + : first + : + $* <<EOI >>EOO + cmd1 <foo | cmd2 + EOI + cmd1 <foo | cmd2 + EOO + + : non-first + : + $* <<EOI 2>>EOE != 0 + cmd1 | cmd2 <foo + EOI + buildfile:11:13: error: stdin is both piped and redirected + EOE + } + + : output + : + { + : last + : + $* <<EOI >>EOO + cmd1 | cmd2 >foo + EOI + cmd1 | cmd2 >foo + EOO + + : non-last + : + $* <<EOI 2>>EOE != 0 + cmd1 >foo | cmd2 + EOI + buildfile:11:11: error: stdout is both redirected and piped + EOE + } +} diff --git a/libbuild2/build/script/parser+pre-parse.test.testscript b/libbuild2/build/script/parser+pre-parse.test.testscript new file mode 100644 index 0000000..4aff3e8 --- /dev/null +++ b/libbuild2/build/script/parser+pre-parse.test.testscript @@ -0,0 +1,22 @@ +# file : libbuild2/build/script/parser+pre-parse.test.testscript +# license : MIT; see accompanying LICENSE file + +: attribute +: +{ + : name + : + $* <<EOI 2>>EOE != 0 + x = [foo] + EOI + buildfile:11:5: error: unknown value attribute foo + EOE + + : name-value + : + $* <<EOI 2>>EOE != 0 + x = [foo=bar] + EOI + buildfile:11:5: error: unknown value attribute foo=bar + EOE +} diff --git a/libbuild2/build/script/parser+redirect.test.testscript b/libbuild2/build/script/parser+redirect.test.testscript new file mode 100644 index 0000000..82c04ea --- /dev/null +++ b/libbuild2/build/script/parser+redirect.test.testscript @@ -0,0 +1,525 @@ +# file : libbuild2/build/script/parser+redirect.test.testscript +# license : MIT; see accompanying LICENSE file + +# @@ Add tests for redirects other than trace, here-*, file and merge. +# @@ Does it make sense to split into separate files - one per redirect type? +# + +: trace +: +{ + $* <'cmd >!' >'cmd >!' : out + $* <'cmd 2>!' >'cmd 2>!' : err +} + +: str +: +{ + : literal + : + { + : portable-path + : + $* <<EOI >>EOO + cmd <<<=/foo >>>?/bar 2>>>?/baz + EOI + cmd <<<=/foo >>>?/bar 2>>>?/baz + EOO + } + + : regex + : + { + : portable-path + : + $* <<EOI >>EOO + cmd >>>?/~%foo% 2>>>?/~%bar% + EOI + cmd >>>?/~%foo% 2>>>?/~%bar% + EOO + } +} + +: doc +: +{ + : literal + : + { + : portable-path + : + $* <<EOI >>EOO + cmd <<=/EOI_ >>?/EOO_ 2>>?/EOE_ + foo + EOI_ + bar + EOO_ + baz + EOE_ + EOI + cmd <<=/EOI_ >>?/EOO_ 2>>?/EOE_ + foo + EOI_ + bar + EOO_ + baz + EOE_ + EOO + + : sharing + : + { + : in-out + : + $* <<EOI >>EOO + cmd <<=:/EOF >>?:/EOF + foo + EOF + EOI + cmd <<=:/EOF >>?:/EOF + foo + EOF + EOO + + : in-alias-out + : + $* <<EOI >>EOO + cmd <<:/EOF >>?:/EOF + foo + EOF + EOI + cmd <<:/EOF >>?:/EOF + foo + EOF + EOO + + : out-in-alias + : + $* <<EOI >>EOO + cmd >>?:/EOF <<:/EOF + foo + EOF + EOI + cmd <<:/EOF >>?:/EOF + foo + EOF + EOO + + : different + : + { + : modifiers + : + $* <<EOI 2>>EOE != 0 + cmd <<=:/EOF >>?:EOF + foo + EOF + EOI + buildfile:11:18: error: different modifiers for shared here-document 'EOF' + EOE + + : quoting + : + $* <<EOI 2>>EOE != 0 + cmd <<=EOF >>?"EOF" + foo + EOF + EOI + buildfile:11:15: error: different quoting for shared here-document 'EOF' + EOE + } + } + } + + : regex + : + { + : portable-path + : + $* <<EOI >>EOO + cmd >>?/~%EOF% 2>>?/~%EOE% + foo + EOF + bar + EOE + EOI + cmd >>?/~%EOF% 2>>?/~%EOE% + foo + EOF + bar + EOE + EOO + + : sharing + : + { + : in-out + : + $* <<EOI >>EOO + cmd >>?~/EOF/ 2>>?~/EOF/ + foo + EOF + EOI + cmd >>?~/EOF/ 2>>?~/EOF/ + foo + EOF + EOO + + : different + : + { + : introducers + : + $* <<EOI 2>>EOE != 0 + cmd >>?~/EOF/ 2>>?~%EOF% + foo + EOF + EOI + buildfile:11:20: error: different introducers for shared here-document regex 'EOF' + EOE + + : flags + : + $* <<EOI 2>>EOE != 0 + cmd >>?~/EOF/ 2>>?~/EOF/i + foo + EOF + EOI + buildfile:11:20: error: different global flags for shared here-document regex 'EOF' + EOE + } + } + } + + : overriding + : + { + : literal + : + { + : with + : + { + : string + : + $* <<EOI >>EOO + cmd >>?EOF >>>?bar + foo + EOF + EOI + cmd >>>?bar + EOO + + : regex + : + $* <<EOI >>EOO + cmd >>?FOO >>?~/BAR/ + foo + FOO + bar + BAR + EOI + cmd >>?~/BAR/ + bar + BAR + EOO + + : self + : + $* <<EOI >>EOO + cmd >>EOF >>EOF + foo + EOF + EOI + cmd >>EOF + foo + EOF + EOO + + : different-modifiers + : + $* <<EOI 2>>EOE != 0 + cmd >>?EOF >>?/EOF + foo + EOF + EOI + buildfile:11:16: error: different modifiers for shared here-document 'EOF' + EOE + } + } + + : shared + : + { + : after-sharing + : + $* <<EOI >>EOO + cmd >>EOF 2>>EOF >bar + foo + EOF + EOI + cmd >bar 2>>EOF + foo + EOF + EOO + + : before-sharing + : + $* <<EOI >>EOO + cmd >>EOF >bar 2>>EOF + foo + EOF + EOI + cmd >bar 2>>EOF + foo + EOF + EOO + } + } +} + +: file +: +{ + : cmp + : + $* <<EOI >>EOO + cmd 0<=a 1>?b 2>?c + EOI + cmd <=a >?b 2>?c + EOO + + : write + : + $* <<EOI >>EOO + cmd 1>=b 2>+c + EOI + cmd >=b 2>+c + EOO + + : quote + : + $* <<EOI >>EOO + cmd 0<="a f" 1>="b f" 2>+"c f" + EOI + cmd <='a f' >='b f' 2>+'c f' + EOO + + : in + : + { + : missed + : + $* <<EOI 2>>EOE !=0 + cmd <= + EOI + buildfile:11:7: error: missing stdin file + EOE + + : empty + : + $* <<EOI 2>>EOE !=0 + cmd <="" + EOI + buildfile:11:7: error: empty stdin redirect path + EOE + } + + : in-alias + : + { + : missed + : + $* <<EOI 2>>EOE !=0 + cmd < + EOI + buildfile:11:6: error: missing stdin file + EOE + + : empty + : + $* <<EOI 2>>EOE !=0 + cmd <"" + EOI + buildfile:11:6: error: empty stdin redirect path + EOE + } + + : out + : + { + : missed + : + $* <<EOI 2>>EOE !=0 + cmd >= + EOI + buildfile:11:7: error: missing stdout file + EOE + + : empty + : + $* <<EOI 2>>EOE !=0 + cmd >="" + EOI + buildfile:11:7: error: empty stdout redirect path + EOE + } + + : out-alias + : + { + : missed + : + $* <<EOI 2>>EOE !=0 + cmd > + EOI + buildfile:11:6: error: missing stdout file + EOE + + : empty + : + $* <<EOI 2>>EOE !=0 + cmd >"" + EOI + buildfile:11:6: error: empty stdout redirect path + EOE + } + + : err + : + { + : missed + : + $* <<EOI 2>>EOE !=0 + cmd 2>= + EOI + buildfile:11:8: error: missing stderr file + EOE + + : empty + : + $* <<EOI 2>>EOE !=0 + cmd 2>="" + EOI + buildfile:11:8: error: empty stderr redirect path + EOE + } + + : err-alias + : + { + : missed + : + $* <<EOI 2>>EOE !=0 + cmd 2> + EOI + buildfile:11:7: error: missing stderr file + EOE + + : empty + : + $* <<EOI 2>>EOE !=0 + cmd 2>"" + EOI + buildfile:11:7: error: empty stderr redirect path + EOE + } +} + +: merge +{ + : out + : + { + : err + : + $* <<EOI >>EOO + cmd 1>&2 + EOI + cmd >&2 + EOO + + : no-mutual + : + $* <<EOI >>EOO + cmd 1>&2 2>&1 2>a + EOI + cmd >&2 2>a + EOO + + : not-descriptor + : + $* <<EOI 2>>EOE != 0 + cmd 1>&a + EOI + buildfile:11:8: error: stdout merge redirect file descriptor must be 2 + EOE + + : self + : + $* <<EOI 2>>EOE != 0 + cmd 1>&1 + EOI + buildfile:11:8: error: stdout merge redirect file descriptor must be 2 + EOE + + : missed + : + $* <<EOI 2>>EOE != 0 + cmd 1>& + EOI + buildfile:11:8: error: missing stdout file descriptor + EOE + } + + : err + { + : out + : + $* <<EOI >>EOO + cmd 2>&1 + EOI + cmd 2>&1 + EOO + + : no-mutual + : + $* <<EOI >>EOO + cmd 1>&2 2>&1 >a + EOI + cmd >a 2>&1 + EOO + + : not-descriptor + : + $* <<EOI 2>>EOE != 0 + cmd 2>&a + EOI + buildfile:11:8: error: stderr merge redirect file descriptor must be 1 + EOE + + : self + : + $* <<EOI 2>>EOE != 0 + cmd 2>&2 + EOI + buildfile:11:8: error: stderr merge redirect file descriptor must be 1 + EOE + + : missed + : + $* <<EOI 2>>EOE != 0 + cmd 2>& + EOI + buildfile:11:8: error: missing stderr file descriptor + EOE + } + + : mutual + : + $* <<EOI 2>>EOE != 0 + cmd 1>&2 2>&1 + EOI + buildfile:11:14: error: stdout and stderr redirected to each other + EOE +} diff --git a/libbuild2/build/script/parser+regex.test.testscript b/libbuild2/build/script/parser+regex.test.testscript new file mode 100644 index 0000000..625bfdf --- /dev/null +++ b/libbuild2/build/script/parser+regex.test.testscript @@ -0,0 +1,225 @@ +# file : libbuild2/build/script/parser+regex.test.testscript +# license : MIT; see accompanying LICENSE file + +: here-string +: +{ + : stdout + : + { + : missed + : + $* <'cmd >>>?~' 2>>EOE != 0 + buildfile:11:10: error: missing stdout here-string regex + EOE + + : no-introducer + : + $* <'cmd >>>?~""' 2>>EOE != 0 + buildfile:11:10: error: no introducer character in stdout regex redirect + EOE + + : no-term-introducer + : + $* <'cmd >>>?~/' 2>>EOE != 0 + buildfile:11:10: error: no closing introducer character in stdout regex redirect + EOE + + : portable-path-introducer + : + $* <'cmd >>>?/~/foo/' 2>>EOE != 0 + buildfile:11:11: error: portable path modifier and '/' introducer in stdout regex redirect + EOE + + : empty + : + $* <'cmd >>>?~//' 2>>EOE != 0 + buildfile:11:10: error: stdout regex redirect is empty + EOE + + : no-flags + : + $* <'cmd >>>?~/fo*/' >'cmd >>>?~/fo*/' + + : idot + : + $* <'cmd >>>?~/fo*/d' >'cmd >>>?~/fo*/d' + + : icase + : + $* <'cmd >>>?~/fo*/i' >'cmd >>>?~/fo*/i' + + : invalid-flags1 + : + $* <'cmd >>>?~/foo/z' 2>>EOE != 0 + buildfile:11:10: error: junk at the end of stdout regex redirect + EOE + + : invalid-flags2 + : + $* <'cmd >>>?~/foo/iz' 2>>EOE != 0 + buildfile:11:10: error: junk at the end of stdout regex redirect + EOE + + : no-newline + : + $* <'cmd >>>?:~/fo*/' >'cmd >>>?:~/fo*/' + } + + : stderr + : + { + : missed + : + $* <'cmd 2>>>?~' 2>>EOE != 0 + buildfile:11:11: error: missing stderr here-string regex + EOE + + : no-introducer + : + : Note that there is no need to reproduce all the errors as for stdout. + : All we need is to make sure that the proper description is passed to + : the parse_regex() function. + : + $* <'cmd 2>>>?~""' 2>>EOE != 0 + buildfile:11:11: error: no introducer character in stderr regex redirect + EOE + } + + : modifier-last + : + $* <'cmd >>>?~/x' 2>>EOE != 0 + buildfile:11:10: error: no closing introducer character in stdout regex redirect + EOE +} + +: here-doc +: +{ + : stdout + : + { + : missed + : + $* <'cmd >>?~' 2>>EOE != 0 + buildfile:11:9: error: expected here-document regex end marker + EOE + + : portable-path-introducer + : + $* <<EOI 2>>EOE != 0 + cmd >>?/~/EOO/ + foo + EOO + EOI + buildfile:11:5: error: portable path modifier and '/' introducer in here-document regex end marker + EOE + + : unterminated-line-char + : + $* <<EOI 2>>EOE != 0 + cmd >>?~/EOO/ + / + EOO + EOI + buildfile:12:1: error: no syntax line characters + EOE + + : empty + : + $* <<EOI 2>>EOE != 0 + cmd >>?:~/EOO/ + EOO + EOI + buildfile:12:1: error: empty here-document regex + EOE + + : no-flags + : + $* <<EOI >>EOO + cmd 2>>?~/EOE/ + foo + /? + /foo/ + /foo/* + /foo/i + /foo/i* + + // + //* + EOE + EOI + cmd 2>>?~/EOE/ + foo + /? + /foo/ + /foo/* + /foo/i + /foo/i* + + // + //* + EOE + EOO + + : no-newline-str + : + $* <'cmd >>>?:~/fo*/' >'cmd >>>?:~/fo*/' + + : no-newline-doc + : + $* <<EOI >>EOO + cmd 2>>?:~/EOE/ + foo + EOE + EOI + cmd 2>>?:~/EOE/ + foo + EOE + EOO + + : end-marker-restore + : + { + : idot + : + $* <<EOI >>EOO + cmd 2>>?~/EOE/d + foo + EOE + EOI + cmd 2>>?~/EOE/d + foo + EOE + EOO + + : icase + : + $* <<EOI >>EOO + cmd 2>>?~/EOE/i + foo + EOE + EOI + cmd 2>>?~/EOE/i + foo + EOE + EOO + } + } + + : stderr + : + { + : missed + : + $* <'cmd 2>>?~' 2>>EOE != 0 + buildfile:11:10: error: expected here-document regex end marker + EOE + } + + : modifier-last + : + $* <'cmd >>?~:/FOO/' 2>>EOE != 0 + buildfile:11:5: error: no closing introducer character in here-document regex end marker + EOE +} diff --git a/libbuild2/build/script/parser+variable.test.testscript b/libbuild2/build/script/parser+variable.test.testscript new file mode 100644 index 0000000..5040e66 --- /dev/null +++ b/libbuild2/build/script/parser+variable.test.testscript @@ -0,0 +1,41 @@ +# file : libbuild2/build/script/parser+variable.test.testscript +# license : MIT; see accompanying LICENSE file + +: assignment +: +$* <<EOI >>EOO +a = b +echo $a +EOI +echo b +EOO + +: primary-target +: +$* <<EOI >>EOO +echo $name($>) +EOI +echo driver +EOO + +: no-newline +: +$* <:'echo a' 2>>EOE != 0 +buildfile:11:7: error: expected newline instead of <end of file> +EOE + +: set-primary-target +: +$* <<EOI 2>>EOE != 0 +> = a +EOI +buildfile:11:1: error: missing program +EOE + +: empty-name +: +$* <<EOI 2>>EOE != 0 += b +EOI +buildfile:11:1: error: missing variable name +EOE diff --git a/libbuild2/build/script/parser.cxx b/libbuild2/build/script/parser.cxx new file mode 100644 index 0000000..e64db91 --- /dev/null +++ b/libbuild2/build/script/parser.cxx @@ -0,0 +1,391 @@ +// file : libbuild2/build/script/parser.cxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#include <libbuild2/build/script/parser.hxx> + +#include <libbuild2/build/script/lexer.hxx> +#include <libbuild2/build/script/runner.hxx> + +using namespace std; + +namespace build2 +{ + namespace build + { + namespace script + { + using type = token_type; + + // + // Pre-parse. + // + + script parser:: + pre_parse (istream& is, const path_name& pn, uint64_t line) + { + path_ = &pn; + + pre_parse_ = true; + + lexer l (is, *path_, line, lexer_mode::command_line); + set_lexer (&l); + + script s; + script_ = &s; + runner_ = nullptr; + environment_ = nullptr; + + s.start_loc = location (*path_, line, 1); + + token t (pre_parse_script ()); + + assert (t.type == type::eos); + + s.end_loc = get_location (t); + + return s; + } + + token parser:: + pre_parse_script () + { + // enter: next token is first token of the script + // leave: eos (returned) + + token t; + type tt; + + // Parse lines until we see eos. + // + for (;;) + { + // Start lexing each line. + // + tt = peek (lexer_mode::first_token); + + // Determine the line type by peeking at the first token. + // + switch (tt) + { + case type::eos: + { + next (t, tt); + return t; + } + default: + { + pre_parse_line (t, tt); + assert (tt == type::newline); + break; + } + } + } + } + + void parser:: + pre_parse_line (token& t, type& tt, bool if_line) + { + // Determine the line type/start token. + // + line_type lt ( + pre_parse_line_start (t, tt, lexer_mode::second_token)); + + line ln; + switch (lt) + { + case line_type::var: + { + // Check if we are trying to modify any of the special variables. + // + if (special_variable (t.value)) + fail (t) << "attempt to set '" << t.value << "' special " + << "variable"; + + // We don't pre-enter variables. + // + ln.var = nullptr; + + next (t, tt); // Assignment kind. + + mode (lexer_mode::variable_line); + parse_variable_line (t, tt); + + if (tt != type::newline) + fail (t) << "expected newline instead of " << t; + + break; + } + case line_type::cmd_elif: + case line_type::cmd_elifn: + case line_type::cmd_else: + case line_type::cmd_end: + { + if (!if_line) + { + fail (t) << lt << " without preceding 'if'"; + } + } + // Fall through. + case line_type::cmd_if: + case line_type::cmd_ifn: + next (t, tt); // Skip to start of command. + // Fall through. + case line_type::cmd: + { + pair<command_expr, here_docs> p; + + if (lt != line_type::cmd_else && lt != line_type::cmd_end) + p = parse_command_expr (t, tt, lexer::redirect_aliases); + + if (tt != type::newline) + fail (t) << "expected newline instead of " << t; + + parse_here_documents (t, tt, p); + break; + } + } + + assert (tt == type::newline); + + ln.type = lt; + ln.tokens = replay_data (); + script_->lines.push_back (move (ln)); + + if (lt == line_type::cmd_if || lt == line_type::cmd_ifn) + { + tt = peek (lexer_mode::first_token); + + pre_parse_if_else (t, tt); + } + } + + void parser:: + pre_parse_if_else (token& t, type& tt) + { + // 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. + // + for (line_type bt (line_type::cmd_if); // Current block. + ; + tt = peek (lexer_mode::first_token)) + { + 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. + // + size_t i (script_->lines.size ()); + + pre_parse_line (t, tt, true /* if_line */); + assert (tt == type::newline); + + line_type lt (script_->lines[i].type); + + // First take care of 'end'. + // + if (lt == line_type::cmd_end) + return; + + // Check if-else block sequencing. + // + if (bt == line_type::cmd_else) + { + if (lt == line_type::cmd_else || + lt == line_type::cmd_elif || + lt == line_type::cmd_elifn) + fail (ll) << lt << " after " << bt; + } + + // Update current if-else block. + // + switch (lt) + { + case line_type::cmd_elif: + case line_type::cmd_elifn: bt = line_type::cmd_elif; break; + case line_type::cmd_else: bt = line_type::cmd_else; break; + default: break; + } + } + } + + command_expr parser:: + parse_command_line (token& t, type& tt) + { + // enter: first token of the command line + // leave: <newline> + + // Note: this one is only used during execution. + // + assert (!pre_parse_); + + pair<command_expr, here_docs> p ( + parse_command_expr (t, tt, lexer::redirect_aliases)); + + assert (tt == type::newline); + + parse_here_documents (t, tt, p); + assert (tt == type::newline); + + return move (p.first); + } + + // + // Execute. + // + + void parser:: + execute (const scope& rs, const scope& bs, + environment& e, const script& s, runner& r) + { + path_ = nullptr; // Set by replays. + + pre_parse_ = false; + + set_lexer (nullptr); + + // The script shouldn't be able to modify the scopes. + // + root_ = const_cast<scope*> (&rs); + scope_ = const_cast<scope*> (&bs); + pbase_ = scope_->src_path_; + + script_ = const_cast<script*> (&s); + runner_ = &r; + environment_ = &e; + + exec_script (); + } + + void parser:: + exec_script () + { + const script& s (*script_); + + runner_->enter (*environment_, s.start_loc); + + // Note that we rely on "small function object" optimization for the + // exec_*() lambdas. + // + auto exec_set = [this] (const variable& var, + token& t, build2::script::token_type& tt, + const location&) + { + next (t, tt); + type kind (tt); // Assignment kind. + + mode (lexer_mode::variable_line); + value rhs (parse_variable_line (t, tt)); + + assert (tt == type::newline); + + // Assign. + // + value& lhs (kind == type::assign + ? environment_->assign (var) + : environment_->append (var)); + + apply_value_attributes (&var, lhs, move (rhs), kind); + }; + + auto exec_cmd = [this] (token& t, build2::script::token_type& tt, + size_t li, + bool single, + const location& ll) + { + // We use the 0 index to signal that this is the only command. + // + if (single) + li = 0; + + command_expr ce ( + parse_command_line (t, static_cast<token_type&> (tt))); + + runner_->run (*environment_, ce, li, ll); + }; + + auto exec_if = [this] (token& t, build2::script::token_type& tt, + size_t li, + const location& ll) + { + command_expr ce ( + parse_command_line (t, static_cast<token_type&> (tt))); + + // Assume if-else always involves multiple commands. + // + return runner_->run_if (*environment_, ce, li, ll); + }; + + size_t li (1); + + exec_lines (s.lines.begin (), s.lines.end (), + exec_set, exec_cmd, exec_if, + li, + &environment_->var_pool); + + runner_->leave (*environment_, s.end_loc); + } + + // When add a special variable don't forget to update lexer::word(). + // + bool parser:: + special_variable (const string& n) noexcept + { + return n == ">" || n == "<" || n == "~"; + } + + lookup parser:: + lookup_variable (name&& qual, string&& name, const location& loc) + { + // In the pre-parse mode collect the referenced variable names for the + // script semantics change tracking. + // + if (pre_parse_) + { + // Add the variable name skipping special variables and suppressing + // duplicates. While at it, check if the script temporary directory + // is referenced and set the flag, if that's the case. + // + if (special_variable (name)) + { + if (name == "~") + script_->temp_dir = true; + } + else if (!name.empty ()) + { + auto& vars (script_->vars); + + if (find (vars.begin (), vars.end (), name) == vars.end ()) + vars.push_back (move (name)); + } + + return lookup (); + } + + if (!qual.empty ()) + fail (loc) << "qualified variable name"; + + lookup r (environment_->lookup (name)); + + // Fail if non-script-local variable with an untracked name. + // + if (r.defined () && !r.belongs (*environment_)) + { + const auto& vars (script_->vars); + + if (find (vars.begin (), vars.end (), name) == vars.end ()) + fail (loc) << "use of untracked variable '" << name << "'"; + } + + return r; + } + } + } +} diff --git a/libbuild2/build/script/parser.hxx b/libbuild2/build/script/parser.hxx new file mode 100644 index 0000000..27e7f49 --- /dev/null +++ b/libbuild2/build/script/parser.hxx @@ -0,0 +1,96 @@ +// file : libbuild2/build/script/parser.hxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#ifndef LIBBUILD2_BUILD_SCRIPT_PARSER_HXX +#define LIBBUILD2_BUILD_SCRIPT_PARSER_HXX + +#include <libbuild2/types.hxx> +#include <libbuild2/forward.hxx> +#include <libbuild2/utility.hxx> + +#include <libbuild2/diagnostics.hxx> + +#include <libbuild2/script/parser.hxx> + +#include <libbuild2/build/script/token.hxx> +#include <libbuild2/build/script/script.hxx> + +namespace build2 +{ + namespace build + { + namespace script + { + class runner; + + class parser: public build2::script::parser + { + // Pre-parse. Issue diagnostics and throw failed in case of an error. + // + public: + parser (context& c): build2::script::parser (c) {} + + // Note that the returned script object references the passed path + // name. + // + script + pre_parse (istream&, const path_name&, uint64_t line); + + // Recursive descent parser. + // + // Usually (but not always) parse functions receive the token/type + // from which it should start consuming and in return the token/type + // should contain the first token that has not been consumed. + // + // Functions that are called parse_*() rather than pre_parse_*() are + // used for both stages. + // + protected: + token + pre_parse_script (); + + void + pre_parse_line (token&, token_type&, bool if_line = false); + + void + pre_parse_if_else (token&, token_type&); + + command_expr + parse_command_line (token&, token_type&); + + // Execute. Issue diagnostics and throw failed in case of an error. + // + public: + void + execute (const scope& root, const scope& base, + environment&, const script&, runner&); + + protected: + void + exec_script (); + + // Helpers. + // + public: + static bool + special_variable (const string&) noexcept; + + // Customization hooks. + // + protected: + virtual lookup + lookup_variable (name&&, string&&, const location&) override; + + protected: + script* script_; + + // Execute state. + // + runner* runner_; + environment* environment_; + }; + } + } +} + +#endif // LIBBUILD2_BUILD_SCRIPT_PARSER_HXX diff --git a/libbuild2/build/script/parser.test.cxx b/libbuild2/build/script/parser.test.cxx new file mode 100644 index 0000000..9046312 --- /dev/null +++ b/libbuild2/build/script/parser.test.cxx @@ -0,0 +1,224 @@ +// file : libbuild2/build/script/parser.test.cxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#include <cassert> +#include <iostream> + +#include <libbuild2/types.hxx> +#include <libbuild2/utility.hxx> + +#include <libbuild2/target.hxx> +#include <libbuild2/context.hxx> +#include <libbuild2/scheduler.hxx> + +#include <libbuild2/build/script/script.hxx> // line +#include <libbuild2/build/script/parser.hxx> +#include <libbuild2/build/script/runner.hxx> + +using namespace std; + +namespace build2 +{ + namespace build + { + namespace script + { + class print_runner: public runner + { + public: + print_runner (bool line): line_ (line) {} + + virtual void + enter (environment&, const location&) override {} + + virtual void + run (environment&, + const command_expr& e, + size_t i, + const location&) override + { + cout << e; + + if (line_) + cout << " # " << i; + + cout << endl; + } + + virtual bool + run_if (environment&, + const command_expr& e, + size_t i, + const location&) override + { + cout << "? " << e; + + if (line_) + cout << " # " << i; + + cout << endl; + + return e.back ().pipe.back ().program.string () == "true"; + } + + virtual void + leave (environment&, const location&) override {} + + private: + bool line_; + }; + + // Usages: + // + // argv[0] [-l] + // argv[0] -d + // argv[0] -p + // + // In the first form read the script from stdin and trace the script + // execution to stdout using the custom print runner. + // + // In the second form read the script from stdin, parse it and dump the + // resulting lines to stdout. + // + // In the third form read the script from stdin, parse it and print + // line tokens quoting information to stdout. + // + // -l + // Print the script line number for each executed expression. + // + // -d + // Dump the parsed script to sdout. + // + // -p + // Print the parsed script tokens quoting information to sdout. If a + // token is quoted follow its representation with its quoting + // information in the [<quoting>/<completeness>] form, where: + // + // <quoting> := 'S' | 'D' | 'M' + // <completeness> := 'C' | 'P' + // + int + main (int argc, char* argv[]) + { + tracer trace ("main"); + + enum class mode + { + run, + dump, + print + } m (mode::run); + + bool print_line (false); + + for (int i (1); i != argc; ++i) + { + string a (argv[i]); + + if (a == "-l") + print_line = true; + else if (a == "-d") + m = mode::dump; + else if (a == "-p") + m = mode::print; + else + assert (false); + } + + assert (m == mode::run || !print_line); + + // Fake build system driver, default verbosity. + // + init_diag (1); + init (nullptr, argv[0]); + + // Serial execution. + // + scheduler sched (1); + global_mutexes mutexes (1); + context ctx (sched, mutexes); + + try + { + cin.exceptions (istream::failbit | istream::badbit); + + // Enter mock target. Use fixed name and path so that we can use + // them in expected results. Strictly speaking target path should + // be absolute. However, the buildscript implementation doesn't + // really care. + // + file& tt ( + ctx.targets.insert<file> (work, + dir_path (), + "driver", + string (), + trace)); + + tt.path (path ("driver")); + + // Parse and run. + // + parser p (ctx); + path_name nm ("buildfile"); + script s (p.pre_parse (cin, nm, 11 /* line */)); + + switch (m) + { + case mode::run: + { + environment e (perform_update_id, tt, false /* temp_dir */); + print_runner r (print_line); + p.execute (ctx.global_scope, ctx.global_scope, e, s, r); + break; + } + case mode::dump: + { + dump (cout, "", s.lines); + break; + } + case mode::print: + { + for (const line& l: s.lines) + { + for (const replay_token& rt: l.tokens) + { + if (&rt != &l.tokens[0]) + cout << ' '; + + const token& t (rt.token); + cout << t; + + char q ('\0'); + switch (t.qtype) + { + case quote_type::single: q = 'S'; break; + case quote_type::double_: q = 'D'; break; + case quote_type::mixed: q = 'M'; break; + case quote_type::unquoted: break; + } + + if (q != '\0') + cout << " [" << q << (t.qcomp ? "/C" : "/P") << ']'; + } + } + + cout << endl; + } + } + } + catch (const failed&) + { + return 1; + } + + return 0; + } + } + } +} + +int +main (int argc, char* argv[]) +{ + return build2::build::script::main (argc, argv); +} diff --git a/libbuild2/build/script/runner.cxx b/libbuild2/build/script/runner.cxx new file mode 100644 index 0000000..315a248 --- /dev/null +++ b/libbuild2/build/script/runner.cxx @@ -0,0 +1,133 @@ +// file : libbuild2/build/script/runner.cxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#include <libbuild2/build/script/runner.hxx> + +#include <libbutl/filesystem.mxx> // try_rmdir() + +#include <libbuild2/target.hxx> +#include <libbuild2/script/run.hxx> + +using namespace butl; + +namespace build2 +{ + namespace build + { + namespace script + { + void default_runner:: + enter (environment&, const location&) + { + } + + void default_runner:: + leave (environment& env, const location& ll) + { + // Drop cleanups of target paths. + // + for (auto i (env.cleanups.begin ()); i != env.cleanups.end (); ) + { + const target* m (&env.target); + for (; m != nullptr; m = m->adhoc_member) + { + if (const path_target* pm = m->is_a<path_target> ()) + if (i->path == pm->path ()) + break; + } + + if (m != nullptr) + i = env.cleanups.erase (i); + else + ++i; + } + + clean (env, ll); + + // Remove the temporary directory, if created. + // + const dir_path& td (env.temp_dir.path); + + if (!td.empty ()) + { + // Note that since the temporary directory may only contain special + // files that are created and registered for cleanup by the script + // running machinery and should all be removed by the above clean() + // function call, its removal failure may not be the script fault + // but potentially a bug or a filesystem problem. Thus, we don't + // ignore the errors and report them. + // + env.temp_dir.cancel (); + + try + { + // Note that the temporary directory must be empty to date. + // + rmdir_status r (try_rmdir (td)); + + if (r != rmdir_status::success) + { + // While there can be no fault of the script being currently + // executed let's add the location anyway to ease the + // troubleshooting. And let's stick to that principle down the + // road. + // + diag_record dr (fail (ll)); + dr << "temporary directory '" << td + << (r == rmdir_status::not_exist + ? "' does not exist" + : "' is not empty"); + + if (r == rmdir_status::not_empty) + build2::script::print_dir (dr, td, ll); + } + } + catch (const system_error& e) + { + fail (ll) << "unable to remove temporary directory '" << td + << "': " << e; + } + + if (verb >= 3) + text << "rmdir " << td; + } + } + + void default_runner:: + run (environment& env, + const command_expr& expr, + size_t li, + const location& ll) + { + if (verb >= 3) + text << ": " << expr; + + // Run the expression if we are not in the dry-run mode or if it + // executes the set or exit builtin and just print the expression + // otherwise at verbosity level 2 and up. + // + if (!env.context.dry_run || + find_if (expr.begin (), expr.end (), + [] (const expr_term& et) + { + const string& p (et.pipe.back ().program.string ()); + return p == "set" || p == "exit"; + }) != expr.end ()) + build2::script::run (env, expr, 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) + { + if (verb >= 3) + text << ": ?" << expr; + + return build2::script::run_if (env, expr, li, ll); + } + } + } +} diff --git a/libbuild2/build/script/runner.hxx b/libbuild2/build/script/runner.hxx new file mode 100644 index 0000000..431c446 --- /dev/null +++ b/libbuild2/build/script/runner.hxx @@ -0,0 +1,84 @@ +// file : libbuild2/build/script/runner.hxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#ifndef LIBBUILD2_BUILD_SCRIPT_RUNNER_HXX +#define LIBBUILD2_BUILD_SCRIPT_RUNNER_HXX + +#include <libbuild2/types.hxx> +#include <libbuild2/utility.hxx> + +#include <libbuild2/build/script/script.hxx> + +namespace build2 +{ + namespace build + { + struct common; + + namespace script + { + class runner + { + public: + // Location is the script start location (for diagnostics, etc). + // + virtual void + enter (environment&, const location&) = 0; + + // Index is the 1-base index of this command line in the command list. + // If it is 0 then it means there is only one command. This + // information can be used, for example, to derive file names. + // + // Location is the start position of this command line in the script. + // It can be used in diagnostics. + // + virtual void + run (environment&, + const command_expr&, + size_t index, + const location&) = 0; + + virtual bool + run_if (environment&, + const command_expr&, + size_t, + const location&) = 0; + + // Location is the script end location (for diagnostics, etc). + // + virtual void + leave (environment&, const location&) = 0; + }; + + // 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 prints them at + // verbosity level 2 and up. + // + class default_runner: public runner + { + public: + virtual void + enter (environment&, const location&) override; + + virtual void + run (environment&, + const command_expr&, + size_t, + const location&) override; + + virtual bool + run_if (environment&, + const command_expr&, + size_t, + const location&) override; + + virtual void + leave (environment&, const location&) override; + }; + } + } +} + +#endif // LIBBUILD2_BUILD_SCRIPT_RUNNER_HXX diff --git a/libbuild2/build/script/script.cxx b/libbuild2/build/script/script.cxx new file mode 100644 index 0000000..3485f54 --- /dev/null +++ b/libbuild2/build/script/script.cxx @@ -0,0 +1,236 @@ +// file : libbuild2/build/script/script.cxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#include <libbuild2/build/script/script.hxx> + +#include <libbutl/filesystem.mxx> + +#include <libbuild2/target.hxx> + +#include <libbuild2/build/script/parser.hxx> + +using namespace std; + +namespace build2 +{ + namespace build + { + namespace script + { + // environment + // + static const optional<string> wd_name ("current directory"); + + environment:: + environment (action a, const target_type& t, bool temp) + : build2::script::environment ( + t.ctx, + cast<target_triplet> (t.ctx.global_scope["build.host"]), + dir_name_view (&work, &wd_name), + temp_dir.path, false /* temp_dir_keep */, + redirect (redirect_type::none), + redirect (redirect_type::merge, 2), + redirect (redirect_type::pass)), + target (t), + vars (context, false /* global */) + { + // Set special variables. + // + { + // $> + // + names ns; + for (const target_type* m (&t); m != nullptr; m = m->adhoc_member) + m->as_name (ns); + + assign (var_pool.insert (">")) = move (ns); + } + + { + // $< + // + // Note that at this stage (after execute_prerequisites()) ad hoc + // prerequisites are no longer in prerequisite_targets which means + // they won't end up in $< either. While at first thought ad hoc + // prerequisites in ad hoc recipes don't seem to make much sense, + // they could be handy to exclude certain preresquisites from $< + // while still treating them as such. + // + names ns; + for (const target_type* pt: t.prerequisite_targets[a]) + if (pt != nullptr) + pt->as_name (ns); + + assign (var_pool.insert ("<")) = move (ns); + } + + // Set the $~ special variable. + // + if (temp) + { + create_temp_dir (); + assign (var_pool.insert<dir_path> ("~")) = temp_dir.path; + } + } + + void environment:: + create_temp_dir () + { + // Create the temporary directory for this run regardless of the + // dry-run mode, since some commands still can be executed (see run() + // for details). This is also the reason why we are not using the + // build2 filesystem API that considers the dry-run mode. + // + // Note that the directory auto-removal is active. + // + dir_path& td (temp_dir.path); + + assert (td.empty ()); // Must be called once. + + try + { + td = dir_path::temp_path ("buildscript"); + } + catch (const system_error& e) + { + fail << "unable to obtain temporary directory for buildscript " + << "execution" << e; + } + + mkdir_status r; + + try + { + r = try_mkdir (td); + } + catch (const system_error& e) + { + fail << "unable to create temporary directory '" << td << "': " + << e << endf; + } + + // Note that the temporary directory can potentially stay after some + // abnormally terminated script run. Clean it up and reuse if that's + // the case. + // + if (r == mkdir_status::already_exists) + try + { + butl::rmdir_r (td, false /* dir */); + } + catch (const system_error& e) + { + fail << "unable to cleanup temporary directory '" << td << "': " + << e; + } + + if (verb >= 3) + text << "mkdir " << td; + } + + void environment:: + set_variable (string&& nm, + names&& val, + const string& attrs, + const location& ll) + { + // Check if we are trying to modify any of the special variables. + // + if (parser::special_variable (nm)) + fail (ll) << "attempt to set '" << nm << "' special variable"; + + // Set the variable value and attributes. + // + const variable& var (var_pool.insert (move (nm))); + + value& lhs (assign (var)); + + // If there are no attributes specified then the variable assignment + // is straightforward. Otherwise we will use the build2 parser helper + // function. + // + if (attrs.empty ()) + lhs.assign (move (val), &var); + else + { + // If there is an error in the attributes string, our diagnostics + // will look like this: + // + // <attributes>:1:1 error: unknown value attribute x + // buildfile:10:1 info: while parsing attributes '[x]' + // + // Note that the attributes parsing error is the only reason for a + // failure. + // + auto df = make_diag_frame ( + [attrs, &ll](const diag_record& dr) + { + dr << info (ll) << "while parsing attributes '" << attrs << "'"; + }); + + parser p (context); + p.apply_value_attributes (&var, + lhs, + value (move (val)), + attrs, + token_type::assign, + path_name ("<attributes>")); + } + } + + lookup environment:: + lookup (const variable& var) const + { + auto p (vars.lookup (var)); + if (p.first != nullptr) + return lookup_type (*p.first, p.second, vars); + + return lookup_in_buildfile (var.name); + } + + lookup environment:: + lookup (const string& name) const + { + // Every variable that is ever set in a script has been added during + // variable line execution or introduced with the set builtin. Which + // means that if one is not found in the environment pool then it can + // only possibly be set in the buildfile. + // + const variable* pvar (var_pool.find (name)); + return pvar != nullptr ? lookup (*pvar) : lookup_in_buildfile (name); + } + + lookup environment:: + lookup_in_buildfile (const string& n) 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 (context.var_pool.find (n)); + + if (pvar == nullptr) + return lookup_type (); + + return target[*pvar]; + } + + value& environment:: + append (const variable& var) + { + auto l (lookup (var)); + + if (l.defined () && l.belongs (*this)) // Existing var. + return vars.modify (l); + + value& r (assign (var)); // NULL. + + if (l.defined ()) + r = *l; // Copy value (and type) from the outer scope. + + return r; + } + } + } +} diff --git a/libbuild2/build/script/script.hxx b/libbuild2/build/script/script.hxx new file mode 100644 index 0000000..2118568 --- /dev/null +++ b/libbuild2/build/script/script.hxx @@ -0,0 +1,156 @@ +// file : libbuild2/build/script/script.hxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#ifndef LIBBUILD2_BUILD_SCRIPT_SCRIPT_HXX +#define LIBBUILD2_BUILD_SCRIPT_SCRIPT_HXX + +#include <libbuild2/types.hxx> +#include <libbuild2/forward.hxx> +#include <libbuild2/utility.hxx> + +#include <libbuild2/variable.hxx> +#include <libbuild2/filesystem.hxx> // auto_rmdir + +#include <libbuild2/script/script.hxx> + +namespace build2 +{ + namespace build + { + namespace script + { + using build2::script::line; + using build2::script::line_type; + using build2::script::redirect; + using build2::script::redirect_type; + using build2::script::expr_term; + using build2::script::command_expr; + + // Notes: + // + // - Once parsed, the script can be executed in multiple threads with + // the state (variable values, etc) maintained in the environment. + // + // - The default script command redirects semantics is 'none' for stdin, + // 'merge' into stderr for stdout, and 'pass' for stderr. + // + class script + { + public: + // Note that the variables are not pre-entered into a pool during the + // parsing phase, so the line variable pointers are NULL. + // + build2::script::lines lines; + + // Referenced ordinary (non-special) variables. + // + // Used for the script semantics change tracking. The variable list is + // filled during the pre-parsing phase and is checked against during + // the execution phase. If during execution some non-script-local + // variable is not found in the list (may happen for a computed name), + // then the execution fails since the script semantics may not be + // properly tracked (the variable value change will not trigger the + // target rebuild). + // + small_vector<string, 2> vars; // 2 for command and options. + + // True if script references the $~ special variable. + // + bool temp_dir = false; + + location start_loc; + location end_loc; + }; + + class environment: public build2::script::environment + { + public: + using target_type = build2::target; + + environment (action, const target_type&, bool temp_dir); + + environment (environment&&) = delete; + environment (const environment&) = delete; + environment& operator= (environment&&) = delete; + environment& operator= (const environment&) = delete; + + public: + // Primary target this environment is for. + // + const target_type& target; + + // Script-local variable pool and map. + // + // Note that if we lookup the variable by passing name as a string, + // then it will be looked up in the wrong pool. + // + variable_pool var_pool; + variable_map vars; + + // Temporary directory for the script run. + // + // Currently this directory is removed regardless of the script + // execution success or failure. Later, to help with troubleshooting, + // we may invent an option that suppresses the removal of temporary + // files in general. + // + // This directory is available to the user via the $~ special + // variable. Note, however, that the following filesystem entry + // prefixes are reserved: + // + // stdin* + // stdout* + // stderr* + // + auto_rmdir temp_dir; + + virtual void + set_variable (string&& name, + names&&, + const string& attrs, + const location&) override; + + virtual void + create_temp_dir () override; + + // Variables. + // + public: + // Lookup the variable starting from this environment, then the + // primary target, and then outer buildfile scopes. + // + using lookup_type = build2::lookup; + + lookup_type + lookup (const variable&) const; + + lookup_type + lookup (const string&) const; + + // As above but only look for buildfile variables. + // + lookup_type + lookup_in_buildfile (const string&) const; + + // Return a value suitable for assignment. If the variable does not + // exist in this environment's variable map, then a new one with the + // NULL value is added and returned. Otherwise the existing value is + // returned. + // + value& + assign (const variable& var) {return vars.assign (var);} + + // Return a value suitable for append/prepend. If the variable does + // not exist in this environment's variable map, then outer scopes are + // searched for the same variable. If found then a new variable with + // the found value is added to the environment and returned. Otherwise + // this function proceeds as assign() above. + // + value& + append (const variable&); + }; + } + } +} + +#endif // LIBBUILD2_BUILD_SCRIPT_SCRIPT_HXX diff --git a/libbuild2/build/script/token.cxx b/libbuild2/build/script/token.cxx new file mode 100644 index 0000000..8f8477b --- /dev/null +++ b/libbuild2/build/script/token.cxx @@ -0,0 +1,23 @@ +// file : libbuild2/build/script/token.cxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#include <libbuild2/build/script/token.hxx> + +using namespace std; + +namespace build2 +{ + namespace build + { + namespace script + { + void + token_printer (ostream& os, const token& t, print_mode m) + { + // No buildscript-specific tokens so far. + // + build2::script::token_printer (os, t, m); + } + } + } +} diff --git a/libbuild2/build/script/token.hxx b/libbuild2/build/script/token.hxx new file mode 100644 index 0000000..954b412 --- /dev/null +++ b/libbuild2/build/script/token.hxx @@ -0,0 +1,36 @@ +// file : libbuild2/build/script/token.hxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#ifndef LIBBUILD2_BUILD_SCRIPT_TOKEN_HXX +#define LIBBUILD2_BUILD_SCRIPT_TOKEN_HXX + +#include <libbuild2/types.hxx> +#include <libbuild2/utility.hxx> + +#include <libbuild2/script/token.hxx> + +namespace build2 +{ + namespace build + { + namespace script + { + struct token_type: build2::script::token_type + { + using base_type = build2::script::token_type; + + // No buildscript-specific tokens so far. + // + + token_type () = default; + token_type (value_type v): base_type (v) {} + token_type (build2::token_type v): base_type (v) {} + }; + + void + token_printer (ostream&, const token&, print_mode); + } + } +} + +#endif // LIBBUILD2_BUILD_SCRIPT_TOKEN_HXX |