mirror of
https://github.com/RGBCube/serenity
synced 2025-07-26 11:07:35 +00:00
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:
parent
53b85bcdd0
commit
4c6f7846b4
4 changed files with 317 additions and 0 deletions
143
Shell/AST.cpp
143
Shell/AST.cpp
|
@ -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
|
void Or::dump(int level) const
|
||||||
{
|
{
|
||||||
Node::dump(level);
|
Node::dump(level);
|
||||||
|
|
25
Shell/AST.h
25
Shell/AST.h
|
@ -757,6 +757,31 @@ private:
|
||||||
RefPtr<Node> m_right;
|
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 {
|
class Or final : public Node {
|
||||||
public:
|
public:
|
||||||
Or(Position, RefPtr<Node>, RefPtr<Node>);
|
Or(Position, RefPtr<Node>, RefPtr<Node>);
|
||||||
|
|
139
Shell/Parser.cpp
139
Shell/Parser.cpp
|
@ -460,6 +460,9 @@ RefPtr<AST::Node> Parser::parse_control_structure()
|
||||||
if (auto subshell = parse_subshell())
|
if (auto subshell = parse_subshell())
|
||||||
return subshell;
|
return subshell;
|
||||||
|
|
||||||
|
if (auto match = parse_match_expr())
|
||||||
|
return match;
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -627,6 +630,142 @@ RefPtr<AST::Node> Parser::parse_subshell()
|
||||||
return create<AST::Subshell>(move(body));
|
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()
|
RefPtr<AST::Node> Parser::parse_redirection()
|
||||||
{
|
{
|
||||||
auto rule_start = push_start();
|
auto rule_start = push_start();
|
||||||
|
|
|
@ -55,6 +55,9 @@ private:
|
||||||
RefPtr<AST::Node> parse_for_loop();
|
RefPtr<AST::Node> parse_for_loop();
|
||||||
RefPtr<AST::Node> parse_if_expr();
|
RefPtr<AST::Node> parse_if_expr();
|
||||||
RefPtr<AST::Node> parse_subshell();
|
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_redirection();
|
||||||
RefPtr<AST::Node> parse_list_expression();
|
RefPtr<AST::Node> parse_list_expression();
|
||||||
RefPtr<AST::Node> parse_expression();
|
RefPtr<AST::Node> parse_expression();
|
||||||
|
@ -135,6 +138,7 @@ pipe_sequence :: command '|' pipe_sequence
|
||||||
control_structure :: for_expr
|
control_structure :: for_expr
|
||||||
| if_expr
|
| if_expr
|
||||||
| subshell
|
| subshell
|
||||||
|
| match_expr
|
||||||
|
|
||||||
for_expr :: 'for' ws+ (identifier ' '+ 'in' ws*)? expression ws+ '{' toplevel '}'
|
for_expr :: 'for' ws+ (identifier ' '+ 'in' ws*)? expression ws+ '{' toplevel '}'
|
||||||
|
|
||||||
|
@ -145,6 +149,12 @@ else_clause :: else '{' toplevel '}'
|
||||||
|
|
||||||
subshell :: '{' 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
|
command :: redirection command
|
||||||
| list_expression command?
|
| list_expression command?
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue