aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2020-05-26 21:35:59 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2020-06-03 12:26:33 +0300
commit920ed11a433b0e292a18adb8c68829a00e8c70cc (patch)
treee365baf8be68b168e19f42f20c5dde1526c1cbba
parent4001ff053071c09008e88312c4f973c417322a07 (diff)
Allow process path values and targets as buildscript program names
Also deduce the recipe name.
-rw-r--r--libbuild2/build/script/parser+line.test.testscript4
-rw-r--r--libbuild2/build/script/parser.cxx423
-rw-r--r--libbuild2/build/script/parser.hxx47
-rw-r--r--libbuild2/build/script/parser.test.cxx9
-rw-r--r--libbuild2/build/script/runner.cxx10
-rw-r--r--libbuild2/build/script/script.hxx1
-rw-r--r--libbuild2/functions-process.cxx7
-rw-r--r--libbuild2/parser.cxx4
-rw-r--r--libbuild2/rule.cxx21
-rw-r--r--libbuild2/rule.hxx6
-rw-r--r--libbuild2/script/parser.cxx83
-rw-r--r--libbuild2/script/parser.hxx32
-rw-r--r--libbuild2/script/run.cxx56
-rw-r--r--libbuild2/script/script.cxx2
-rw-r--r--libbuild2/script/script.hxx7
-rw-r--r--libbuild2/target.hxx6
-rw-r--r--libbuild2/target.txx18
-rw-r--r--libbuild2/test/script/parser.test.cxx2
-rw-r--r--libbuild2/variable.cxx2
-rw-r--r--tests/dependency/recipe/testscript336
20 files changed, 953 insertions, 123 deletions
diff --git a/libbuild2/build/script/parser+line.test.testscript b/libbuild2/build/script/parser+line.test.testscript
index 6401d91..45b07b7 100644
--- a/libbuild2/build/script/parser+line.test.testscript
+++ b/libbuild2/build/script/parser+line.test.testscript
@@ -38,7 +38,7 @@ $* <<EOF >>EOF
$* <<EOI >>EOO
foo 'bar' "baz" '' ""
"$foo"
- "foo$"
+ "foo$bar"
"fo"o
"foo"\"
"foo\\"
@@ -55,7 +55,7 @@ $* <<EOI >>EOO
EOI
foo 'bar' "baz" '' ""
"$foo"
- "foo$"
+ "foo$bar"
"foo"
"foo\""
"foo\\"
diff --git a/libbuild2/build/script/parser.cxx b/libbuild2/build/script/parser.cxx
index 81e78f3..2a0555e 100644
--- a/libbuild2/build/script/parser.cxx
+++ b/libbuild2/build/script/parser.cxx
@@ -3,10 +3,15 @@
#include <libbuild2/build/script/parser.hxx>
+#include <libbutl/builtin.mxx>
+
+#include <libbuild2/algorithm.hxx>
+
#include <libbuild2/build/script/lexer.hxx>
#include <libbuild2/build/script/runner.hxx>
using namespace std;
+using namespace butl;
namespace build2
{
@@ -21,8 +26,9 @@ namespace build2
//
script parser::
- pre_parse (istream& is, const path_name& pn, uint64_t line,
- optional<string> diag)
+ pre_parse (const target& tg,
+ istream& is, const path_name& pn, uint64_t line,
+ optional<string> diag_name, const location& diag_loc)
{
path_ = &pn;
@@ -31,13 +37,25 @@ namespace build2
lexer l (is, *path_, line, lexer_mode::command_line);
set_lexer (&l);
- script s;
- s.diag = move (diag);
+ // The script shouldn't be able to modify the target/scopes.
+ //
+ target_ = const_cast<target*> (&tg);
+ scope_ = const_cast<scope*> (&tg.base_scope ());
+ root_ = scope_->root_scope ();
+
+ pbase_ = scope_->src_path_;
+ script s;
script_ = &s;
runner_ = nullptr;
environment_ = nullptr;
+ if (diag_name)
+ {
+ diag = make_pair (move (*diag_name), diag_loc);
+ diag_weight = 4;
+ }
+
s.start_loc = location (*path_, line, 1);
token t (pre_parse_script ());
@@ -46,6 +64,36 @@ namespace build2
s.end_loc = get_location (t);
+ // Diagnose absent/ambigous script name.
+ //
+ {
+ diag_record dr;
+
+ if (!diag)
+ {
+ dr << fail (s.start_loc)
+ << "unable to deduce low-verbosity script diagnostics name";
+ }
+ else if (diag2)
+ {
+ dr << fail (s.start_loc)
+ << "low-verbosity script diagnostics name is ambiguous" <<
+ info (diag->second) << "could be '" << diag->first << "'" <<
+ info (diag2->second) << "could be '" << diag2->first << "'";
+ }
+
+ if (!dr.empty ())
+ {
+ dr << info << "consider specifying it explicitly with the 'diag' "
+ << "recipe attribute";
+
+ dr << info << "or provide custom low-verbosity diagnostics with "
+ << "the 'diag' builtin";
+ }
+ }
+
+ s.diag = move (diag->first);
+
return s;
}
@@ -150,16 +198,26 @@ namespace build2
assert (tt == type::newline);
- ln.type = lt;
- ln.tokens = replay_data ();
- script_->lines.push_back (move (ln));
+ //@@ TODO: we need to make sure special builtin is the first command.
- if (lt == line_type::cmd_if || lt == line_type::cmd_ifn)
+ // Save the script line, unless this is a special builtin (indicated
+ // by the replay::stop mode).
+ //
+ if (replay_ == replay::save)
{
- tt = peek (lexer_mode::first_token);
+ 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);
+ pre_parse_if_else (t, tt);
+ }
}
+ else
+ assert (replay_ == replay::stop && lt == line_type::cmd);
}
void parser::
@@ -244,6 +302,333 @@ namespace build2
// Execute.
//
+ optional<process_path> parser::
+ parse_program (token& t, build2::script::token_type& tt, names& ns)
+ {
+ const location l (get_location (t));
+
+ // Set the current script name if it is not set or its weight is less
+ // than the new name weight, skipping names with the zero weight. If
+ // the weight is the same but the name is different then record this
+ // ambiguity, unless one is already recorded. This ambiguity will be
+ // reported at the end of the script pre-parsing, unless discarded by
+ // the name with a greater weight.
+ //
+ auto set_diag = [&l, this] (string d, uint8_t w)
+ {
+ if (diag_weight < w)
+ {
+ diag = make_pair (move (d), l);
+ diag_weight = w;
+ diag2 = nullopt;
+ }
+ else if (w != 0 && w == diag_weight && d != diag->first && !diag2)
+ diag2 = make_pair (move (d), l);
+ };
+
+ // Handle special builtins.
+ //
+ if (pre_parse_)
+ {
+ if (tt == type::word && t.value == "diag")
+ {
+ // @@ Redo the diag directive handling (save line separately and
+ // execute later, before script execution).
+ //
+ if (diag_weight == 4)
+ {
+ fail (script_->start_loc)
+ << "low-verbosity script diagnostics name is ambiguous" <<
+ info (diag->second) << "could be '" << diag->first << "'" <<
+ info (l) << "could be '<name>'";
+ }
+
+ set_diag ("<name>", 4);
+
+ build2::script::parser::parse_program (t, tt, ns);
+ replay_stop ();
+
+ return nullopt;
+ }
+ }
+
+ auto suggest_diag = [this] (const diag_record& dr)
+ {
+ dr << info << "consider specifying it explicitly with "
+ << "the 'diag' recipe attribute";
+
+ dr << info << "or provide custom low-verbosity diagnostics "
+ << " with the 'diag' builtin";
+ };
+
+ parse_names_result pr;
+ {
+ // During pre-parse, if the script name is not set manually, we
+ // suspend pre-parse, parse the command names for real and try to
+ // deduce the script name from the result. Otherwise, we continue to
+ // pre-parse and bail out after parsing the names.
+ //
+ // Note that the later is not just an optimization since expansion
+ // that wouldn't fail during execution may fail in this special
+ // mode, for example:
+ //
+ // ...
+ // {{
+ // x = true
+ // ba($x ? r : z)
+ // }}
+ //
+ // v = a b
+ // ...
+ // {{
+ // v = o
+ // fo$v
+ // }}
+ //
+ // This is also the reason why we add a diag frame.
+ //
+ if (pre_parse_ && diag_weight != 4)
+ {
+ pre_parse_ = false; // Make parse_names() perform expansions.
+ pre_parse_suspended_ = true;
+ }
+
+ auto df = make_diag_frame (
+ [&l, &suggest_diag, this] (const diag_record& dr)
+ {
+ if (pre_parse_suspended_)
+ {
+ dr << info (l) << "while deducing low-verbosity script "
+ << "diagnostics name";
+
+ suggest_diag (dr);
+ }
+ });
+
+ pr = parse_names (t, tt,
+ ns,
+ pattern_mode::ignore,
+ true /* chunk */,
+ "command line",
+ nullptr);
+
+ if (pre_parse_suspended_)
+ {
+ pre_parse_suspended_ = false;
+ pre_parse_ = true;
+ }
+
+ if (pre_parse_ && diag_weight == 4)
+ return nullopt;
+ }
+
+ // Try to translate names into a process path, unless there is nothing
+ // to translate.
+ //
+ // We only end up here in the pre-parse mode if we are still searching
+ // for the script name.
+ //
+ if (!pr.not_null || ns.empty ())
+ {
+ if (pre_parse_)
+ {
+ diag_record dr (fail (l));
+ dr << "unable to deduce low-verbosity script diagnostics name";
+ suggest_diag (dr);
+ }
+
+ return nullopt;
+ }
+
+ // We have to handle process_path[_ex] and executable target. The
+ // process_path[_ex] we may have to recognize syntactically because
+ // of the loss of type, for example:
+ //
+ // c = $cxx.path --version
+ //
+ // {{
+ // $c ...
+ // }}
+ //
+ // This is further complicated by the fact that the first name in
+ // process_path[_ex] may or may not be a pair (it's not a pair if
+ // recall and effective paths are the same). If it's not a pair and we
+ // are dealing with process_path, then we don't need to do anything
+ // extra -- it will just be treated as normal program path. However,
+ // if it's process_path_ex, then we may end up with something along
+ // these lines:
+ //
+ // /usr/bin/g++ name@c++ checksum@deadbeef --version
+ //
+ // Which is a bit harder to recognize syntactically. So what we are
+ // going to do is have a separate first pass which reduces the
+ // syntactic cases to the typed ones.
+ //
+ names pp_ns;
+ if (pr.type == &value_traits<process_path>::value_type ||
+ pr.type == &value_traits<process_path_ex>::value_type)
+ {
+ pp_ns = move (ns);
+ ns.clear ();
+ }
+ else if (ns[0].file ())
+ {
+ // Find the end of the value.
+ //
+ auto b (ns.begin ()), i (b), e (ns.end ());
+ for (i += i->pair ? 2 : 1; i != e && i->pair; i += 2)
+ {
+ if (!i->simple () ||
+ (i->value != "name" && i->value != "checksum"))
+ break;
+ }
+
+ if (b->pair || i != b + 1) // First is a pair or pairs after.
+ {
+ pp_ns.insert (pp_ns.end (),
+ make_move_iterator (b), make_move_iterator (i));
+
+ ns.erase (b, i);
+
+ pr.type = i != b + 1
+ ? &value_traits<process_path_ex>::value_type
+ : &value_traits<process_path>::value_type;
+ }
+ }
+
+ // Handle process_path[_ex], for example:
+ //
+ // {{
+ // $cxx.path ...
+ // }}
+ //
+ if (pr.type == &value_traits<process_path>::value_type)
+ {
+ auto pp (convert<process_path> (move (pp_ns)));
+
+ if (pre_parse_)
+ {
+ diag_record dr (fail (l));
+ dr << "unable to deduce low-verbosity script diagnostics name "
+ << "from process path " << pp;
+ suggest_diag (dr);
+ }
+ else
+ return optional<process_path> (move (pp));
+ }
+ else if (pr.type == &value_traits<process_path_ex>::value_type)
+ {
+ auto pp (convert<process_path_ex> (move (pp_ns)));
+
+ if (pre_parse_)
+ {
+ if (pp.name)
+ {
+ set_diag (move (*pp.name), 3);
+ return nullopt;
+ }
+
+ diag_record dr (fail (l));
+ dr << "unable to deduce low-verbosity script diagnostics name "
+ << "from process path " << pp;
+ suggest_diag (dr);
+ }
+ else
+ return optional<process_path> (move (pp));
+ }
+ //
+ // Handle the executable target, for example:
+ //
+ // import! [metadata] cli = cli%exe{cli}
+ // ...
+ // {{
+ // $cli ...
+ // }}
+ //
+ else if (!ns[0].simple ())
+ {
+ if (const target* t = search_existing (
+ ns[0], *scope_, ns[0].pair ? ns[1].dir : empty_dir_path))
+ {
+ if (const auto* et = t->is_a<exe> ())
+ {
+ if (pre_parse_)
+ {
+ if (auto* n = et->lookup_metadata<string> ("name"))
+ {
+ set_diag (*n, 3);
+ return nullopt;
+ }
+ // Fall through.
+ }
+ else
+ {
+ process_path pp (et->process_path ());
+
+ if (pp.empty ())
+ fail (l) << "target " << *et << " is out of date" <<
+ info << "consider specifying it as a prerequisite of "
+ << environment_->target;
+
+ ns.erase (ns.begin (), ns.begin () + (ns[0].pair ? 2 : 1));
+ return optional<process_path> (move (pp));
+ }
+ }
+
+ if (pre_parse_)
+ {
+ diag_record dr (fail (l));
+ dr << "unable to deduce low-verbosity script diagnostics name "
+ << "from target " << *t;
+ suggest_diag (dr);
+ }
+ }
+
+ if (pre_parse_)
+ {
+ diag_record dr (fail (l));
+ dr << "unable to deduce low-verbosity script diagnostics name "
+ << "from " << ns;
+ suggest_diag (dr);
+ }
+ else
+ return nullopt;
+ }
+ else if (pre_parse_)
+ {
+ // If we are here, the name is simple and is not part of a pair.
+ //
+ string& v (ns[0].value);
+
+ // Try to interpret the name as a builtin.
+ //
+ const builtin_info* bi (builtins.find (v));
+
+ if (bi != nullptr)
+ {
+ set_diag (move (v), bi->weight);
+ return nullopt;
+ }
+ //
+ // Try to interpret the name as a pseudo-builtin.
+ //
+ // Note that both of them has the zero weight and cannot be picked
+ // up as a script name.
+ //
+ else if (v == "set" || v == "exit")
+ {
+ return nullopt;
+ }
+
+ diag_record dr (fail (l));
+ dr << "unable to deduce low-verbosity script diagnostics name "
+ << "for program " << ns[0];
+ suggest_diag (dr);
+ }
+
+ return nullopt;
+ }
+
void parser::
execute (const scope& rs, const scope& bs,
environment& e, const script& s, runner& r)
@@ -256,6 +641,10 @@ namespace build2
// The script shouldn't be able to modify the scopes.
//
+ // Note that for now we don't set target_ since it's not clear what
+ // it could be used for (we need scope_ for calling functions such as
+ // $target.path()).
+ //
root_ = const_cast<scope*> (&rs);
scope_ = const_cast<scope*> (&bs);
pbase_ = scope_->src_path_;
@@ -350,8 +739,10 @@ namespace build2
// In the pre-parse mode collect the referenced variable names for the
// script semantics change tracking.
//
- if (pre_parse_)
+ if (pre_parse_ || pre_parse_suspended_)
{
+ lookup r;
+
// 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.
@@ -363,13 +754,21 @@ namespace build2
}
else if (!name.empty ())
{
+ if (pre_parse_suspended_)
+ {
+ const variable* pvar (scope_->ctx.var_pool.find (name));
+
+ if (pvar != nullptr)
+ r = (*target_)[*pvar];
+ }
+
auto& vars (script_->vars);
if (find (vars.begin (), vars.end (), name) == vars.end ())
vars.push_back (move (name));
}
- return lookup ();
+ return r;
}
if (!qual.empty ())
diff --git a/libbuild2/build/script/parser.hxx b/libbuild2/build/script/parser.hxx
index 5f08209..15d4ede 100644
--- a/libbuild2/build/script/parser.hxx
+++ b/libbuild2/build/script/parser.hxx
@@ -34,8 +34,9 @@ namespace build2
// name.
//
script
- pre_parse (istream&, const path_name&, uint64_t line,
- optional<string> diag);
+ pre_parse (const target&,
+ istream&, const path_name&, uint64_t line,
+ optional<string> diag, const location& diag_loc);
// Recursive descent parser.
//
@@ -82,9 +83,51 @@ namespace build2
virtual lookup
lookup_variable (name&&, string&&, const location&) override;
+ // During execution translate the process path and executable targets
+ // leaving the rest for the base parser to handle.
+ //
+ // During pre-parsing try to deduce the low-verbosity script
+ // diagnostics name.
+ //
+ virtual optional<process_path>
+ parse_program (token&, build2::script::token_type&, names&) override;
+
+ void
+ parse_program_diag (token&, build2::script::token_type&, names&);
+
protected:
script* script_;
+ // Current low-verbosity script diagnostics name and weight.
+ //
+ // During pre-parsing each command leading names are translated into a
+ // potential script name, unless it is set manually (with the diag
+ // directive or via the constructor). The potential script name has a
+ // weight associated with it, so script names with greater weights
+ // override names with lesser weights. The possible weights are:
+ //
+ // 0 - builtins that do not add to the script semantics (exit,
+ // true, etc) and are never picked up as a script name
+ //
+ // [1 2] - other builtins
+ //
+ // 3 - process path or executable target
+ //
+ // 4 - manually set names
+ //
+ // If two potential script names with the same weights are encountered
+ // then this ambiguity is reported unless a higher-weighted name is
+ // encountered later.
+ //
+ optional<pair<string, location>> diag;
+ optional<pair<string, location>> diag2;
+ uint8_t diag_weight = 0;
+
+ // True during pre-parsing when the pre-parse mode is temporarily
+ // suspended to perform expansion.
+ //
+ bool pre_parse_suspended_ = false;
+
// Execute state.
//
runner* runner_;
diff --git a/libbuild2/build/script/parser.test.cxx b/libbuild2/build/script/parser.test.cxx
index 10e1701..de3e839 100644
--- a/libbuild2/build/script/parser.test.cxx
+++ b/libbuild2/build/script/parser.test.cxx
@@ -58,7 +58,7 @@ namespace build2
cout << endl;
- return e.back ().pipe.back ().program.string () == "true";
+ return e.back ().pipe.back ().program.recall.string () == "true";
}
virtual void
@@ -160,7 +160,12 @@ namespace build2
//
parser p (ctx);
path_name nm ("buildfile");
- script s (p.pre_parse (cin, nm, 11 /* line */, nullopt));
+
+ script s (p.pre_parse (tt,
+ cin, nm,
+ 11 /* line */,
+ string ("test"),
+ location (nm, 10)));
switch (m)
{
diff --git a/libbuild2/build/script/runner.cxx b/libbuild2/build/script/runner.cxx
index 315a248..2a59505 100644
--- a/libbuild2/build/script/runner.cxx
+++ b/libbuild2/build/script/runner.cxx
@@ -61,14 +61,14 @@ namespace build2
try
{
- // Note that the temporary directory must be empty to date.
+ // Note that the temporary directory must be empty.
//
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
+ // executed let's add the location anyway to help with
// troubleshooting. And let's stick to that principle down the
// road.
//
@@ -110,8 +110,10 @@ namespace build2
find_if (expr.begin (), expr.end (),
[] (const expr_term& et)
{
- const string& p (et.pipe.back ().program.string ());
- return p == "set" || p == "exit";
+ const process_path& p (et.pipe.back ().program);
+ return p.initial == nullptr &&
+ (p.recall.string () == "set" ||
+ p.recall.string () == "exit");
}) != expr.end ())
build2::script::run (env, expr, li, ll);
else if (verb >= 2)
diff --git a/libbuild2/build/script/script.hxx b/libbuild2/build/script/script.hxx
index 08cdbd2..de759de 100644
--- a/libbuild2/build/script/script.hxx
+++ b/libbuild2/build/script/script.hxx
@@ -37,7 +37,6 @@ namespace build2
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.
//
diff --git a/libbuild2/functions-process.cxx b/libbuild2/functions-process.cxx
index 3fa7819..e6c0582 100644
--- a/libbuild2/functions-process.cxx
+++ b/libbuild2/functions-process.cxx
@@ -93,8 +93,8 @@ namespace build2
return value (move (r));
}
- // Return the builtin function pointer if this is a call to a builtin and
- // NULL otherwise.
+ // Return the builtin function pointer if this is a call to an internal
+ // builtin and NULL otherwise.
//
static builtin_function*
builtin (const names& args)
@@ -106,7 +106,8 @@ namespace build2
if (!nm.simple () || nm.pair)
return nullptr;
- return builtins.find (nm.value);
+ const builtin_info* r (builtins.find (nm.value));
+ return r != nullptr ? r->function : nullptr;
}
// Return the builtin name and its arguments. The builtin function is only
diff --git a/libbuild2/parser.cxx b/libbuild2/parser.cxx
index dada34d..756a45b 100644
--- a/libbuild2/parser.cxx
+++ b/libbuild2/parser.cxx
@@ -1187,7 +1187,7 @@ namespace build2
{
if (d.first)
{
- if (ar->recipe_text (ctx, move (t.value), d.as))
+ if (ar->recipe_text (ctx, *target_, move (t.value), d.as))
d.clean = true;
// Verify we have no unhandled attributes.
@@ -5862,7 +5862,7 @@ namespace build2
{
fail (l) << "invalid value subscript: " << e <<
info (bl) << "use the '\\[' escape sequence if this is a "
- << "wildcard pattern";
+ << "wildcard pattern" << endf;
}
// Similar to expanding an undefined variable, we return NULL if
diff --git a/libbuild2/rule.cxx b/libbuild2/rule.cxx
index 9e2ef95..a5d51ca 100644
--- a/libbuild2/rule.cxx
+++ b/libbuild2/rule.cxx
@@ -365,7 +365,7 @@ namespace build2
// adhoc_script_rule
//
bool adhoc_script_rule::
- recipe_text (context& ctx, string&& t, attributes& as)
+ recipe_text (context& ctx, const target& tg, string&& t, attributes& as)
{
// Handle and erase recipe-specific attributes.
//
@@ -397,7 +397,10 @@ namespace build2
istringstream is (move (t));
build::script::parser p (ctx);
- script = p.pre_parse (is, loc.file, loc.line + 1, move (diag));
+
+ script = p.pre_parse (tg,
+ is, loc.file, loc.line + 1,
+ move (diag), as.loc);
return false;
}
@@ -545,17 +548,9 @@ namespace build2
{
if (auto* e = pt->is_a<exe> ())
{
- if (auto* ns = cast_null<names> (e->vars[ctx.var_export_metadata]))
+ if (auto* c = e->lookup_metadata<string> ("checksum"))
{
- // Metadata variable prefix is in the second name.
- //
- assert (ns->size () == 2 && (*ns)[1].simple ());
-
- if (auto* c = cast_null<string> (
- e->vars[(*ns)[1].value + ".checksum"]))
- {
- prog_cs.append (*c);
- }
+ prog_cs.append (*c);
}
}
}
@@ -778,7 +773,7 @@ namespace build2
}
bool adhoc_cxx_rule::
- recipe_text (context&, string&& t, attributes&)
+ recipe_text (context&, const target&, string&& t, attributes&)
{
code = move (t);
return true;
diff --git a/libbuild2/rule.hxx b/libbuild2/rule.hxx
index 76ab306..a79eeed 100644
--- a/libbuild2/rule.hxx
+++ b/libbuild2/rule.hxx
@@ -134,7 +134,7 @@ namespace build2
// therefore requires cleanup.
//
virtual bool
- recipe_text (context&, string&&, attributes&) = 0;
+ recipe_text (context&, const target&, string&&, attributes&) = 0;
public:
// Some of the operations come in compensating pairs, such as update and
@@ -194,7 +194,7 @@ namespace build2
adhoc_script_rule (const location& l, size_t b): adhoc_rule (l, b) {}
virtual bool
- recipe_text (context&, string&&, attributes&) override;
+ recipe_text (context&, const target&, string&&, attributes&) override;
public:
using script_type = build::script::script;
@@ -251,7 +251,7 @@ namespace build2
adhoc_cxx_rule (const location&, size_t, uint64_t version);
virtual bool
- recipe_text (context&, string&& t, attributes&) override;
+ recipe_text (context&, const target&, string&& t, attributes&) override;
virtual
~adhoc_cxx_rule () override;
diff --git a/libbuild2/script/parser.cxx b/libbuild2/script/parser.cxx
index aa60111..33dc273 100644
--- a/libbuild2/script/parser.cxx
+++ b/libbuild2/script/parser.cxx
@@ -96,6 +96,19 @@ namespace build2
return regex_parts (string (s, 1, rn), s[0], string (s, fp, p - fp));
}
+ optional<process_path> parser::
+ parse_program (token& t, type& tt, names& ns)
+ {
+ parse_names (t, tt,
+ ns,
+ pattern_mode::ignore,
+ true /* chunk */,
+ "command line",
+ nullptr);
+
+ return nullopt;
+ }
+
pair<command_expr, parser::here_docs> parser::
parse_command_expr (token& t, type& tt,
const redirect_aliases& ra)
@@ -279,7 +292,9 @@ namespace build2
{
case pending::none: c.arguments.push_back (move (w)); break;
case pending::program:
- c.program = parse_path (move (w), "program path");
+ c.program = process_path (nullptr /* initial */,
+ parse_path (move (w), "program path"),
+ path () /* effect */);
break;
case pending::out_merge: add_merge (c.out, w, 2); break;
@@ -726,12 +741,20 @@ namespace build2
{
if (pre_parse_)
{
- // The only things we need to handle here are the here-document
- // and here-document regex end markers since we need to know
- // how many of them to pre-parse after the command.
+ // The only things we need to handle here are the tokens that
+ // introduce the next command, since we handle the command
+ // leading name chunks specially, and the here-document and
+ // here-document regex end markers, since we need to know how
+ // many of them to pre-parse after the command.
//
switch (tt)
{
+ case type::pipe:
+ case type::log_or:
+ case type::log_and:
+ p = pending::program;
+ break;
+
case type::in_doc:
case type::out_doc:
mod = move (t.value);
@@ -994,23 +1017,53 @@ namespace build2
}
}
- // Parse the next chunk as simple names to get expansion, etc.
- // Note that we do it in the chunking mode to detect whether
- // anything in each chunk is quoted.
+ // Parse the next chunk as names to get expansion, etc. Note that
+ // we do it in the chunking mode to detect whether anything in
+ // each chunk is quoted. If we are waiting for the command
+ // program, then delegate the parsing to the derived parser, so it
+ // can translate complex program names (targets, process_paths)
+ // during execution and perform some static analysis during
+ // pre-parsing.
//
// @@ PAT: should we support pattern expansion? This is even
// fuzzier than the variable case above. Though this is the
// shell semantics. Think what happens when we do rm *.txt?
//
reset_quoted (t);
- parse_names (t, tt,
- ns,
- pattern_mode::ignore,
- true,
- "command line",
- nullptr);
-
- if (pre_parse_) // Nothing else to do if we are pre-parsing.
+
+ if (p == pending::program)
+ {
+ optional<process_path> pp (parse_program (t, tt, ns));
+
+ // During pre-parsing we are not interested in the
+ // parse_program() call result, so just discard the potentially
+ // unhandled program chunk names.
+ //
+ if (!pre_parse_)
+ {
+ if (pp)
+ {
+ c.program = move (*pp);
+ p = pending::none;
+ }
+ }
+ else
+ {
+ ns.clear ();
+ p = pending::none;
+ }
+ }
+ else
+ parse_names (t, tt,
+ ns,
+ pattern_mode::ignore,
+ true /* chunk */,
+ "command line",
+ nullptr);
+
+ // Nothing else to do if we are pre-parsing.
+ //
+ if (pre_parse_)
break;
// Process what we got. Determine whether anything inside was
diff --git a/libbuild2/script/parser.hxx b/libbuild2/script/parser.hxx
index a63ecde..b15f632 100644
--- a/libbuild2/script/parser.hxx
+++ b/libbuild2/script/parser.hxx
@@ -162,14 +162,42 @@ namespace build2
size_t& li,
variable_pool* = nullptr);
+ // Customization hooks.
+ //
+ protected:
+ // Parse the command's leading name chunk.
+ //
+ // During the execution phase try to parse and translate the leading
+ // names into the process path and return nullopt if choose not to do
+ // so, leaving it to the parser to handle. Also return in the last
+ // argument uninterpreted names, if any.
+ //
+ // The default implementation always returns nullopt. The derived parser
+ // can provide an override that can, for example, handle process path
+ // values, executable targets, etc.
+ //
+ // Note that normally it makes sense to leave simple unpaired names for
+ // the parser to handle, unless there is a good reason not to (e.g.,
+ // it's a special builtin or some such). Such names may contain
+ // something that requires re-lexing, for example `foo|bar`, which won't
+ // be easy to translate but which are handled by the parser.
+ //
+ // During the pre-parsing phase the returned process path and names
+ // (that must still be parsed) are discarded. The main purpose of the
+ // call is to allow implementations to perform static script analysis,
+ // recognize and execute certain directives, or some such.
+ //
+ virtual optional<process_path>
+ parse_program (token&, token_type&, names&);
+
// Set lexer pointers for both the current and the base classes.
//
protected:
void
set_lexer (lexer*);
- // Number of quoted tokens since last reset. Note that this includes
- // the peeked token, if any.
+ // Number of quoted tokens since last reset. Note that this includes the
+ // peeked token, if any.
//
protected:
size_t
diff --git a/libbuild2/script/run.cxx b/libbuild2/script/run.cxx
index 38436b9..5629a15 100644
--- a/libbuild2/script/run.cxx
+++ b/libbuild2/script/run.cxx
@@ -102,7 +102,7 @@ namespace build2
catch (const io_error& e)
{
// While there can be no fault of the script command being currently
- // executed let's add the location anyway to ease the
+ // executed let's add the location anyway to help with
// troubleshooting. And let's stick to that principle down the road.
//
fail (ll) << "unable to read " << p << ": " << e << endf;
@@ -949,7 +949,14 @@ namespace build2
command_pipe::const_iterator nc (bc + 1);
bool last (nc == ec);
- const string& program (c.program.string ());
+ // True if the process path is not pre-searched and the program path
+ // still needs to be resolved.
+ //
+ bool resolve (c.program.initial == nullptr);
+
+ // Program name that may require resolution.
+ //
+ const string& program (c.program.recall.string ());
const redirect& in ((c.in ? *c.in : env.in).effective ());
@@ -961,7 +968,7 @@ namespace build2
auto process_args = [&c] () -> cstrings
{
- cstrings args {c.program.string ().c_str ()};
+ cstrings args {c.program.recall_string ()};
for (const auto& a: c.arguments)
args.push_back (a.c_str ());
@@ -982,7 +989,7 @@ namespace build2
// specify any redirects or exit code check sounds like a right thing
// to do.
//
- if (program == "exit")
+ if (resolve && program == "exit")
{
// In case the builtin is erroneously pipelined from the other
// command, we will close stdin gracefully (reading out the stream
@@ -1150,7 +1157,7 @@ namespace build2
// that. Checking that the user didn't specify any meaningless
// redirects or exit code check sounds as a right thing to do.
//
- if (program == "set")
+ if (resolve && program == "set")
{
if (!last)
fail (ll) << "set builtin must be the last pipe command";
@@ -1322,11 +1329,13 @@ namespace build2
assert (ofd.out.get () != -1 && efd.get () != -1);
optional<process_exit> exit;
- builtin_function* bf (builtins.find (program));
+ const builtin_info* bi (resolve
+ ? builtins.find (program)
+ : nullptr);
bool success;
- if (bf != nullptr)
+ if (bi != nullptr && bi->function != nullptr)
{
// Execute the builtin.
//
@@ -1544,11 +1553,11 @@ namespace build2
try
{
uint8_t r; // Storage.
- builtin b (bf (r,
- c.arguments,
- move (ifd), move (ofd.out), move (efd),
- *env.work_dir.path,
- bcs));
+ builtin b (bi->function (r,
+ c.arguments,
+ move (ifd), move (ofd.out), move (efd),
+ *env.work_dir.path,
+ bcs));
success = run_pipe (env,
nc,
@@ -1570,14 +1579,15 @@ namespace build2
//
cstrings args (process_args ());
- // Resolve the relative not simple program path against the script's
- // working directory. The simple one will be left for the process
- // path search machinery. Also strip the potential leading `^`,
- // indicating that this is an external program rather than a
- // builtin.
+ // If the process path is not pre-searched then resolve the relative
+ // non-simple program path against the script's working directory. The
+ // simple one will be left for the process path search machinery. Also
+ // strip the potential leading `^` (indicates that this is an external
+ // program rather than a builtin).
//
path p;
+ if (resolve)
try
{
p = path (args[0]);
@@ -1610,7 +1620,9 @@ namespace build2
try
{
- process_path pp (process::path_search (args[0]));
+ process_path pp (resolve
+ ? process::path_search (args[0])
+ : process_path ());
// Note: the builtin-escaping character '^' is not printed.
//
@@ -1618,7 +1630,7 @@ namespace build2
print_process (args);
process pr (
- pp,
+ resolve ? pp : c.program,
args.data (),
{ifd.get (), -1}, process::pipe (ofd), {-1, efd.get ()},
env.work_dir.path->string ().c_str ());
@@ -1656,7 +1668,11 @@ namespace build2
if (!success)
return false;
- const path& pr (c.program);
+ // Use the program path for diagnostics (print relative, etc).
+ //
+ const path& pr (resolve
+ ? c.program.recall
+ : path (c.program.recall_string ())); // Can't throw.
// If there is no valid exit code available by whatever reason then we
// print the proper diagnostics, dump stderr (if cached and not too
diff --git a/libbuild2/script/script.cxx b/libbuild2/script/script.cxx
index c85bfd3..7722b47 100644
--- a/libbuild2/script/script.cxx
+++ b/libbuild2/script/script.cxx
@@ -350,7 +350,7 @@ namespace build2
{
// Program.
//
- to_stream_q (o, c.program.string ());
+ to_stream_q (o, c.program.recall_string ());
// Arguments.
//
diff --git a/libbuild2/script/script.hxx b/libbuild2/script/script.hxx
index f4998b7..891b2f6 100644
--- a/libbuild2/script/script.hxx
+++ b/libbuild2/script/script.hxx
@@ -294,7 +294,12 @@ namespace build2
//
struct command
{
- path program;
+ // We use NULL initial as an indication that the path stored in recall
+ // is a program name that still needs to be resolved into the builtin
+ // function or the process path.
+ //
+ process_path program;
+
strings arguments;
optional<redirect> in;
diff --git a/libbuild2/target.hxx b/libbuild2/target.hxx
index eb958c8..667ef79 100644
--- a/libbuild2/target.hxx
+++ b/libbuild2/target.hxx
@@ -1711,6 +1711,12 @@ namespace build2
void
process_path (process_path_type);
+ // Lookup metadata variable (see {import,export}.metadata).
+ //
+ template <typename T>
+ const T*
+ lookup_metadata (const char* var) const;
+
public:
static const target_type static_type;
virtual const target_type& dynamic_type () const {return static_type;}
diff --git a/libbuild2/target.txx b/libbuild2/target.txx
index b482d64..ef14cf5 100644
--- a/libbuild2/target.txx
+++ b/libbuild2/target.txx
@@ -179,4 +179,22 @@ namespace build2
t.prerequisites (move (ps));
return &t;
}
+
+ // exe
+ //
+ template <typename T>
+ const T* exe::
+ lookup_metadata (const char* var) const
+ {
+ if (auto* ns = cast_null<names> (vars[ctx.var_export_metadata]))
+ {
+ // Metadata variable prefix is in the second name.
+ //
+ assert (ns->size () == 2 && (*ns)[1].simple ());
+
+ return cast_null<T> (vars[(*ns)[1].value + '.' + var]);
+ }
+
+ return nullptr;
+ }
}
diff --git a/libbuild2/test/script/parser.test.cxx b/libbuild2/test/script/parser.test.cxx
index a12fda4..69583ae 100644
--- a/libbuild2/test/script/parser.test.cxx
+++ b/libbuild2/test/script/parser.test.cxx
@@ -123,7 +123,7 @@ namespace build2
cout << endl;
- return e.back ().pipe.back ().program.string () == "true";
+ return e.back ().pipe.back ().program.recall.string () == "true";
}
virtual void
diff --git a/libbuild2/variable.cxx b/libbuild2/variable.cxx
index ed710af..e0502ef 100644
--- a/libbuild2/variable.cxx
+++ b/libbuild2/variable.cxx
@@ -1083,6 +1083,8 @@ namespace build2
const string& k ((i++)->value);
+ // NOTE: see also build::script::parser::parse_program().
+ //
if (k == "name")
{
if (!i->simple ())
diff --git a/tests/dependency/recipe/testscript b/tests/dependency/recipe/testscript
index c5e4a8c..9843151 100644
--- a/tests/dependency/recipe/testscript
+++ b/tests/dependency/recipe/testscript
@@ -11,14 +11,15 @@
$* <<EOI 2>>/~%EOE%
alias{x}: alias{z}
{{
- cmd
+ echo
}}
dump alias{x}
EOI
<stdin>:5:1: dump:
% .+/alias\{x\}: .+/:alias\{z\}%
+ % [diag=echo]
{{
- cmd
+ echo
}}
EOE
@@ -27,14 +28,15 @@ EOE
$* <<EOI 2>>/~%EOE%
alias{x y}: alias{z}
{{
- cmd
+ echo
}}
dump alias{y}
EOI
<stdin>:5:1: dump:
% .+/alias\{y\}: .+/:alias\{z\}%
+ % [diag=echo]
{{
- cmd
+ echo
}}
EOE
@@ -44,14 +46,15 @@ $* <<EOI 2>>/~%EOE%
alias{x}:
%
{{
- cmd
+ echo
}}
dump alias{x}
EOI
<stdin>:6:1: dump:
% .+/alias\{x\}:%
+ % [diag=echo]
{{
- cmd
+ echo
}}
EOE
@@ -61,14 +64,15 @@ $* <<EOI 2>>/~%EOE%
alias{x y}:
%
{{
- cmd
+ echo
}}
dump alias{y}
EOI
<stdin>:6:1: dump:
% .+/alias\{y\}:%
+ % [diag=echo]
{{
- cmd
+ echo
}}
EOE
@@ -96,7 +100,7 @@ alias{x}:
var = x
}
{{
- cmd
+ echo
}}
dump alias{x}
EOI
@@ -105,8 +109,9 @@ EOI
{
var = x
}
+ % [diag=echo]
{{
- cmd
+ echo
}}
EOE
@@ -118,7 +123,7 @@ alias{x y}: alias{z}
var = x
}
{{
- cmd
+ echo
}}
dump alias{y}
EOI
@@ -127,8 +132,9 @@ EOI
{
var = x
}
+ % [diag=echo]
{{
- cmd
+ echo
}}
EOE
@@ -141,7 +147,7 @@ alias{x}: alias{z}
}
%
{{
- cmd
+ echo
}}
dump alias{x}
EOI
@@ -150,8 +156,9 @@ EOI
{
var = x
}
+ % [diag=echo]
{{
- cmd
+ echo
}}
EOE
@@ -164,7 +171,7 @@ alias{x y}:
}
%
{{
- cmd
+ echo
}}
dump alias{y}
EOI
@@ -173,8 +180,9 @@ EOI
{
var = x
}
+ % [diag=echo]
{{
- cmd
+ echo
}}
EOE
@@ -183,20 +191,22 @@ EOE
$* <<EOI 2>>/~%EOE%
alias{x}:
{{
- cmd1
+ echo
}}
{{{
- cmd2
+ cat
}}}
dump alias{x}
EOI
<stdin>:8:1: dump:
% .+/alias\{x\}:%
+ % [diag=echo]
{{
- cmd1
+ echo
}}
+ % [diag=cat]
{{{
- cmd2
+ cat
}}}
EOE
@@ -205,20 +215,22 @@ EOE
$* <<EOI 2>>/~%EOE%
alias{x y}: alias{z}
{{
- cmd1
+ echo
}}
{{{
- cmd2
+ cat
}}}
dump alias{y}
EOI
<stdin>:8:1: dump:
% .+/alias\{y\}: .+/:alias\{z\}%
+ % [diag=echo]
{{
- cmd1
+ echo
}}
+ % [diag=cat]
{{{
- cmd2
+ cat
}}}
EOE
@@ -228,22 +240,24 @@ $* <<EOI 2>>/~%EOE%
alias{x}: alias{z}
{{
- cmd1
+ echo
}}
%
{{{
- cmd2
+ cat
}}}
dump alias{x}
EOI
<stdin>:11:1: dump:
% .+/alias\{x\}: .+/:alias\{z\}%
+ % [diag=echo]
{{
- cmd1
+ echo
}}
+ % [diag=cat]
{{{
- cmd2
+ cat
}}}
EOE
@@ -253,22 +267,24 @@ $* <<EOI 2>>/~%EOE%
alias{x y}:
{{
- cmd1
+ echo
}}
%
{{{
- cmd2
+ cat
}}}
dump alias{y}
EOI
<stdin>:11:1: dump:
% .+/alias\{y\}:%
+ % [diag=echo]
{{
- cmd1
+ echo
}}
+ % [diag=cat]
{{{
- cmd2
+ cat
}}}
EOE
@@ -277,7 +293,7 @@ EOE
$* <<EOI 2>>EOE != 0
alias{x}:
{{{
- cmd
+ echo
}}
EOI
<stdin>:5:1: error: unterminated recipe block
@@ -289,7 +305,7 @@ EOE
$* <<EOI 2>>EOE != 0
alias{x}:
{{ $lang
- cmd
+ echo
}}
EOI
<stdin>:2:4: error: expected recipe language instead of '$'
@@ -301,7 +317,7 @@ $* <<EOI 2>>/~!EOE!
alias{x}:
% [diag=gen]
{{
- cmd
+ echo
}}
dump alias{x}
EOI
@@ -309,7 +325,7 @@ EOI
! .+/alias\{x\}:!
% [diag=gen]
{{
- cmd
+ echo
}}
EOE
@@ -319,7 +335,7 @@ $* <<EOI 2>>/~!EOE!
alias{x y}:
% [diag=gen]
{{
- cmd
+ echo
}}
dump alias{y}
EOI
@@ -327,7 +343,7 @@ EOI
! .+/alias\{y\}:!
% [diag=gen]
{{
- cmd
+ echo
}}
EOE
@@ -337,8 +353,250 @@ $* <<EOI 2>>EOE != 0
alias{x}:
%
{
- cmd
+ echo
}
EOI
<stdin>:3:1: error: expected recipe block instead of '{'
EOE
+
+: diag
+:
+{
+ : builtins
+ :
+ {
+ : weight-0
+ :
+ $* <<EOI 2>>EOE != 0
+ alias{x}:
+ {{
+
+ exit
+ }}
+ dump alias{x}
+ EOI
+ <stdin>:3:1: error: unable to deduce low-verbosity script diagnostics name
+ info: consider specifying it explicitly with the 'diag' recipe attribute
+ info: or provide custom low-verbosity diagnostics with the 'diag' builtin
+ EOE
+
+ : weight-1
+ :
+ $* <<EOI 2>>~%EOE%
+ alias{x}:
+ {{
+ true
+ rm b
+ }}
+ dump alias{x}
+ EOI
+ %.{2}
+ % [diag=rm]
+ %.{4}
+ EOE
+
+ : weight-2
+ :
+ $* <<EOI 2>>~%EOE%
+ alias{x}:
+ {{
+ rm a
+ echo a
+ }}
+ dump alias{x}
+ EOI
+ %.{2}
+ % [diag=echo]
+ %.{4}
+ EOE
+
+ : ambiguity
+ :
+ $* <<EOI 2>>EOE != 0
+ alias{x}:
+ {{
+ echo a
+ cat b
+ }}
+ dump alias{x}
+ EOI
+ <stdin>:3:1: error: low-verbosity script diagnostics name is ambiguous
+ <stdin>:3:3: info: could be 'echo'
+ <stdin>:4:3: info: could be 'cat'
+ info: consider specifying it explicitly with the 'diag' recipe attribute
+ info: or provide custom low-verbosity diagnostics with the 'diag' builtin
+ EOE
+ }
+
+ : process-path-ex
+ :
+ {
+ config_cxx = config.cxx=$quote($recall($cxx.path) $cxx.mode, true)
+
+ mkdir build;
+ cat <<EOI >=build/bootstrap.build;
+ project = test
+ amalgamation =
+ subprojects =
+
+ using config
+ EOI
+
+ cat <<EOI >=build/root.build;
+ using cxx
+ EOI
+
+ $* $config_cxx <<EOI 2>>~%EOE%
+ c = $cxx.path --version
+ alias{x}:
+ {{
+ $c
+ }}
+ dump alias{x}
+ EOI
+ %.{2}
+ % [diag=c++]
+ %.{3}
+ EOE
+ }
+
+ : unrecognized
+ :
+ {
+ : expansion-failure
+ :
+ $* <<EOI 2>>EOE != 0
+ alias{x}:
+ {{
+ x = true
+ ech($x ? o : a)
+ }}
+ dump alias{x}
+ EOI
+ <stdin>:4:8: error: invalid bool value: null
+ <stdin>:4:11: info: use the '\?' escape sequence if this is a wildcard pattern
+ <stdin>:4:4: info: while deducing low-verbosity script diagnostics name
+ info: consider specifying it explicitly with the 'diag' recipe attribute
+ info: or provide custom low-verbosity diagnostics with the 'diag' builtin
+ EOE
+
+ : empty
+ :
+ $* <<EOI 2>>EOE != 0
+ alias{x}:
+ {{
+ foo = bar
+ $foo
+ }}
+ dump alias{x}
+ EOI
+ <stdin>:4:3: error: unable to deduce low-verbosity script diagnostics name
+ info: consider specifying it explicitly with the 'diag' recipe attribute
+ info: or provide custom low-verbosity diagnostics with the 'diag' builtin
+ EOE
+
+ : process-path-typed
+ :
+ $* <<EOI 2>>~%EOE% != 0
+ alias{x}:
+ {{
+ $build.path --version
+ }}
+ dump alias{x}
+ EOI
+ %<stdin>:3:4: error: unable to deduce low-verbosity script diagnostics name from process path .+%
+ info: consider specifying it explicitly with the 'diag' recipe attribute
+ info: or provide custom low-verbosity diagnostics with the 'diag' builtin
+ EOE
+
+ : process-path-syntactic
+ :
+ $* <<EOI 2>>~%EOE% != 0
+ b = $build.path
+ alias{x}:
+ {{
+ $b --version
+ }}
+ dump alias{x}
+ EOI
+ %<stdin>:4:4: error: unable to deduce low-verbosity script diagnostics name from process path .+%
+ info: consider specifying it explicitly with the 'diag' recipe attribute
+ info: or provide custom low-verbosity diagnostics with the 'diag' builtin
+ EOE
+
+ : target-no-name
+ :
+ : Disable when cross-testing for the sake of simplicity.
+ :
+ if ($test.target == $build.host)
+ {
+ $* <<EOI 2>>/~%EOE% != 0
+ import! b = build2%exe{b}
+
+ alias{x}: $b
+ {{
+ $b --version
+ }}
+ dump alias{x}
+ EOI
+ %<stdin>:5:3: error: unable to deduce low-verbosity script diagnostics name from target .+/exe\{b\}%
+ info: consider specifying it explicitly with the 'diag' recipe attribute
+ info: or provide custom low-verbosity diagnostics with the 'diag' builtin
+ EOE
+ }
+
+ : program
+ :
+ $* <<EOI 2>>~%EOE% != 0
+ alias{x}:
+ {{
+ echo a
+ foo
+ }}
+ dump alias{x}
+ EOI
+ <stdin>:4:3: error: unable to deduce low-verbosity script diagnostics name for program foo
+ info: consider specifying it explicitly with the 'diag' recipe attribute
+ info: or provide custom low-verbosity diagnostics with the 'diag' builtin
+ EOE
+ }
+
+ : manual
+ :
+ {
+ : attribute
+ :
+ $* <<EOI 2>>~%EOE%
+ alias{x}:
+ % [diag=foo]
+ {{
+ rm a
+ echo b | set c
+ bar
+ }}
+ dump alias{x}
+ EOI
+ %.{2}
+ % [diag=foo]
+ %.{5}
+ EOE
+
+ : diag
+ :
+ $* <<EOI 2>>~%EOE%
+ v = a b
+ alias{x}:
+ {{
+ rm a
+ echo b | set c
+ diag bar
+ fo$v
+ }}
+ dump alias{x}
+ EOI
+ %.{2}
+ % [diag=<name>]
+ %.{5}
+ EOE
+ }
+}