Shell: Add 'match' expressions

This commit adds an equivalent to the sh 'case' construct, except it's
much more pleasing to look at and write:
```sh
match "$something" {
    p1 { echo "p1!" }
    p2 { echo "p2!" }
    *  { echo "string catch-all!" }
}
```
is the equivalent of:
```sh
case $something in
    p1)
        echo "p1!"
        ;;
    p2)
        echo "p2!"
        ;;
    *)
        echo "catch-all!"
        ;;
esac
```

Since our shell does not treat lists as strings, matching lists is also
possible:

```sh
match (1foo 2foo foo3) {
    (?foo 2* *) { echo wowzers! }
    (* * *) { echo 3-element list catch-all }
}
```
This commit is contained in:
AnotherTest 2020-09-14 19:32:21 +04:30 committed by Andreas Kling
parent 53b85bcdd0
commit 4c6f7846b4
Notes: sideshowbarker 2024-07-19 02:38:07 +09:00
4 changed files with 317 additions and 0 deletions

View File

@ -1347,6 +1347,149 @@ Join::~Join()
{
}
void MatchExpr::dump(int level) const
{
Node::dump(level);
print_indented(String::format("(expression)", m_expr_name.characters()), level + 1);
m_matched_expr->dump(level + 2);
print_indented(String::format("(named: %s)", m_expr_name.characters()), level + 1);
print_indented("(entries)", level + 1);
for (auto& entry : m_entries) {
print_indented("(match)", level + 2);
for (auto& node : entry.options)
node.dump(level + 3);
print_indented("(execute)", level + 2);
if (entry.body)
entry.body->dump(level + 3);
else
print_indented("(nothing)", level + 3);
}
}
RefPtr<Value> MatchExpr::run(RefPtr<Shell> shell)
{
auto value = m_matched_expr->run(shell)->resolve_without_cast(shell);
auto list = value->resolve_as_list(shell);
auto list_matches = [&](auto&& pattern) {
if (pattern.size() != list.size())
return false;
for (size_t i = 0; i < pattern.size(); ++i) {
if (!list[i].matches(pattern[i]))
return false;
}
return true;
};
auto resolve_pattern = [&](auto& option) {
Vector<String> pattern;
if (option.is_glob()) {
pattern.append(static_cast<const Glob*>(&option)->text());
} else if (option.is_bareword()) {
pattern.append(static_cast<const BarewordLiteral*>(&option)->text());
} else if (option.is_list()) {
auto list = option.run(shell);
option.for_each_entry(shell, [&](auto&& value) {
pattern.append(value->resolve_as_list(nullptr)); // Note: 'nullptr' incurs special behaviour,
// asking the node for a 'raw' value.
return IterationDecision::Continue;
});
}
return pattern;
};
auto frame = shell->push_frame();
if (!m_expr_name.is_empty())
shell->set_local_variable(m_expr_name, value);
for (auto& entry : m_entries) {
for (auto& option : entry.options) {
if (list_matches(resolve_pattern(option))) {
if (entry.body)
return entry.body->run(shell);
else
return create<AST::ListValue>({});
}
}
}
// FIXME: Somehow raise an error in the shell.
dbg() << "Non-exhaustive match rules!";
return create<AST::ListValue>({});
}
void MatchExpr::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata)
{
editor.stylize({ m_position.start_offset, m_position.start_offset + 5 }, { Line::Style::Foreground(Line::Style::XtermColor::Yellow) });
if (m_as_position.has_value())
editor.stylize({ m_as_position.value().start_offset, m_as_position.value().end_offset }, { Line::Style::Foreground(Line::Style::XtermColor::Yellow) });
metadata.is_first_in_list = false;
if (m_matched_expr)
m_matched_expr->highlight_in_editor(editor, shell, metadata);
for (auto& entry : m_entries) {
metadata.is_first_in_list = false;
for (auto& option : entry.options)
option.highlight_in_editor(editor, shell, metadata);
metadata.is_first_in_list = true;
if (entry.body)
entry.body->highlight_in_editor(editor, shell, metadata);
for (auto& position : entry.pipe_positions)
editor.stylize({ position.start_offset, position.end_offset }, { Line::Style::Foreground(Line::Style::XtermColor::Yellow) });
}
}
HitTestResult MatchExpr::hit_test_position(size_t offset)
{
if (!position().contains(offset))
return {};
auto result = m_matched_expr->hit_test_position(offset);
if (result.matching_node)
return result;
for (auto& entry : m_entries) {
if (!entry.body)
continue;
auto result = entry.body->hit_test_position(offset);
if (result.matching_node)
return result;
}
return {};
}
MatchExpr::MatchExpr(Position position, RefPtr<Node> expr, String name, Optional<Position> as_position, Vector<MatchEntry> entries)
: Node(move(position))
, m_matched_expr(move(expr))
, m_expr_name(move(name))
, m_as_position(move(as_position))
, m_entries(move(entries))
{
if (m_matched_expr && m_matched_expr->is_syntax_error()) {
set_is_syntax_error(m_matched_expr->syntax_error_node());
} else {
for (auto& entry : m_entries) {
if (!entry.body)
continue;
if (entry.body->is_syntax_error()) {
set_is_syntax_error(entry.body->syntax_error_node());
break;
}
}
}
}
MatchExpr::~MatchExpr()
{
}
void Or::dump(int level) const
{
Node::dump(level);

View File

@ -757,6 +757,31 @@ private:
RefPtr<Node> m_right;
};
struct MatchEntry {
NonnullRefPtrVector<Node> options;
Vector<Position> pipe_positions;
RefPtr<Node> body;
};
class MatchExpr final : public Node {
public:
MatchExpr(Position, RefPtr<Node> expr, String name, Optional<Position> as_position, Vector<MatchEntry> entries);
virtual ~MatchExpr();
private:
virtual void dump(int level) const override;
virtual RefPtr<Value> run(RefPtr<Shell>) override;
virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
virtual HitTestResult hit_test_position(size_t) override;
virtual String class_name() const override { return "MatchExpr"; }
virtual bool would_execute() const override { return true; }
RefPtr<Node> m_matched_expr;
String m_expr_name;
Optional<Position> m_as_position;
Vector<MatchEntry> m_entries;
};
class Or final : public Node {
public:
Or(Position, RefPtr<Node>, RefPtr<Node>);

View File

@ -460,6 +460,9 @@ RefPtr<AST::Node> Parser::parse_control_structure()
if (auto subshell = parse_subshell())
return subshell;
if (auto match = parse_match_expr())
return match;
return nullptr;
}
@ -627,6 +630,142 @@ RefPtr<AST::Node> Parser::parse_subshell()
return create<AST::Subshell>(move(body));
}
RefPtr<AST::Node> Parser::parse_match_expr()
{
auto rule_start = push_start();
if (!expect("match"))
return nullptr;
if (consume_while(is_whitespace).is_empty()) {
m_offset = rule_start->offset;
return nullptr;
}
auto match_expression = parse_expression();
if (!match_expression) {
return create<AST::MatchExpr>(
create<AST::SyntaxError>("Expected an expression after 'match'"),
String {}, Optional<AST::Position> {}, Vector<AST::MatchEntry> {});
}
consume_while(is_any_of(" \t\n"));
String match_name;
Optional<AST::Position> as_position;
auto as_start = m_offset;
if (expect("as")) {
as_position = AST::Position { as_start, m_offset };
if (consume_while(is_any_of(" \t\n")).is_empty()) {
auto node = create<AST::MatchExpr>(
move(match_expression),
String {}, move(as_position), Vector<AST::MatchEntry> {});
node->set_is_syntax_error(create<AST::SyntaxError>("Expected whitespace after 'as' in 'match'"));
return node;
}
match_name = consume_while(is_word_character);
if (match_name.is_empty()) {
auto node = create<AST::MatchExpr>(
move(match_expression),
String {}, move(as_position), Vector<AST::MatchEntry> {});
node->set_is_syntax_error(create<AST::SyntaxError>("Expected an identifier after 'as' in 'match'"));
return node;
}
}
consume_while(is_any_of(" \t\n"));
if (!expect('{')) {
auto node = create<AST::MatchExpr>(
move(match_expression),
move(match_name), move(as_position), Vector<AST::MatchEntry> {});
node->set_is_syntax_error(create<AST::SyntaxError>("Expected an open brace '{' to start a 'match' entry list"));
return node;
}
consume_while(is_any_of(" \t\n"));
Vector<AST::MatchEntry> entries;
for (;;) {
auto entry = parse_match_entry();
consume_while(is_any_of(" \t\n"));
if (entry.options.is_empty())
break;
entries.append(entry);
}
consume_while(is_any_of(" \t\n"));
if (!expect('}')) {
auto node = create<AST::MatchExpr>(
move(match_expression),
move(match_name), move(as_position), move(entries));
node->set_is_syntax_error(create<AST::SyntaxError>("Expected a close brace '}' to end a 'match' entry list"));
return node;
}
return create<AST::MatchExpr>(move(match_expression), move(match_name), move(as_position), move(entries));
}
AST::MatchEntry Parser::parse_match_entry()
{
auto rule_start = push_start();
NonnullRefPtrVector<AST::Node> patterns;
Vector<AST::Position> pipe_positions;
auto pattern = parse_match_pattern();
if (!pattern)
return { {}, {}, create<AST::SyntaxError>("Expected a pattern in 'match' body") };
patterns.append(pattern.release_nonnull());
consume_while(is_any_of(" \t\n"));
auto previous_pipe_start_position = m_offset;
RefPtr<AST::SyntaxError> error;
while (expect('|')) {
pipe_positions.append({ previous_pipe_start_position, m_offset });
consume_while(is_any_of(" \t\n"));
auto pattern = parse_match_pattern();
if (!pattern) {
error = create<AST::SyntaxError>("Expected a pattern to follow '|' in 'match' body");
break;
}
consume_while(is_any_of(" \t\n"));
patterns.append(pattern.release_nonnull());
}
consume_while(is_any_of(" \t\n"));
if (!expect('{')) {
if (!error)
error = create<AST::SyntaxError>("Expected an open brace '{' to start a match entry body");
}
auto body = parse_toplevel();
if (!expect('}')) {
if (!error)
error = create<AST::SyntaxError>("Expected a close brace '}' to end a match entry body");
}
if (body && error)
body->set_is_syntax_error(*error);
else if (error)
body = error;
return { move(patterns), move(pipe_positions), move(body) };
}
RefPtr<AST::Node> Parser::parse_match_pattern()
{
return parse_expression();
}
RefPtr<AST::Node> Parser::parse_redirection()
{
auto rule_start = push_start();

View File

@ -55,6 +55,9 @@ private:
RefPtr<AST::Node> parse_for_loop();
RefPtr<AST::Node> parse_if_expr();
RefPtr<AST::Node> parse_subshell();
RefPtr<AST::Node> parse_match_expr();
AST::MatchEntry parse_match_entry();
RefPtr<AST::Node> parse_match_pattern();
RefPtr<AST::Node> parse_redirection();
RefPtr<AST::Node> parse_list_expression();
RefPtr<AST::Node> parse_expression();
@ -135,6 +138,7 @@ pipe_sequence :: command '|' pipe_sequence
control_structure :: for_expr
| if_expr
| subshell
| match_expr
for_expr :: 'for' ws+ (identifier ' '+ 'in' ws*)? expression ws+ '{' toplevel '}'
@ -145,6 +149,12 @@ else_clause :: else '{' toplevel '}'
subshell :: '{' toplevel '}'
match_expr :: 'match' ws+ expression ws* ('as' ws+ identifier)? '{' match_entry* '}'
match_entry :: match_pattern ws* '{' toplevel '}'
match_pattern :: expression (ws* '|' ws* expression)*
command :: redirection command
| list_expression command?