From 5640e1bc3a22465b3f20d20a2afa112f3f621094 Mon Sep 17 00:00:00 2001 From: AnotherTest Date: Sat, 24 Oct 2020 18:13:02 +0330 Subject: [PATCH] Shell: Add support for brace expansions This adds support for (basic) brace expansions with the following syntaxes: - `{expr?,expr?,expr?,...}` which is directly equivalent to `(expr expr expr ...)`, with the missing expressions replaced with an empty string literal. - `{expr..expr}` which is a new range expansion, with two modes: - if both expressions are one unicode code point long, the range is equivalent to the two code points and all code points between the two (numerically). - if both expressions are numeric, the range is equivalent to both numbers, and all numbers between the two. - otherwise, it is equivalent to `(expr expr)`. Closes #3832. --- Shell/AST.cpp | 181 ++++++++++++++++++++++++++++++++++++++++++ Shell/AST.h | 44 ++++++++++ Shell/Formatter.cpp | 34 ++++++++ Shell/Formatter.h | 2 + Shell/Forward.h | 2 + Shell/NodeVisitor.cpp | 12 +++ Shell/NodeVisitor.h | 2 + Shell/Parser.cpp | 89 ++++++++++++++++++++- Shell/Parser.h | 11 +++ 9 files changed, 374 insertions(+), 3 deletions(-) diff --git a/Shell/AST.cpp b/Shell/AST.cpp index 6f13e1572a0..89e5309f3cc 100644 --- a/Shell/AST.cpp +++ b/Shell/AST.cpp @@ -446,6 +446,66 @@ BarewordLiteral::~BarewordLiteral() { } +void BraceExpansion::dump(int level) const +{ + Node::dump(level); + for (auto& entry : m_entries) + entry.dump(level + 1); +} + +RefPtr BraceExpansion::run(RefPtr shell) +{ + NonnullRefPtrVector values; + for (auto& entry : m_entries) { + auto value = entry.run(shell); + if (value) + values.append(value.release_nonnull()); + } + + return create(move(values)); +} + +HitTestResult BraceExpansion::hit_test_position(size_t offset) +{ + if (!position().contains(offset)) + return {}; + + for (auto& entry : m_entries) { + auto result = entry.hit_test_position(offset); + if (result.matching_node) { + if (!result.closest_command_node) + result.closest_command_node = &entry; + return result; + } + } + + return {}; +} + +void BraceExpansion::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata) +{ + for (auto& entry : m_entries) { + entry.highlight_in_editor(editor, shell, metadata); + metadata.is_first_in_list = false; + } +} + +BraceExpansion::BraceExpansion(Position position, NonnullRefPtrVector entries) + : Node(move(position)) + , m_entries(move(entries)) +{ + for (auto& entry : m_entries) { + if (entry.is_syntax_error()) { + set_is_syntax_error(entry.syntax_error_node()); + break; + } + } +} + +BraceExpansion::~BraceExpansion() +{ +} + void CastToCommand::dump(int level) const { Node::dump(level); @@ -1700,6 +1760,124 @@ PathRedirectionNode::~PathRedirectionNode() { } +void Range::dump(int level) const +{ + Node::dump(level); + print_indented("(From)", level + 1); + m_start->dump(level + 2); + print_indented("(To)", level + 1); + m_end->dump(level + 2); +} + +RefPtr Range::run(RefPtr shell) +{ + constexpr static auto interpolate = [](RefPtr start, RefPtr end, RefPtr shell) -> NonnullRefPtrVector { + NonnullRefPtrVector values; + + if (start->is_string() && end->is_string()) { + auto start_str = start->resolve_as_list(shell)[0]; + auto end_str = end->resolve_as_list(shell)[0]; + + Utf8View start_view { start_str }, end_view { end_str }; + if (start_view.validate() && end_view.validate()) { + if (start_view.length() == 1 && end_view.length() == 1) { + // Interpolate between two code points. + auto start_code_point = *start_view.begin(); + auto end_code_point = *end_view.begin(); + auto step = start_code_point > end_code_point ? -1 : 1; + StringBuilder builder; + for (u32 code_point = start_code_point; code_point != end_code_point; code_point += step) { + builder.clear(); + builder.append_code_point(code_point); + values.append(create(builder.to_string())); + } + // Append the ending code point too, most shells treat this as inclusive. + builder.clear(); + builder.append_code_point(end_code_point); + values.append(create(builder.to_string())); + } else { + // Could be two numbers? + auto start_int = start_str.to_int(); + auto end_int = end_str.to_int(); + if (start_int.has_value() && end_int.has_value()) { + auto start = start_int.value(); + auto end = end_int.value(); + auto step = start > end ? 1 : -1; + for (int value = start; value != end; value += step) + values.append(create(String::number(value))); + // Append the range end too, most shells treat this as inclusive. + values.append(create(String::number(end))); + } else { + goto yield_start_end; + } + } + } else { + yield_start_end:; + warnln("Shell: Cannot interpolate between '{}' and '{}'!", start_str, end_str); + // We can't really interpolate between the two, so just yield both. + values.append(create(move(start_str))); + values.append(create(move(end_str))); + } + + return values; + } + + warnln("Shell: Cannot apply the requested interpolation"); + return values; + }; + + auto start_value = m_start->run(shell); + auto end_value = m_end->run(shell); + if (!start_value || !end_value) + return create({}); + + return create(interpolate(*start_value, *end_value, shell)); +} + +void Range::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata) +{ + m_start->highlight_in_editor(editor, shell, metadata); + + // Highlight the '..' + editor.stylize({ m_start->position().end_offset, m_end->position().start_offset }, { Line::Style::Foreground(Line::Style::XtermColor::Yellow) }); + + metadata.is_first_in_list = false; + m_end->highlight_in_editor(editor, shell, metadata); +} + +HitTestResult Range::hit_test_position(size_t offset) +{ + if (!position().contains(offset)) + return {}; + + auto result = m_start->hit_test_position(offset); + if (result.matching_node) { + if (!result.closest_command_node) + result.closest_command_node = m_start; + return result; + } + + result = m_end->hit_test_position(offset); + if (!result.closest_command_node) + result.closest_command_node = m_end; + return result; +} + +Range::Range(Position position, NonnullRefPtr start, NonnullRefPtr end) + : Node(move(position)) + , m_start(move(start)) + , m_end(move(end)) +{ + if (m_start->is_syntax_error()) + set_is_syntax_error(m_start->syntax_error_node()); + else if (m_end->is_syntax_error()) + set_is_syntax_error(m_end->syntax_error_node()); +} + +Range::~Range() +{ +} + void ReadRedirection::dump(int level) const { Node::dump(level); @@ -2116,6 +2294,9 @@ RefPtr StringLiteral::run(RefPtr) void StringLiteral::highlight_in_editor(Line::Editor& editor, Shell&, HighlightMetadata metadata) { + if (m_text.is_empty()) + return; + Line::Style style { Line::Style::Foreground(Line::Style::XtermColor::Yellow) }; if (metadata.is_first_in_list) style.unify_with({ Line::Style::Bold }); diff --git a/Shell/AST.h b/Shell/AST.h index f9465f59808..178bd12c9a3 100644 --- a/Shell/AST.h +++ b/Shell/AST.h @@ -297,6 +297,10 @@ public: : m_contained_values(move(static_cast&>(values))) { } + ListValue(NonnullRefPtrVector values) + : m_contained_values(move(values)) + { + } const NonnullRefPtrVector& values() const { return m_contained_values; } NonnullRefPtrVector& values() { return m_contained_values; } @@ -433,6 +437,7 @@ public: ListConcatenate, Background, BarewordLiteral, + BraceExpansion, CastToCommand, CastToList, CloseFdRedirection, @@ -450,6 +455,7 @@ public: MatchExpr, Or, Pipe, + Range, ReadRedirection, ReadWriteRedirection, Sequence, @@ -576,6 +582,24 @@ private: String m_text; }; +class BraceExpansion final : public Node { +public: + BraceExpansion(Position, NonnullRefPtrVector); + virtual ~BraceExpansion(); + virtual void visit(NodeVisitor& visitor) override { visitor.visit(this); } + + const NonnullRefPtrVector& entries() const { return m_entries; } + +private: + NODE(BraceExpansion); + virtual void dump(int level) const override; + virtual RefPtr run(RefPtr) override; + virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override; + virtual HitTestResult hit_test_position(size_t) override; + + NonnullRefPtrVector m_entries; +}; + class CastToCommand final : public Node { public: CastToCommand(Position, NonnullRefPtr); @@ -958,6 +982,26 @@ private: NonnullRefPtr m_right; }; +class Range final : public Node { +public: + Range(Position, NonnullRefPtr, NonnullRefPtr); + virtual ~Range(); + virtual void visit(NodeVisitor& visitor) override { visitor.visit(this); } + + const NonnullRefPtr& start() const { return m_start; } + const NonnullRefPtr& end() const { return m_end; } + +private: + NODE(Range); + virtual void dump(int level) const override; + virtual RefPtr run(RefPtr) override; + virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override; + virtual HitTestResult hit_test_position(size_t) override; + + NonnullRefPtr m_start; + NonnullRefPtr m_end; +}; + class ReadRedirection final : public PathRedirectionNode { public: ReadRedirection(Position, int, NonnullRefPtr); diff --git a/Shell/Formatter.cpp b/Shell/Formatter.cpp index 4d5b732e1d5..35a162afd69 100644 --- a/Shell/Formatter.cpp +++ b/Shell/Formatter.cpp @@ -190,6 +190,25 @@ void Formatter::visit(const AST::BarewordLiteral* node) visited(node); } +void Formatter::visit(const AST::BraceExpansion* node) +{ + will_visit(node); + test_and_update_output_cursor(node); + current_builder().append('{'); + + TemporaryChange parent { m_parent_node, node }; + bool first = true; + for (auto& entry : node->entries()) { + if (!first) + current_builder().append(','); + first = false; + entry.visit(*this); + } + + current_builder().append('}'); + visited(node); +} + void Formatter::visit(const AST::CastToCommand* node) { will_visit(node); @@ -480,6 +499,21 @@ void Formatter::visit(const AST::Pipe* node) visited(node); } +void Formatter::visit(const AST::Range* node) +{ + will_visit(node); + test_and_update_output_cursor(node); + current_builder().append('{'); + + TemporaryChange parent { m_parent_node, node }; + node->start()->visit(*this); + current_builder().append(".."); + node->end()->visit(*this); + + current_builder().append('}'); + visited(node); +} + void Formatter::visit(const AST::ReadRedirection* node) { will_visit(node); diff --git a/Shell/Formatter.h b/Shell/Formatter.h index 47a4bdc1197..92c6c0ac36e 100644 --- a/Shell/Formatter.h +++ b/Shell/Formatter.h @@ -59,6 +59,7 @@ private: virtual void visit(const AST::ListConcatenate*) override; virtual void visit(const AST::Background*) override; virtual void visit(const AST::BarewordLiteral*) override; + virtual void visit(const AST::BraceExpansion*) override; virtual void visit(const AST::CastToCommand*) override; virtual void visit(const AST::CastToList*) override; virtual void visit(const AST::CloseFdRedirection*) override; @@ -76,6 +77,7 @@ private: virtual void visit(const AST::MatchExpr*) override; virtual void visit(const AST::Or*) override; virtual void visit(const AST::Pipe*) override; + virtual void visit(const AST::Range*) override; virtual void visit(const AST::ReadRedirection*) override; virtual void visit(const AST::ReadWriteRedirection*) override; virtual void visit(const AST::Sequence*) override; diff --git a/Shell/Forward.h b/Shell/Forward.h index e769910ba0b..d869c02fbf4 100644 --- a/Shell/Forward.h +++ b/Shell/Forward.h @@ -41,6 +41,7 @@ class And; class ListConcatenate; class Background; class BarewordLiteral; +class BraceExpansion; class CastToCommand; class CastToList; class CloseFdRedirection; @@ -58,6 +59,7 @@ class Join; class MatchExpr; class Or; class Pipe; +class Range; class ReadRedirection; class ReadWriteRedirection; class Sequence; diff --git a/Shell/NodeVisitor.cpp b/Shell/NodeVisitor.cpp index 22723d79a2e..01ae2819d14 100644 --- a/Shell/NodeVisitor.cpp +++ b/Shell/NodeVisitor.cpp @@ -55,6 +55,12 @@ void NodeVisitor::visit(const AST::BarewordLiteral*) { } +void NodeVisitor::visit(const AST::BraceExpansion* node) +{ + for (auto& entry : node->entries()) + entry.visit(*this); +} + void NodeVisitor::visit(const AST::CastToCommand* node) { node->inner()->visit(*this); @@ -153,6 +159,12 @@ void NodeVisitor::visit(const AST::Pipe* node) node->right()->visit(*this); } +void NodeVisitor::visit(const AST::Range* node) +{ + node->start()->visit(*this); + node->end()->visit(*this); +} + void NodeVisitor::visit(const AST::ReadRedirection* node) { visit(static_cast(node)); diff --git a/Shell/NodeVisitor.h b/Shell/NodeVisitor.h index 63e6dcb0c35..41d0ece5e5a 100644 --- a/Shell/NodeVisitor.h +++ b/Shell/NodeVisitor.h @@ -37,6 +37,7 @@ public: virtual void visit(const AST::ListConcatenate*); virtual void visit(const AST::Background*); virtual void visit(const AST::BarewordLiteral*); + virtual void visit(const AST::BraceExpansion*); virtual void visit(const AST::CastToCommand*); virtual void visit(const AST::CastToList*); virtual void visit(const AST::CloseFdRedirection*); @@ -54,6 +55,7 @@ public: virtual void visit(const AST::MatchExpr*); virtual void visit(const AST::Or*); virtual void visit(const AST::Pipe*); + virtual void visit(const AST::Range*); virtual void visit(const AST::ReadRedirection*); virtual void visit(const AST::ReadWriteRedirection*); virtual void visit(const AST::Sequence*); diff --git a/Shell/Parser.cpp b/Shell/Parser.cpp index b0584b32756..6252f5e232a 100644 --- a/Shell/Parser.cpp +++ b/Shell/Parser.cpp @@ -25,6 +25,7 @@ */ #include "Parser.h" +#include #include #include #include @@ -940,7 +941,13 @@ RefPtr Parser::parse_expression() return move(expr); }; - if (strchr("&|){} ;<>\n", starting_char) != nullptr) + if (strchr("&|)} ;<>\n", starting_char) != nullptr) + return nullptr; + + if (m_is_in_brace_expansion_spec && starting_char == ',') + return nullptr; + + if (m_is_in_brace_expansion_spec && next_is("..")) return nullptr; if (isdigit(starting_char)) { @@ -1002,6 +1009,13 @@ RefPtr Parser::parse_string_composite() return glob; } + if (auto expansion = parse_brace_expansion()) { + if (auto next_part = parse_string_composite()) + return create(expansion.release_nonnull(), next_part.release_nonnull()); // Concatenate BraceExpansion StringComposite + + return expansion; + } + if (auto bareword = parse_bareword()) { if (auto next_part = parse_string_composite()) return create(bareword.release_nonnull(), next_part.release_nonnull()); // Concatenate Bareword StringComposite @@ -1223,8 +1237,9 @@ RefPtr Parser::parse_bareword() { auto rule_start = push_start(); StringBuilder builder; - auto is_acceptable_bareword_character = [](char c) { - return strchr("\\\"'*$&#|(){} ?;<>\n", c) == nullptr; + auto is_acceptable_bareword_character = [&](char c) { + return strchr("\\\"'*$&#|(){} ?;<>\n", c) == nullptr + && ((m_is_in_brace_expansion_spec && c != ',') || !m_is_in_brace_expansion_spec); }; while (!at_end()) { char ch = peek(); @@ -1239,6 +1254,11 @@ RefPtr Parser::parse_bareword() continue; } + if (m_is_in_brace_expansion_spec && next_is("..")) { + // Don't eat '..' in a brace expansion spec. + break; + } + if (is_acceptable_bareword_character(ch)) { builder.append(consume()); continue; @@ -1343,6 +1363,61 @@ RefPtr Parser::parse_glob() return bareword_part; } +RefPtr Parser::parse_brace_expansion() +{ + auto rule_start = push_start(); + + if (!expect('{')) + return nullptr; + + if (auto spec = parse_brace_expansion_spec()) { + if (!expect('}')) + spec->set_is_syntax_error(create("Expected a close brace '}' to end a brace expansion")); + + return spec; + } + + restore_to(*rule_start); + return nullptr; +} + +RefPtr Parser::parse_brace_expansion_spec() +{ + TemporaryChange is_in_brace_expansion { m_is_in_brace_expansion_spec, true }; + auto rule_start = push_start(); + auto start_expr = parse_expression(); + if (start_expr) { + if (expect("..")) { + if (auto end_expr = parse_expression()) { + if (end_expr->position().start_offset != start_expr->position().end_offset + 2) + end_expr->set_is_syntax_error(create("Expected no whitespace between '..' and the following expression in brace expansion")); + + return create(start_expr.release_nonnull(), end_expr.release_nonnull()); + } + + return create(start_expr.release_nonnull(), create("Expected an expression to end range brace expansion with")); + } + } + + NonnullRefPtrVector subexpressions; + if (start_expr) + subexpressions.append(start_expr.release_nonnull()); + + while (expect(',')) { + auto expr = parse_expression(); + if (expr) { + subexpressions.append(expr.release_nonnull()); + } else { + subexpressions.append(create("")); + } + } + + if (subexpressions.is_empty()) + return nullptr; + + return create(move(subexpressions)); +} + StringView Parser::consume_while(Function condition) { auto start_offset = m_offset; @@ -1353,4 +1428,12 @@ StringView Parser::consume_while(Function condition) return m_input.substring_view(start_offset, m_offset - start_offset); } +bool Parser::next_is(const StringView& next) +{ + auto start = push_start(); + auto res = expect(next); + restore_to(*start); + return res; +} + } diff --git a/Shell/Parser.h b/Shell/Parser.h index a189818014a..ec18dfd2da2 100644 --- a/Shell/Parser.h +++ b/Shell/Parser.h @@ -77,6 +77,8 @@ private: RefPtr parse_comment(); RefPtr parse_bareword(); RefPtr parse_glob(); + RefPtr parse_brace_expansion(); + RefPtr parse_brace_expansion_spec(); template NonnullRefPtr create(Args... args); @@ -86,6 +88,7 @@ private: char consume(); bool expect(char); bool expect(const StringView&); + bool next_is(const StringView&); void restore_to(size_t offset, AST::Position::Line line) { @@ -133,6 +136,8 @@ private: Vector m_rule_start_offsets; Vector m_rule_start_lines; + + bool m_is_in_brace_expansion_spec { false }; }; #if 0 @@ -206,6 +211,7 @@ string_composite :: string string_composite? | variable string_composite? | bareword string_composite? | glob string_composite? + | brace_expansion string_composite? string :: '"' dquoted_string_inner '"' | "'" [^']* "'" @@ -232,6 +238,11 @@ bareword_with_tilde_expansion :: '~' bareword? glob :: [*?] bareword? | bareword [*?] + +brace_expansion :: '{' brace_expansion_spec '}' + +brace_expansion_spec :: expression? (',' expression?)* + | expression '..' expression )"; #endif