aboutsummaryrefslogtreecommitdiff
path: root/libbuild2/build/script
diff options
context:
space:
mode:
authorBoris Kolpackov <boris@codesynthesis.com>2020-04-28 08:48:53 +0200
committerBoris Kolpackov <boris@codesynthesis.com>2020-05-27 15:47:28 +0200
commitb808c255b6a9ddba085bf5646e7d20ec344f2e2d (patch)
tree32730291f7e6de8ef0a227905520dd66fb4ec0f3 /libbuild2/build/script
parent3552356a87402727e663131994fa87f48b3cd4fb (diff)
Initial support for ad hoc recipes (still work in progress)
Diffstat (limited to 'libbuild2/build/script')
-rw-r--r--libbuild2/build/script/lexer+command-line.test.testscript164
-rw-r--r--libbuild2/build/script/lexer+first-token.test.testscript30
-rw-r--r--libbuild2/build/script/lexer+second-token.test.testscript53
-rw-r--r--libbuild2/build/script/lexer+variable-line.test.testscript12
-rw-r--r--libbuild2/build/script/lexer+variable.test.testscript25
-rw-r--r--libbuild2/build/script/lexer.cxx270
-rw-r--r--libbuild2/build/script/lexer.hxx80
-rw-r--r--libbuild2/build/script/lexer.test.cxx77
-rw-r--r--libbuild2/build/script/parser+cleanup.test.testscript57
-rw-r--r--libbuild2/build/script/parser+command-if.test.testscript395
-rw-r--r--libbuild2/build/script/parser+command-re-parse.test.testscript11
-rw-r--r--libbuild2/build/script/parser+exit.test.testscript26
-rw-r--r--libbuild2/build/script/parser+expansion.test.testscript35
-rw-r--r--libbuild2/build/script/parser+here-document.test.testscript272
-rw-r--r--libbuild2/build/script/parser+here-string.test.testscript34
-rw-r--r--libbuild2/build/script/parser+line.test.testscript72
-rw-r--r--libbuild2/build/script/parser+pipe-expr.test.testscript132
-rw-r--r--libbuild2/build/script/parser+pre-parse.test.testscript22
-rw-r--r--libbuild2/build/script/parser+redirect.test.testscript525
-rw-r--r--libbuild2/build/script/parser+regex.test.testscript225
-rw-r--r--libbuild2/build/script/parser+variable.test.testscript41
-rw-r--r--libbuild2/build/script/parser.cxx391
-rw-r--r--libbuild2/build/script/parser.hxx96
-rw-r--r--libbuild2/build/script/parser.test.cxx224
-rw-r--r--libbuild2/build/script/runner.cxx133
-rw-r--r--libbuild2/build/script/runner.hxx84
-rw-r--r--libbuild2/build/script/script.cxx236
-rw-r--r--libbuild2/build/script/script.hxx156
-rw-r--r--libbuild2/build/script/token.cxx23
-rw-r--r--libbuild2/build/script/token.hxx36
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