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.
This commit is contained in:
AnotherTest 2020-10-24 18:13:02 +03:30 committed by Andreas Kling
parent 567f2f3548
commit 5640e1bc3a
Notes: sideshowbarker 2024-07-19 01:46:16 +09:00
9 changed files with 374 additions and 3 deletions

View file

@ -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<Value> BraceExpansion::run(RefPtr<Shell> shell)
{
NonnullRefPtrVector<Value> values;
for (auto& entry : m_entries) {
auto value = entry.run(shell);
if (value)
values.append(value.release_nonnull());
}
return create<ListValue>(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<Node> 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<Value> Range::run(RefPtr<Shell> shell)
{
constexpr static auto interpolate = [](RefPtr<Value> start, RefPtr<Value> end, RefPtr<Shell> shell) -> NonnullRefPtrVector<Value> {
NonnullRefPtrVector<Value> 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<StringValue>(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<StringValue>(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<StringValue>(String::number(value)));
// Append the range end too, most shells treat this as inclusive.
values.append(create<StringValue>(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<StringValue>(move(start_str)));
values.append(create<StringValue>(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<ListValue>({});
return create<ListValue>(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<Node> start, NonnullRefPtr<Node> 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<Value> StringLiteral::run(RefPtr<Shell>)
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 });

View file

@ -297,6 +297,10 @@ public:
: m_contained_values(move(static_cast<NonnullRefPtrVector<Value>&>(values)))
{
}
ListValue(NonnullRefPtrVector<Value> values)
: m_contained_values(move(values))
{
}
const NonnullRefPtrVector<Value>& values() const { return m_contained_values; }
NonnullRefPtrVector<Value>& 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<Node>);
virtual ~BraceExpansion();
virtual void visit(NodeVisitor& visitor) override { visitor.visit(this); }
const NonnullRefPtrVector<Node>& entries() const { return m_entries; }
private:
NODE(BraceExpansion);
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;
NonnullRefPtrVector<Node> m_entries;
};
class CastToCommand final : public Node {
public:
CastToCommand(Position, NonnullRefPtr<Node>);
@ -958,6 +982,26 @@ private:
NonnullRefPtr<Node> m_right;
};
class Range final : public Node {
public:
Range(Position, NonnullRefPtr<Node>, NonnullRefPtr<Node>);
virtual ~Range();
virtual void visit(NodeVisitor& visitor) override { visitor.visit(this); }
const NonnullRefPtr<Node>& start() const { return m_start; }
const NonnullRefPtr<Node>& end() const { return m_end; }
private:
NODE(Range);
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;
NonnullRefPtr<Node> m_start;
NonnullRefPtr<Node> m_end;
};
class ReadRedirection final : public PathRedirectionNode {
public:
ReadRedirection(Position, int, NonnullRefPtr<Node>);

View file

@ -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<const AST::Node*> 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<const AST::Node*> 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);

View file

@ -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;

View file

@ -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;

View file

@ -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<const AST::PathRedirectionNode*>(node));

View file

@ -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*);

View file

@ -25,6 +25,7 @@
*/
#include "Parser.h"
#include <AK/TemporaryChange.h>
#include <ctype.h>
#include <stdio.h>
#include <unistd.h>
@ -940,7 +941,13 @@ RefPtr<AST::Node> 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<AST::Node> Parser::parse_string_composite()
return glob;
}
if (auto expansion = parse_brace_expansion()) {
if (auto next_part = parse_string_composite())
return create<AST::Juxtaposition>(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<AST::Juxtaposition>(bareword.release_nonnull(), next_part.release_nonnull()); // Concatenate Bareword StringComposite
@ -1223,8 +1237,9 @@ RefPtr<AST::Node> 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<AST::Node> 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<AST::Node> Parser::parse_glob()
return bareword_part;
}
RefPtr<AST::Node> 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<AST::SyntaxError>("Expected a close brace '}' to end a brace expansion"));
return spec;
}
restore_to(*rule_start);
return nullptr;
}
RefPtr<AST::Node> 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<AST::SyntaxError>("Expected no whitespace between '..' and the following expression in brace expansion"));
return create<AST::Range>(start_expr.release_nonnull(), end_expr.release_nonnull());
}
return create<AST::Range>(start_expr.release_nonnull(), create<AST::SyntaxError>("Expected an expression to end range brace expansion with"));
}
}
NonnullRefPtrVector<AST::Node> 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<AST::StringLiteral>(""));
}
}
if (subexpressions.is_empty())
return nullptr;
return create<AST::BraceExpansion>(move(subexpressions));
}
StringView Parser::consume_while(Function<bool(char)> condition)
{
auto start_offset = m_offset;
@ -1353,4 +1428,12 @@ StringView Parser::consume_while(Function<bool(char)> 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;
}
}

View file

@ -77,6 +77,8 @@ private:
RefPtr<AST::Node> parse_comment();
RefPtr<AST::Node> parse_bareword();
RefPtr<AST::Node> parse_glob();
RefPtr<AST::Node> parse_brace_expansion();
RefPtr<AST::Node> parse_brace_expansion_spec();
template<typename A, typename... Args>
NonnullRefPtr<A> 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<size_t> m_rule_start_offsets;
Vector<AST::Position::Line> 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