aboutsummaryrefslogtreecommitdiff
path: root/libbuild2/test/script
diff options
context:
space:
mode:
Diffstat (limited to 'libbuild2/test/script')
-rw-r--r--libbuild2/test/script/lexer+for-loop.test.testscript231
-rw-r--r--libbuild2/test/script/lexer.cxx55
-rw-r--r--libbuild2/test/script/lexer.hxx13
-rw-r--r--libbuild2/test/script/lexer.test.cxx1
-rw-r--r--libbuild2/test/script/parser+for.test.testscript736
-rw-r--r--libbuild2/test/script/parser.cxx311
-rw-r--r--libbuild2/test/script/parser.test.cxx25
-rw-r--r--libbuild2/test/script/runner.cxx43
-rw-r--r--libbuild2/test/script/runner.hxx5
-rw-r--r--libbuild2/test/script/script.cxx10
-rw-r--r--libbuild2/test/script/script.hxx22
11 files changed, 1285 insertions, 167 deletions
diff --git a/libbuild2/test/script/lexer+for-loop.test.testscript b/libbuild2/test/script/lexer+for-loop.test.testscript
new file mode 100644
index 0000000..fcd12f7
--- /dev/null
+++ b/libbuild2/test/script/lexer+for-loop.test.testscript
@@ -0,0 +1,231 @@
+# file : libbuild2/test/script/lexer+for-loop.test.testscript
+# license : MIT; see accompanying LICENSE file
+
+test.arguments = for-loop
+
+: semi
+{
+ : immediate
+ :
+ $* <"cmd;" >>EOO
+ 'cmd'
+ ;
+ <newline>
+ EOO
+
+ : separated
+ :
+ $* <"cmd ;" >>EOO
+ 'cmd'
+ ;
+ <newline>
+ EOO
+
+ : only
+ :
+ $* <";" >>EOO
+ ;
+ <newline>
+ EOO
+}
+
+: colon
+:
+{
+ : immediate
+ :
+ $* <"cmd: dsc" >>EOO
+ 'cmd'
+ :
+ 'dsc'
+ <newline>
+ EOO
+
+ : separated
+ :
+ $* <"cmd :dsc" >>EOO
+ 'cmd'
+ :
+ 'dsc'
+ <newline>
+ EOO
+
+ : only
+ :
+ $* <":" >>EOO
+ :
+ <newline>
+ EOO
+}
+
+: 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
+
+ : 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
+}
+
+: for
+:
+{
+ : form-1
+ :
+ $* <"for x: a" >>EOO
+ 'for'
+ 'x'
+ :
+ 'a'
+ <newline>
+ EOO
+
+ : form-3
+ :
+ $* <"for <<<a x" >>EOO
+ 'for'
+ <<<
+ 'a'
+ 'x'
+ <newline>
+ EOO
+}
diff --git a/libbuild2/test/script/lexer.cxx b/libbuild2/test/script/lexer.cxx
index f9c8ac6..aec91fc 100644
--- a/libbuild2/test/script/lexer.cxx
+++ b/libbuild2/test/script/lexer.cxx
@@ -34,13 +34,16 @@ namespace build2
bool q (true); // quotes
if (!esc)
- {
- assert (!state_.empty ());
- esc = state_.top ().escapes;
- }
+ esc = current_state ().escapes;
switch (m)
{
+ case lexer_mode::for_loop:
+ {
+ // Leading tokens of the for-loop. Like command_line but also
+ // recognizes lsbrace like value.
+ }
+ // Fall through.
case lexer_mode::command_line:
{
s1 = ":;=!|&<> $(#\t\n";
@@ -107,7 +110,7 @@ namespace build2
}
assert (ps == '\0');
- state_.push (
+ mode_impl (
state {m, data, nullopt, false, false, ps, s, n, q, *esc, s1, s2});
}
@@ -116,12 +119,13 @@ namespace build2
{
token r;
- switch (state_.top ().mode)
+ switch (mode ())
{
case lexer_mode::command_line:
case lexer_mode::first_token:
case lexer_mode::second_token:
case lexer_mode::variable_line:
+ case lexer_mode::for_loop:
r = next_line ();
break;
case lexer_mode::description_line:
@@ -144,7 +148,7 @@ namespace build2
xchar c (get ());
uint64_t ln (c.line), cn (c.column);
- state st (state_.top ()); // Make copy (see first/second_token).
+ state st (current_state ()); // Make copy (see first/second_token).
lexer_mode m (st.mode);
auto make_token = [&sep, ln, cn] (type t)
@@ -157,9 +161,10 @@ namespace build2
//
if (st.lsbrace)
{
- assert (m == lexer_mode::variable_line);
+ assert (m == lexer_mode::variable_line ||
+ m == lexer_mode::for_loop);
- state_.top ().lsbrace = false; // Note: st is a copy.
+ current_state ().lsbrace = false; // Note: st is a copy.
if (c == '[' && (!st.lsbrace_unsep || !sep))
return make_token (type::lsbrace);
@@ -172,7 +177,7 @@ namespace build2
// we push any new mode (e.g., double quote).
//
if (m == lexer_mode::first_token || m == lexer_mode::second_token)
- state_.pop ();
+ expire_mode ();
// NOTE: remember to update mode() if adding new special characters.
@@ -183,7 +188,7 @@ namespace build2
// Expire variable value mode at the end of the line.
//
if (m == lexer_mode::variable_line)
- state_.pop ();
+ expire_mode ();
sep = true; // Treat newline as always separated.
return make_token (type::newline);
@@ -197,10 +202,11 @@ namespace build2
// Line separators.
//
- if (m == lexer_mode::command_line ||
- m == lexer_mode::first_token ||
- m == lexer_mode::second_token ||
- m == lexer_mode::variable_line)
+ if (m == lexer_mode::command_line ||
+ m == lexer_mode::first_token ||
+ m == lexer_mode::second_token ||
+ m == lexer_mode::variable_line ||
+ m == lexer_mode::for_loop)
{
switch (c)
{
@@ -210,7 +216,8 @@ namespace build2
if (m == lexer_mode::command_line ||
m == lexer_mode::first_token ||
- m == lexer_mode::second_token)
+ m == lexer_mode::second_token ||
+ m == lexer_mode::for_loop)
{
switch (c)
{
@@ -222,7 +229,8 @@ namespace build2
//
if (m == lexer_mode::command_line ||
m == lexer_mode::first_token ||
- m == lexer_mode::second_token)
+ m == lexer_mode::second_token ||
+ m == lexer_mode::for_loop)
{
switch (c)
{
@@ -244,7 +252,8 @@ namespace build2
//
if (m == lexer_mode::command_line ||
m == lexer_mode::first_token ||
- m == lexer_mode::second_token)
+ m == lexer_mode::second_token ||
+ m == lexer_mode::for_loop)
{
if (optional<token> t = next_cmd_op (c, sep))
return move (*t);
@@ -310,7 +319,7 @@ namespace build2
if (c == '\n')
{
get ();
- state_.pop (); // Expire the description mode.
+ expire_mode (); // Expire the description mode.
return token (type::newline, true, ln, cn, token_printer);
}
@@ -330,15 +339,17 @@ namespace build2
}
token lexer::
- word (state st, bool sep)
+ word (const state& st, bool sep)
{
- lexer_mode m (st.mode);
+ lexer_mode m (st.mode); // Save.
token r (base_lexer::word (st, sep));
if (m == lexer_mode::variable)
{
- if (r.value.size () == 1 && digit (r.value[0])) // $N
+ if (r.type == type::word &&
+ r.value.size () == 1 &&
+ digit (r.value[0])) // $N
{
xchar c (peek ());
diff --git a/libbuild2/test/script/lexer.hxx b/libbuild2/test/script/lexer.hxx
index 452e794..39b950a 100644
--- a/libbuild2/test/script/lexer.hxx
+++ b/libbuild2/test/script/lexer.hxx
@@ -24,10 +24,11 @@ namespace build2
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.
- description_line // Expires at the end of the line.
+ 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.
+ description_line, // Expires at the end of the line.
+ for_loop // Used for sensing the for-loop leading tokens.
};
lexer_mode () = default;
@@ -67,6 +68,8 @@ namespace build2
static redirect_aliases_type redirect_aliases;
private:
+ using build2::script::lexer::mode; // Getter.
+
token
next_line ();
@@ -74,7 +77,7 @@ namespace build2
next_description ();
virtual token
- word (state, bool) override;
+ word (const state&, bool) override;
};
}
}
diff --git a/libbuild2/test/script/lexer.test.cxx b/libbuild2/test/script/lexer.test.cxx
index 76f102d..ef3ce4d 100644
--- a/libbuild2/test/script/lexer.test.cxx
+++ b/libbuild2/test/script/lexer.test.cxx
@@ -36,6 +36,7 @@ namespace build2
else if (s == "variable-line") m = lexer_mode::variable_line;
else if (s == "description-line") m = lexer_mode::description_line;
else if (s == "variable") m = lexer_mode::variable;
+ else if (s == "for-loop") m = lexer_mode::for_loop;
else assert (false);
}
diff --git a/libbuild2/test/script/parser+for.test.testscript b/libbuild2/test/script/parser+for.test.testscript
index 92fc714..985f9c9 100644
--- a/libbuild2/test/script/parser+for.test.testscript
+++ b/libbuild2/test/script/parser+for.test.testscript
@@ -16,7 +16,7 @@
cmd
end
EOI
- testscript:1:4: error: expected variable name instead of <newline>
+ testscript:1:1: error: for: missing variable name
EOE
: untyped
@@ -58,7 +58,7 @@
cmd b
EOO
- : typed-value
+ : typed-values
:
$* <<EOI >>~%EOO%
for x: [dir_paths] a b
@@ -69,10 +69,10 @@
%cmd (b/|'b\\')%
EOO
- : typed-var
+ : typed-elem
:
$* <<EOI >>~%EOO%
- for [dir_path] x: a b
+ for x [dir_path]: a b
cmd $x
end
EOI
@@ -80,17 +80,16 @@
%cmd (b/|'b\\')%
EOO
- : type-mismatch
+ : typed-elem-value
:
- $* <<EOI 2>>EOE != 0
- for [string] x: [dir_paths] a b
+ $* <<EOI >>~%EOO%
+ for x [dir_path]: [strings] a b
cmd $x
end
EOI
- error: type mismatch in variable x
- info: value type is dir_path
- info: variable type is string
- EOE
+ %cmd (a/|'a\\')%
+ %cmd (b/|'b\\')%
+ EOO
: scope-var
:
@@ -313,3 +312,718 @@
testscript:4:1: error: both leading and trailing descriptions
EOE
}
+
+: form-2
+:
+: ... | for x
+:
+{
+ : for
+ :
+ {
+ : status
+ :
+ $* <<EOI 2>>EOE != 0
+ echo 'a b' | for x != 0
+ cmd
+ end
+ EOI
+ testscript:1:20: error: for-loop exit code cannot be checked
+ EOE
+
+ : not-last
+ :
+ $* <<EOI 2>>EOE != 0
+ echo 'a b' | for x | echo x
+ cmd
+ end
+ EOI
+ testscript:1:20: error: for-loop must be last command in a pipe
+ EOE
+
+ : not-last-relex
+ :
+ $* <<EOI 2>>EOE != 0
+ echo 'a b' | for x|echo x
+ cmd
+ end
+ EOI
+ testscript:1:19: error: for-loop must be last command in a pipe
+ EOE
+
+ : expression-after
+ :
+ $* <<EOI 2>>EOE != 0
+ echo 'a b' | for x && echo x
+ cmd
+ end
+ EOI
+ testscript:1:20: error: command expression involving for-loop
+ EOE
+
+ : expression-after-relex
+ :
+ $* <<EOI 2>>EOE != 0
+ echo 'a b' | for x&&echo x
+ cmd
+ end
+ EOI
+ testscript:1:19: error: command expression involving for-loop
+ EOE
+
+ : expression-before
+ :
+ $* <<EOI 2>>EOE != 0
+ echo 'a b' && echo x | for x
+ cmd
+ end
+ EOI
+ testscript:1:24: error: command expression involving for-loop
+ EOE
+
+ : expression-before-relex
+ :
+ $* <<EOI 2>>EOE != 0
+ echo 'a b' && echo x|for x
+ cmd
+ end
+ EOI
+ testscript:1:22: error: command expression involving for-loop
+ EOE
+
+ : cleanup
+ :
+ $* <<EOI 2>>EOE != 0
+ echo 'a b' | for x &f
+ cmd
+ end
+ EOI
+ testscript:1:20: error: cleanup in for-loop
+ EOE
+
+ : cleanup-relex
+ :
+ $* <<EOI 2>>EOE != 0
+ echo 'a b' | for x&f
+ cmd
+ end
+ EOI
+ testscript:1:19: error: cleanup in for-loop
+ EOE
+
+ : stdout-redirect
+ :
+ $* <<EOI 2>>EOE != 0
+ echo 'a b' | for x >a
+ cmd
+ end
+ EOI
+ testscript:1:20: error: output redirect in for-loop
+ EOE
+
+ : stdout-redirect-relex
+ :
+ $* <<EOI 2>>EOE != 0
+ echo 'a b' | for x>a
+ cmd
+ end
+ EOI
+ testscript:1:19: error: output redirect in for-loop
+ EOE
+
+ : stdin-redirect
+ :
+ $* <<EOI 2>>EOE != 0
+ echo 'a b' | for x <a
+ cmd
+ end
+ EOI
+ testscript:1:20: error: stdin is both piped and redirected
+ EOE
+
+ : no-var
+ :
+ $* <<EOI 2>>EOE != 0
+ echo 'a b' | for
+ cmd
+ end
+ EOI
+ testscript:1:1: error: for: missing variable name
+ EOE
+
+ : untyped
+ :
+ $* <<EOI >>EOO
+ echo 'a b' | for -w x
+ cmd $x
+ end
+ EOI
+ echo 'a b' | for -w x
+ EOO
+
+ : expansion
+ :
+ $* <<EOI >>EOO
+ vs = a b
+ echo $vs | for x
+ cmd $x
+ end
+ EOI
+ echo a b | for x
+ EOO
+
+ : typed-elem
+ :
+ $* <<EOI >>EOO
+ echo 'a b' | for -w x [dir_path]
+ cmd $x
+ end
+ EOI
+ echo 'a b' | for -w x [dir_path]
+ EOO
+ }
+
+ : after-semi
+ :
+ $* -s <<EOI >>EOO
+ cmd1;
+ echo 'a b' | for x
+ cmd2 $x
+ end
+ EOI
+ {
+ {
+ cmd1
+ echo 'a b' | for x
+ }
+ }
+ EOO
+
+ : setup
+ :
+ $* -s <<EOI >>EOO
+ +echo 'a b' | for x
+ cmd $x
+ end
+ EOI
+ {
+ +echo 'a b' | for x
+ }
+ EOO
+
+ : tdown
+ :
+ $* -s <<EOI >>EOO
+ -echo 'a b' | for x
+ cmd $x
+ end
+ EOI
+ {
+ -echo 'a b' | for x
+ }
+ EOO
+
+ : end
+ :
+ {
+ : without-end
+ :
+ $* <<EOI 2>>EOE != 0
+ echo 'a b' | for x
+ cmd
+ EOI
+ testscript:3:1: error: expected closing 'end'
+ EOE
+ }
+
+ : elif
+ :
+ {
+ : without-if
+ :
+ $* <<EOI 2>>EOE != 0
+ echo 'a b' | for x
+ elif true
+ cmd
+ end
+ end
+ EOI
+ testscript:2:3: error: 'elif' without preceding 'if'
+ EOE
+ }
+
+ : nested
+ :
+ {
+ $* -l -r <<EOI >>EOO
+ echo 'a b' | for x # 1
+ cmd1 $x # 2
+ if ($x == "a") # 3
+ cmd2 # 4
+ echo x y | for y # 5
+ cmd3 # 6
+ end
+ else
+ cmd4 # 7
+ end
+ cmd5 # 8
+ end;
+ cmd6 # 9
+ EOI
+ echo 'a b' | for x # 1
+ cmd6 # 9
+ EOO
+ }
+
+ : contained
+ :
+ {
+ : semi
+ :
+ $* <<EOI 2>>EOE != 0
+ echo 'a b' | for x
+ cmd;
+ cmd
+ end
+ EOI
+ testscript:2:3: error: ';' inside 'for'
+ EOE
+
+ : colon-leading
+ :
+ $* <<EOI 2>>EOE != 0
+ echo 'a b' | for x
+ : foo
+ cmd
+ end
+ EOI
+ testscript:2:3: error: description inside 'for'
+ EOE
+
+ : colon-trailing
+ :
+ $* <<EOI 2>>EOE != 0
+ echo 'a b' | for x
+ cmd : foo
+ end
+ EOI
+ testscript:2:3: error: description inside 'for'
+ EOE
+
+ : eos
+ :
+ $* <<EOI 2>>EOE != 0
+ echo 'a b' | for x
+ EOI
+ testscript:2:1: error: expected closing 'end'
+ EOE
+
+ : scope
+ :
+ $* <<EOI 2>>EOE != 0
+ echo 'a b' | for x
+ cmd
+ {
+ }
+ end
+ EOI
+ testscript:3:3: error: expected closing 'end'
+ EOE
+
+ : setup
+ :
+ $* <<EOI 2>>EOE != 0
+ echo 'a b' | for x
+ +cmd
+ end
+ EOI
+ testscript:2:3: error: setup command inside 'for'
+ EOE
+
+ : tdown
+ :
+ $* <<EOI 2>>EOE != 0
+ echo 'a b' | for x
+ -cmd
+ end
+ EOI
+ testscript:2:3: error: teardown command inside 'for'
+ EOE
+ }
+
+ : leading-and-trailing-description
+ :
+ $* <<EOI 2>>EOE != 0
+ : foo
+ echo 'a b' | for x
+ cmd
+ end : bar
+ EOI
+ testscript:4:1: error: both leading and trailing descriptions
+ EOE
+}
+
+: form-3
+:
+: for x <...
+:
+{
+ : for
+ :
+ {
+ : status
+ :
+ $* <<EOI 2>>EOE != 0
+ for x <a != 0
+ cmd
+ end
+ EOI
+ testscript:1:10: error: for-loop exit code cannot be checked
+ EOE
+
+ : not-last
+ :
+ $* <<EOI 2>>EOE != 0
+ for x <a | echo x
+ cmd
+ end
+ EOI
+ testscript:1:10: error: for-loop must be last command in a pipe
+ EOE
+
+ : not-last-relex
+ :
+ $* <<EOI 2>>EOE != 0
+ for <a x|echo x
+ cmd
+ end
+ EOI
+ testscript:1:9: error: for-loop must be last command in a pipe
+ EOE
+
+ : expression-after
+ :
+ $* <<EOI 2>>EOE != 0
+ for x <a && echo x
+ cmd
+ end
+ EOI
+ testscript:1:10: error: command expression involving for-loop
+ EOE
+
+ : expression-after-relex
+ :
+ $* <<EOI 2>>EOE != 0
+ for <a x&&echo x
+ cmd
+ end
+ EOI
+ testscript:1:9: error: command expression involving for-loop
+ EOE
+
+ : expression-before
+ :
+ $* <<EOI 2>>EOE != 0
+ echo 'a b' && for x <a
+ cmd
+ end
+ EOI
+ testscript:1:15: error: command expression involving for-loop
+ EOE
+
+ : cleanup
+ :
+ $* <<EOI 2>>EOE != 0
+ for x <a &f
+ cmd
+ end
+ EOI
+ testscript:1:10: error: cleanup in for-loop
+ EOE
+
+ : cleanup-before-var
+ :
+ $* <<EOI 2>>EOE != 0
+ for &f x <a
+ cmd
+ end
+ EOI
+ testscript:1:5: error: cleanup in for-loop
+ EOE
+
+ : cleanup-relex
+ :
+ $* <<EOI 2>>EOE != 0
+ for <a x&f
+ cmd
+ end
+ EOI
+ testscript:1:9: error: cleanup in for-loop
+ EOE
+
+ : stdout-redirect
+ :
+ $* <<EOI 2>>EOE != 0
+ for x >a
+ cmd
+ end
+ EOI
+ testscript:1:7: error: output redirect in for-loop
+ EOE
+
+ : stdout-redirect-before-var
+ :
+ $* <<EOI 2>>EOE != 0
+ for >a x
+ cmd
+ end
+ EOI
+ testscript:1:5: error: output redirect in for-loop
+ EOE
+
+ : stdout-redirect-relex
+ :
+ $* <<EOI 2>>EOE != 0
+ for x>a
+ cmd
+ end
+ EOI
+ testscript:1:6: error: output redirect in for-loop
+ EOE
+
+ : no-var
+ :
+ $* <<EOI 2>>EOE != 0
+ for <a
+ cmd
+ end
+ EOI
+ testscript:1:1: error: for: missing variable name
+ EOE
+
+ : quoted-opt
+ :
+ $* <<EOI >>EOO
+ o = -w
+ for "$o" x <'a b'
+ cmd $x
+ end;
+ for "($o)" x <'a b'
+ cmd $x
+ end
+ EOI
+ for -w x <'a b'
+ for -w x <'a b'
+ EOO
+
+ : untyped
+ :
+ $* <<EOI >>EOO
+ for -w x <'a b'
+ cmd $x
+ end
+ EOI
+ for -w x <'a b'
+ EOO
+
+ : expansion
+ :
+ $* <<EOI >>EOO
+ vs = a b
+ for x <$vs
+ cmd $x
+ end
+ EOI
+ for x b <a
+ EOO
+
+ : typed-elem
+ :
+ $* <<EOI >>EOO
+ for -w x [dir_path] <'a b'
+ cmd $x
+ end
+ EOI
+ for -w x [dir_path] <'a b'
+ EOO
+ }
+
+ : after-semi
+ :
+ $* -s <<EOI >>EOO
+ cmd1;
+ for x <'a b'
+ cmd2 $x
+ end
+ EOI
+ {
+ {
+ cmd1
+ for x <'a b'
+ }
+ }
+ EOO
+
+ : setup
+ :
+ $* -s <<EOI >>EOO
+ +for x <'a b'
+ cmd $x
+ end
+ EOI
+ {
+ +for x <'a b'
+ }
+ EOO
+
+ : tdown
+ :
+ $* -s <<EOI >>EOO
+ -for x <'a b'
+ cmd $x
+ end
+ EOI
+ {
+ -for x <'a b'
+ }
+ EOO
+
+ : end
+ :
+ {
+ : without-end
+ :
+ $* <<EOI 2>>EOE != 0
+ for x <'a b'
+ cmd
+ EOI
+ testscript:3:1: error: expected closing 'end'
+ EOE
+ }
+
+ : elif
+ :
+ {
+ : without-if
+ :
+ $* <<EOI 2>>EOE != 0
+ for x <'a b'
+ elif true
+ cmd
+ end
+ end
+ EOI
+ testscript:2:3: error: 'elif' without preceding 'if'
+ EOE
+ }
+
+ : nested
+ :
+ {
+ $* -l -r <<EOI >>EOO
+ for -w x <'a b' # 1
+ cmd1 $x # 2
+ if ($x == "a") # 3
+ cmd2 # 4
+ for -w y <'x y' # 5
+ cmd3 # 6
+ end
+ else
+ cmd4 # 7
+ end
+ cmd5 # 8
+ end;
+ cmd6 # 9
+ EOI
+ for -w x <'a b' # 1
+ cmd6 # 9
+ EOO
+ }
+
+ : contained
+ :
+ {
+ : semi
+ :
+ $* <<EOI 2>>EOE != 0
+ for x <'a b'
+ cmd;
+ cmd
+ end
+ EOI
+ testscript:2:3: error: ';' inside 'for'
+ EOE
+
+ : colon-leading
+ :
+ $* <<EOI 2>>EOE != 0
+ for x <'a b'
+ : foo
+ cmd
+ end
+ EOI
+ testscript:2:3: error: description inside 'for'
+ EOE
+
+ : colon-trailing
+ :
+ $* <<EOI 2>>EOE != 0
+ for x <'a b'
+ cmd : foo
+ end
+ EOI
+ testscript:2:3: error: description inside 'for'
+ EOE
+
+ : eos
+ :
+ $* <<EOI 2>>EOE != 0
+ for x <'a b'
+ EOI
+ testscript:2:1: error: expected closing 'end'
+ EOE
+
+ : scope
+ :
+ $* <<EOI 2>>EOE != 0
+ for x <'a b'
+ cmd
+ {
+ }
+ end
+ EOI
+ testscript:3:3: error: expected closing 'end'
+ EOE
+
+ : setup
+ :
+ $* <<EOI 2>>EOE != 0
+ for x <'a b'
+ +cmd
+ end
+ EOI
+ testscript:2:3: error: setup command inside 'for'
+ EOE
+
+ : tdown
+ :
+ $* <<EOI 2>>EOE != 0
+ for x <'a b'
+ -cmd
+ end
+ EOI
+ testscript:2:3: error: teardown command inside 'for'
+ EOE
+ }
+
+ : leading-and-trailing-description
+ :
+ $* <<EOI 2>>EOE != 0
+ : foo
+ for x <'a b'
+ cmd
+ end : bar
+ EOI
+ testscript:4:1: error: both leading and trailing descriptions
+ EOE
+}
diff --git a/libbuild2/test/script/parser.cxx b/libbuild2/test/script/parser.cxx
index 0ba5a79..337b162 100644
--- a/libbuild2/test/script/parser.cxx
+++ b/libbuild2/test/script/parser.cxx
@@ -311,10 +311,11 @@ namespace build2
// enter: next token is peeked at (type in tt)
// leave: newline
- assert (!fct ||
- *fct == line_type::cmd_if ||
- *fct == line_type::cmd_while ||
- *fct == line_type::cmd_for);
+ assert (!fct ||
+ *fct == line_type::cmd_if ||
+ *fct == line_type::cmd_while ||
+ *fct == line_type::cmd_for_stream ||
+ *fct == line_type::cmd_for_args);
// Note: token is only peeked at.
//
@@ -324,6 +325,52 @@ namespace build2
//
line_type lt;
type st (type::eos); // Later, can only be set to plus or minus.
+ bool semi (false);
+
+ // Parse the command line tail, starting from the newline or the
+ // potential colon/semicolon token.
+ //
+ // Note that colon and semicolon are only valid in test command lines
+ // and after 'end' in flow control constructs. Note that we always
+ // recognize them lexically, even when they are not valid tokens per
+ // the grammar.
+ //
+ auto parse_command_tail = [&t, &tt, &st, &lt, &d, &semi, &ll, this] ()
+ {
+ if (tt != type::newline)
+ {
+ if (lt != line_type::cmd && lt != line_type::cmd_end)
+ fail (t) << "expected newline instead of " << t;
+
+ switch (st)
+ {
+ case type::plus: fail (t) << t << " after setup command" << endf;
+ case type::minus: fail (t) << t << " after teardown command" << endf;
+ }
+ }
+
+ switch (tt)
+ {
+ case type::colon:
+ {
+ if (d)
+ fail (ll) << "both leading and trailing descriptions";
+
+ d = parse_trailing_description (t, tt);
+ break;
+ }
+ case type::semi:
+ {
+ semi = true;
+ replay_pop (); // See above for the reasoning.
+ next (t, tt); // Get newline.
+ break;
+ }
+ }
+
+ if (tt != type::newline)
+ fail (t) << "expected newline instead of " << t;
+ };
switch (tt)
{
@@ -371,10 +418,12 @@ namespace build2
{
const string& n (t.value);
+ // Handle the for-loop consistently with pre_parse_line_start().
+ //
if (n == "if") lt = line_type::cmd_if;
else if (n == "if!") lt = line_type::cmd_ifn;
else if (n == "while") lt = line_type::cmd_while;
- else if (n == "for") lt = line_type::cmd_for;
+ else if (n == "for") lt = line_type::cmd_for_stream;
}
break;
@@ -388,8 +437,6 @@ namespace build2
// Pre-parse the line keeping track of whether it ends with a semi.
//
- bool semi (false);
-
line ln;
switch (lt)
{
@@ -436,47 +483,89 @@ namespace build2
break;
}
- case line_type::cmd_for:
+ //
+ // See pre_parse_line_start() for details.
+ //
+ case line_type::cmd_for_args: assert (false); break;
+ case line_type::cmd_for_stream:
{
- // First take care of the variable name. There is no reason not to
- // support variable attributes.
+ // First we need to sense the next few tokens and detect which
+ // form of the for-loop that actually is (see
+ // libbuild2/build/script/parser.cxx for details).
//
- mode (lexer_mode::normal);
+ token pt (t);
+ assert (pt.type == type::word && pt.value == "for");
- next_with_attributes (t, tt);
- attributes_push (t, tt);
-
- if (tt != type::word || t.qtype != quote_type::unquoted)
- fail (t) << "expected variable name instead of " << t;
+ mode (lexer_mode::for_loop);
+ next (t, tt);
string& n (t.value);
- if (special_variable (n))
- fail (t) << "attempt to set '" << n << "' variable directly";
+ if (tt == type::word && t.qtype == quote_type::unquoted &&
+ (n[0] == '_' || alpha (n[0]) || // Variable.
+ n == "*" || n == "~" || n == "@")) // Special variable.
+ {
+ // Detect patterns analogous to parse_variable_name() (so we
+ // diagnose `for x[string]: ...`).
+ //
+ if (n.find_first_of ("[*?") != string::npos)
+ fail (t) << "expected variable name instead of " << n;
- ln.var = &script_->var_pool.insert (move (n));
+ if (special_variable (n))
+ fail (t) << "attempt to set '" << n << "' variable directly";
- next (t, tt);
+ if (lexer_->peek_char ().first == '[')
+ {
+ token vt (move (t));
+ next_with_attributes (t, tt);
+
+ attributes_push (t, tt,
+ true /* standalone */,
+ false /* next_token */);
+
+ t = move (vt);
+ tt = t.type;
+ }
+
+ if (lexer_->peek_char ().first == ':')
+ lt = line_type::cmd_for_args;
+ }
- if (tt != type::colon)
+ if (lt == line_type::cmd_for_stream) // for x <...
{
- // @@ TMP We will need to fallback to parsing the 'for x <...'
- // form instead.
- //
- fail (t) << "expected ':' instead of " << t
- << " after variable name";
+ ln.var = nullptr;
+
+ expire_mode ();
+
+ parse_command_expr_result r (
+ parse_command_expr (t, tt,
+ lexer::redirect_aliases,
+ move (pt)));
+
+ assert (r.for_loop);
+
+ parse_command_tail ();
+ parse_here_documents (t, tt, r);
}
+ else // for x: ...
+ {
+ ln.var = &script_->var_pool.insert (move (n));
- expire_mode (); // Expire the normal lexer mode.
+ next (t, tt);
- // Parse the value similar to the var line type (see above),
- // except for the fact that we don't expect a trailing semicolon.
- //
- mode (lexer_mode::variable_line);
- parse_variable_line (t, tt);
+ assert (tt == type::colon);
- if (tt != type::newline)
- fail (t) << "expected newline instead of " << t << " after for";
+ expire_mode ();
+
+ // Parse the value similar to the var line type (see above),
+ // except for the fact that we don't expect a trailing semicolon.
+ //
+ mode (lexer_mode::variable_line);
+ parse_variable_line (t, tt);
+
+ if (tt != type::newline)
+ fail (t) << "expected newline instead of " << t << " after for";
+ }
break;
}
@@ -501,51 +590,20 @@ namespace build2
// Fall through.
case line_type::cmd:
{
- pair<command_expr, here_docs> p;
+ parse_command_expr_result r;
if (lt != line_type::cmd_else && lt != line_type::cmd_end)
- p = parse_command_expr (t, tt, lexer::redirect_aliases);
+ r = parse_command_expr (t, tt, lexer::redirect_aliases);
- // Colon and semicolon are only valid in test command lines and
- // after 'end' in a flow control construct. Note that we still
- // recognize them lexically, they are just not valid tokens per
- // the grammar.
- //
- if (tt != type::newline)
- {
- if (lt != line_type::cmd && lt != line_type::cmd_end)
- fail (t) << "expected newline instead of " << t;
-
- switch (st)
- {
- case type::plus: fail (t) << t << " after setup command" << endf;
- case type::minus: fail (t) << t << " after teardown command" << endf;
- }
- }
-
- switch (tt)
+ if (r.for_loop)
{
- case type::colon:
- {
- if (d)
- fail (ll) << "both leading and trailing descriptions";
-
- d = parse_trailing_description (t, tt);
- break;
- }
- case type::semi:
- {
- semi = true;
- replay_pop (); // See above for the reasoning.
- next (t, tt); // Get newline.
- break;
- }
+ lt = line_type::cmd_for_stream;
+ ln.var = nullptr;
}
- if (tt != type::newline)
- fail (t) << "expected newline instead of " << t;
+ parse_command_tail ();
+ parse_here_documents (t, tt, r);
- parse_here_documents (t, tt, p);
break;
}
}
@@ -579,7 +637,8 @@ namespace build2
break;
}
case line_type::cmd_while:
- case line_type::cmd_for:
+ case line_type::cmd_for_stream:
+ case line_type::cmd_for_args:
{
semi = pre_parse_loop (t, tt, lt, d, *ls);
break;
@@ -608,7 +667,8 @@ namespace build2
case line_type::cmd_if:
case line_type::cmd_ifn:
case line_type::cmd_while:
- case line_type::cmd_for:
+ case line_type::cmd_for_stream:
+ case line_type::cmd_for_args:
{
// See if this is a variable-only flow control construct.
//
@@ -951,7 +1011,7 @@ namespace build2
//
size_t i (ls.size ());
- line_type fct; // Flow control type the block type relates to.
+ line_type fct; // Flow control construct type the block type relates to.
switch (bt)
{
@@ -965,7 +1025,8 @@ namespace build2
break;
}
case line_type::cmd_while:
- case line_type::cmd_for:
+ case line_type::cmd_for_stream:
+ case line_type::cmd_for_args:
{
fct = bt;
break;
@@ -1068,7 +1129,9 @@ namespace build2
// enter: <newline> (previous line)
// leave: <newline>
- assert (lt == line_type::cmd_while || lt == line_type::cmd_for);
+ assert (lt == line_type::cmd_while ||
+ lt == line_type::cmd_for_stream ||
+ lt == line_type::cmd_for_args);
tt = peek (lexer_mode::first_token);
@@ -1428,7 +1491,7 @@ namespace build2
// Note: this one is only used during execution.
- pair<command_expr, here_docs> p (
+ parse_command_expr_result pr (
parse_command_expr (t, tt, lexer::redirect_aliases));
if (tt == type::colon)
@@ -1436,10 +1499,10 @@ namespace build2
assert (tt == type::newline);
- parse_here_documents (t, tt, p);
+ parse_here_documents (t, tt, pr);
assert (tt == type::newline);
- command_expr r (move (p.first));
+ command_expr r (move (pr.expr));
// If the test program runner is specified, then adjust the
// expressions to run test programs via this runner.
@@ -1546,22 +1609,41 @@ namespace build2
{
runner_->enter (*scope_, scope_->start_loc_);
+ // Set thread-specific current directory override. In particular, this
+ // makes sure functions like $path.complete() work correctly.
+ //
+ auto wdg = make_guard (
+ [old = path_traits::thread_current_directory ()] ()
+ {
+ path_traits::thread_current_directory (old);
+ });
+
+ path_traits::thread_current_directory (&scope_->work_dir.path->string ());
+
// Note that we rely on "small function object" optimization for the
// exec_*() lambdas.
//
- auto exec_assign = [this] (const variable& var,
- value&& val,
- type kind,
- const location& l)
+ auto exec_set = [this] (const variable& var,
+ token& t, build2::script::token_type& tt,
+ const location&)
{
+ next (t, tt);
+ type kind (tt); // Assignment kind.
+
+ // We cannot reuse the value mode (see above for details).
+ //
+ mode (lexer_mode::variable_line);
+ value rhs (parse_variable_line (t, tt));
+
+ assert (tt == type::newline);
+
+ // Assign.
+ //
value& lhs (kind == type::assign
? scope_->assign (var)
: scope_->append (var));
- if (kind == type::assign)
- lhs = move (val);
- else
- append_value (&var, lhs, move (val), l);
+ apply_value_attributes (&var, lhs, move (rhs), kind);
if (script_->test_command_var (var.name))
scope_->reset_special ();
@@ -1574,6 +1656,7 @@ namespace build2
auto exec_cmd = [&ct, this] (token& t, build2::script::token_type& tt,
const iteration_index* ii, size_t li,
bool single,
+ const function<command_function>& cf,
const location& ll)
{
// We use the 0 index to signal that this is the only command.
@@ -1585,7 +1668,7 @@ namespace build2
command_expr ce (
parse_command_line (t, static_cast<token_type&> (tt)));
- runner_->run (*scope_, ce, ct, ii, li, ll);
+ runner_->run (*scope_, ce, ct, ii, li, cf, ll);
};
auto exec_cond = [this] (token& t, build2::script::token_type& tt,
@@ -1601,6 +1684,21 @@ namespace build2
return runner_->run_cond (*scope_, ce, ii, li, ll);
};
+ auto exec_for = [this] (const variable& var,
+ value&& val,
+ const attributes& val_attrs,
+ const location&)
+ {
+ value& lhs (scope_->assign (var));
+
+ attributes_.push_back (val_attrs);
+
+ apply_value_attributes (&var, lhs, move (val), type::assign);
+
+ if (script_->test_command_var (var.name))
+ scope_->reset_special ();
+ };
+
size_t li (1);
if (test* t = dynamic_cast<test*> (scope_))
@@ -1608,7 +1706,7 @@ namespace build2
ct = command_type::test;
exec_lines (t->tests_.begin (), t->tests_.end (),
- exec_assign, exec_cmd, exec_cond,
+ exec_set, exec_cmd, exec_cond, exec_for,
nullptr /* iteration_index */, li);
}
else if (group* g = dynamic_cast<group*> (scope_))
@@ -1617,7 +1715,7 @@ namespace build2
bool exec_scope (
exec_lines (g->setup_.begin (), g->setup_.end (),
- exec_assign, exec_cmd, exec_cond,
+ exec_set, exec_cmd, exec_cond, exec_for,
nullptr /* iteration_index */, li));
if (exec_scope)
@@ -1744,19 +1842,19 @@ namespace build2
// UBSan workaround.
//
const diag_frame* df (diag_frame::stack ());
- if (!ctx->sched.async (task_count,
- [] (const diag_frame* ds,
- scope& s,
- script& scr,
- runner& r)
- {
- diag_frame::stack_guard dsg (ds);
- execute_impl (s, scr, r);
- },
- df,
- ref (*chain),
- ref (*script_),
- ref (*runner_)))
+ if (!ctx->sched->async (task_count,
+ [] (const diag_frame* ds,
+ scope& s,
+ script& scr,
+ runner& r)
+ {
+ diag_frame::stack_guard dsg (ds);
+ execute_impl (s, scr, r);
+ },
+ df,
+ ref (*chain),
+ ref (*script_),
+ ref (*runner_)))
{
// Bail out if the scope has failed and we weren't instructed
// to keep going.
@@ -1788,7 +1886,7 @@ namespace build2
ct = command_type::teardown;
exec_lines (g->tdown_.begin (), g->tdown_.end (),
- exec_assign, exec_cmd, exec_cond,
+ exec_set, exec_cmd, exec_cond, exec_for,
nullptr /* iteration_index */, li);
}
else
@@ -1803,7 +1901,8 @@ namespace build2
// The rest.
//
- // When add a special variable don't forget to update lexer::word().
+ // When add a special variable don't forget to update lexer::word() and
+ // for-loop parsing in pre_parse_line().
//
bool parser::
special_variable (const string& n) noexcept
diff --git a/libbuild2/test/script/parser.test.cxx b/libbuild2/test/script/parser.test.cxx
index ab0aee9..6838e47 100644
--- a/libbuild2/test/script/parser.test.cxx
+++ b/libbuild2/test/script/parser.test.cxx
@@ -100,11 +100,32 @@ namespace build2
}
virtual void
- run (scope&,
+ run (scope& env,
const command_expr& e, command_type t,
const iteration_index* ii, size_t i,
- const location&) override
+ const function<command_function>& cf,
+ const location& ll) override
{
+ // If the functions is specified, then just execute it with an empty
+ // stdin so it can perform the housekeeping (stop replaying tokens,
+ // increment line index, etc).
+ //
+ if (cf != nullptr)
+ {
+ assert (e.size () == 1 && !e[0].pipe.empty ());
+
+ const command& c (e[0].pipe.back ());
+
+ // Must be enforced by the caller.
+ //
+ assert (!c.out && !c.err && !c.exit);
+
+ cf (env, c.arguments,
+ fdopen_null (), nullptr /* pipe */,
+ nullopt /* deadline */,
+ ll);
+ }
+
const char* s (nullptr);
switch (t)
diff --git a/libbuild2/test/script/runner.cxx b/libbuild2/test/script/runner.cxx
index 42eef04..98d6868 100644
--- a/libbuild2/test/script/runner.cxx
+++ b/libbuild2/test/script/runner.cxx
@@ -143,6 +143,7 @@ namespace build2
run (scope& sp,
const command_expr& expr, command_type ct,
const iteration_index* ii, size_t li,
+ const function<command_function>& cf,
const location& ll)
{
// Noop for teardown commands if keeping tests output is requested.
@@ -165,18 +166,24 @@ namespace build2
text << ": " << c << expr;
}
- // Print test id once per test expression.
+ // Print test id once per test expression and only for the topmost
+ // one.
//
auto df = make_diag_frame (
- [&sp](const diag_record& dr)
+ [&sp, print = (sp.exec_level == 0)](const diag_record& dr)
{
- // Let's not depend on how the path representation can be improved
- // for readability on printing.
- //
- dr << info << "test id: " << sp.id_path.posix_string ();
+ if (print)
+ {
+ // Let's not depend on how the path representation can be
+ // improved for readability on printing.
+ //
+ dr << info << "test id: " << sp.id_path.posix_string ();
+ }
});
- build2::script::run (sp, expr, ii, li, ll);
+ ++sp.exec_level;
+ build2::script::run (sp, expr, ii, li, ll, cf);
+ --sp.exec_level;
}
bool default_runner::
@@ -188,18 +195,26 @@ namespace build2
if (verb >= 3)
text << ": ?" << expr;
- // Print test id once per test expression.
+ // Print test id once per test expression and only for the topmost
+ // one.
//
auto df = make_diag_frame (
- [&sp](const diag_record& dr)
+ [&sp, print = (sp.exec_level == 0)](const diag_record& dr)
{
- // Let's not depend on how the path representation can be improved
- // for readability on printing.
- //
- dr << info << "test id: " << sp.id_path.posix_string ();
+ if (print)
+ {
+ // Let's not depend on how the path representation can be
+ // improved for readability on printing.
+ //
+ dr << info << "test id: " << sp.id_path.posix_string ();
+ }
});
- return build2::script::run_cond (sp, expr, ii, li, ll);
+ ++sp.exec_level;
+ bool r (build2::script::run_cond (sp, expr, ii, li, ll));
+ --sp.exec_level;
+
+ return r;
}
}
}
diff --git a/libbuild2/test/script/runner.hxx b/libbuild2/test/script/runner.hxx
index 0309a35..687d991 100644
--- a/libbuild2/test/script/runner.hxx
+++ b/libbuild2/test/script/runner.hxx
@@ -48,10 +48,14 @@ namespace build2
// Location is the start position of this command line in the
// testscript. It can be used in diagnostics.
//
+ // Optionally, execute the specified function instead of the last
+ // pipe command.
+ //
virtual void
run (scope&,
const command_expr&, command_type,
const iteration_index*, size_t index,
+ const function<command_function>&,
const location&) = 0;
virtual bool
@@ -88,6 +92,7 @@ namespace build2
run (scope&,
const command_expr&, command_type,
const iteration_index*, size_t,
+ const function<command_function>&,
const location&) override;
virtual bool
diff --git a/libbuild2/test/script/script.cxx b/libbuild2/test/script/script.cxx
index be86117..f7827f6 100644
--- a/libbuild2/test/script/script.cxx
+++ b/libbuild2/test/script/script.cxx
@@ -30,7 +30,7 @@ namespace build2
scope_base::
scope_base (script& s)
: root (s),
- vars (s.test_target.ctx, false /* global */)
+ vars (s.test_target.ctx, false /* shared */) // Note: managed.
{
vars.assign (root.wd_var) = dir_path ();
}
@@ -115,7 +115,7 @@ namespace build2
}
void scope::
- set_variable (string&& nm,
+ set_variable (string nm,
names&& val,
const string& attrs,
const location& ll)
@@ -268,7 +268,7 @@ namespace build2
v = path (n->dir);
else
{
- // Must be a target name.
+ // Must be a target name. Could be from src (e.g., a script).
//
// @@ OUT: what if this is a @-qualified pair of names?
//
@@ -355,7 +355,7 @@ namespace build2
// in parallel). Plus, if there is no such variable, then we cannot
// possibly find any value.
//
- const variable* pvar (context.var_pool.find (n));
+ const variable* pvar (root.target_scope.var_pool ().find (n));
if (pvar == nullptr)
return lookup_type ();
@@ -502,7 +502,7 @@ namespace build2
if (p == string::npos)
{
- v = "'" + v + "'";
+ v = '\'' + v + '\'';
continue;
}
diff --git a/libbuild2/test/script/script.hxx b/libbuild2/test/script/script.hxx
index b75f68e..9409b01 100644
--- a/libbuild2/test/script/script.hxx
+++ b/libbuild2/test/script/script.hxx
@@ -32,6 +32,8 @@ namespace build2
using build2::script::environment_vars;
using build2::script::deadline;
using build2::script::timeout;
+ using build2::script::pipe_command;
+ using build2::script::command_function;
class parser; // Required by VC for 'friend class parser' declaration.
@@ -95,6 +97,22 @@ namespace build2
scope_state state = scope_state::unknown;
+ // The command expression execution nesting level. Can be maintained
+ // by the runner to, for example, only perform some housekeeping on
+ // the topmost level (add the test id to the diagnostics, etc).
+ //
+ // Note that the command expression execution can be nested, so that
+ // the outer expression execution is not completed before all the
+ // inner expressions are executed. As for example in:
+ //
+ // echo 'a b' | for x
+ // echo 'c d' | for y
+ // test $x $y
+ // end
+ // end
+ //
+ size_t exec_level = 0;
+
// Test program paths.
//
// Currently always contains a single element (see test_program() for
@@ -104,8 +122,8 @@ namespace build2
//
small_vector<const path*, 1> test_programs;
- void
- set_variable (string&& name,
+ virtual void
+ set_variable (string name,
names&&,
const string& attrs,
const location&) override;