From 3b8fa5a75387d1b9a609dca1d911bebfacdf7060 Mon Sep 17 00:00:00 2001 From: AnotherTest Date: Sat, 13 Mar 2021 03:10:18 +0330 Subject: [PATCH] Shell: Add support for indexing into variables Now a variable may have an optional slice (only _one_ slice), which can also use negative indices to index from the end. This works on both lists and strings. The contents of the slice have the same semantics as brace expansions. For example: ```sh $ x=(1 2 3 4 5 6) $ echo $x[1..3] # select indices 1, 2, 3 2 3 4 $ echo $x[3,4,1,0] # select indices 3, 4, 1, 0 (in that order) 4 5 2 1 $ x="Well Hello Friends!" $ echo $x[5..9] Hello ``` --- Userland/Shell/AST.cpp | 259 ++++++++++++++++++++++++++++----- Userland/Shell/AST.h | 66 ++++++++- Userland/Shell/Formatter.cpp | 45 ++++-- Userland/Shell/Formatter.h | 1 + Userland/Shell/Forward.h | 1 + Userland/Shell/NodeVisitor.cpp | 13 +- Userland/Shell/NodeVisitor.h | 1 + Userland/Shell/Parser.cpp | 51 ++++++- Userland/Shell/Parser.h | 8 +- Userland/Shell/Shell.cpp | 1 + Userland/Shell/Shell.h | 1 + 11 files changed, 391 insertions(+), 56 deletions(-) diff --git a/Userland/Shell/AST.cpp b/Userland/Shell/AST.cpp index 02c873e241..3c0cab49ca 100644 --- a/Userland/Shell/AST.cpp +++ b/Userland/Shell/AST.cpp @@ -173,6 +173,101 @@ static inline Vector join_commands(Vector left, Vector shell, String&& input_value, NonnullRefPtrVector slices) +{ + if (slices.is_empty()) + return move(input_value); + + for (auto& slice : slices) { + auto value = slice.run(shell); + if (!value) { + shell->raise_error(Shell::ShellError::InvalidSliceContentsError, "Invalid slice contents", slice.position()); + return move(input_value); + } + + auto index_values = value->resolve_as_list(shell); + Vector indices; + indices.ensure_capacity(index_values.size()); + + size_t i = 0; + for (auto& value : index_values) { + auto maybe_index = value.to_int(); + if (!maybe_index.has_value()) { + shell->raise_error(Shell::ShellError::InvalidSliceContentsError, String::formatted("Invalid value in slice index {}: {} (expected a number)", i, value), slice.position()); + return move(input_value); + } + ++i; + + auto index = maybe_index.value(); + auto original_index = index; + if (index < 0) + index += input_value.length(); + + if (index < 0 || (size_t)index >= input_value.length()) { + shell->raise_error(Shell::ShellError::InvalidSliceContentsError, String::formatted("Slice index {} (evaluated as {}) out of value bounds [0-{})", index, original_index, input_value.length()), slice.position()); + return move(input_value); + } + indices.unchecked_append(index); + } + + StringBuilder builder { indices.size() }; + for (auto& index : indices) + builder.append(input_value[index]); + + input_value = builder.build(); + } + + return move(input_value); +} + +static Vector resolve_slices(RefPtr shell, Vector&& values, NonnullRefPtrVector slices) +{ + if (slices.is_empty()) + return move(values); + + for (auto& slice : slices) { + auto value = slice.run(shell); + if (!value) { + shell->raise_error(Shell::ShellError::InvalidSliceContentsError, "Invalid slice contents", slice.position()); + return move(values); + } + + auto index_values = value->resolve_as_list(shell); + Vector indices; + indices.ensure_capacity(index_values.size()); + + size_t i = 0; + for (auto& value : index_values) { + auto maybe_index = value.to_int(); + if (!maybe_index.has_value()) { + shell->raise_error(Shell::ShellError::InvalidSliceContentsError, String::formatted("Invalid value in slice index {}: {} (expected a number)", i, value), slice.position()); + return move(values); + } + ++i; + + auto index = maybe_index.value(); + auto original_index = index; + if (index < 0) + index += values.size(); + + if (index < 0 || (size_t)index >= values.size()) { + shell->raise_error(Shell::ShellError::InvalidSliceContentsError, String::formatted("Slice index {} (evaluated as {}) out of value bounds [0-{})", index, original_index, values.size()), slice.position()); + return move(values); + } + indices.unchecked_append(index); + } + + Vector result; + result.ensure_capacity(indices.size()); + for (auto& index : indices) + result.unchecked_append(values[index]); + + values = move(result); + } + + return move(values); +} + void Node::for_each_entry(RefPtr shell, Function)> callback) { auto value = run(shell)->resolve_without_cast(shell); @@ -2529,23 +2624,73 @@ Subshell::~Subshell() { } +void Slice::dump(int level) const +{ + Node::dump(level); + m_selector->dump(level + 1); +} + +RefPtr Slice::run(RefPtr shell) +{ + return m_selector->run(shell); +} + +void Slice::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata) +{ + m_selector->highlight_in_editor(editor, shell, metadata); +} + +HitTestResult Slice::hit_test_position(size_t offset) const +{ + return m_selector->hit_test_position(offset); +} + +Vector Slice::complete_for_editor(Shell& shell, size_t offset, const HitTestResult& hit_test_result) +{ + // TODO: Maybe intercept this, and suggest values in range? + return m_selector->complete_for_editor(shell, offset, hit_test_result); +} + +Slice::Slice(Position position, NonnullRefPtr selector) + : Node(move(position)) + , m_selector(move(selector)) +{ + if (m_selector->is_syntax_error()) + set_is_syntax_error(m_selector->syntax_error_node()); +} + +Slice::~Slice() +{ +} + void SimpleVariable::dump(int level) const { Node::dump(level); - print_indented(m_name, level + 1); + print_indented("(Name)", level + 1); + print_indented(m_name, level + 2); + print_indented("(Slice)", level + 1); + if (m_slice) + m_slice->dump(level + 2); + else + print_indented("(None)", level + 2); } RefPtr SimpleVariable::run(RefPtr) { - return create(m_name); + NonnullRefPtr value = create(m_name); + if (m_slice) + value = value->with_slices(*m_slice); + return value; } -void SimpleVariable::highlight_in_editor(Line::Editor& editor, Shell&, HighlightMetadata metadata) +void SimpleVariable::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata) { Line::Style style { Line::Style::Foreground(214, 112, 214) }; if (metadata.is_first_in_list) style.unify_with({ Line::Style::Bold }); editor.stylize({ m_position.start_offset, m_position.end_offset }, move(style)); + if (m_slice) + m_slice->highlight_in_editor(editor, shell, metadata); } HitTestResult SimpleVariable::hit_test_position(size_t offset) const @@ -2553,6 +2698,9 @@ HitTestResult SimpleVariable::hit_test_position(size_t offset) const if (!position().contains(offset)) return {}; + if (m_slice && m_slice->position().contains(offset)) + return m_slice->hit_test_position(offset); + return { this, this, nullptr }; } @@ -2574,7 +2722,7 @@ Vector SimpleVariable::complete_for_editor(Shell& sh } SimpleVariable::SimpleVariable(Position position, String name) - : Node(move(position)) + : VariableNode(move(position)) , m_name(move(name)) { } @@ -2586,17 +2734,28 @@ SimpleVariable::~SimpleVariable() void SpecialVariable::dump(int level) const { Node::dump(level); + print_indented("(Name)", level + 1); print_indented(String { &m_name, 1 }, level + 1); + print_indented("(Slice)", level + 1); + if (m_slice) + m_slice->dump(level + 2); + else + print_indented("(None)", level + 2); } RefPtr SpecialVariable::run(RefPtr) { - return create(m_name); + NonnullRefPtr value = create(m_name); + if (m_slice) + value = value->with_slices(*m_slice); + return value; } -void SpecialVariable::highlight_in_editor(Line::Editor& editor, Shell&, HighlightMetadata) +void SpecialVariable::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata) { editor.stylize({ m_position.start_offset, m_position.end_offset }, { Line::Style::Foreground(214, 112, 214) }); + if (m_slice) + m_slice->highlight_in_editor(editor, shell, metadata); } Vector SpecialVariable::complete_for_editor(Shell&, size_t, const HitTestResult&) @@ -2609,11 +2768,14 @@ HitTestResult SpecialVariable::hit_test_position(size_t offset) const if (!position().contains(offset)) return {}; + if (m_slice && m_slice->position().contains(offset)) + return m_slice->hit_test_position(offset); + return { this, this, nullptr }; } SpecialVariable::SpecialVariable(Position position, char name) - : Node(move(position)) + : VariableNode(move(position)) , m_name(name) { } @@ -3091,6 +3253,20 @@ ListValue::ListValue(Vector values) m_contained_values.append(adopt(*new StringValue(move(str)))); } +NonnullRefPtr Value::with_slices(NonnullRefPtr slice) const& +{ + auto value = clone(); + value->m_slices.append(move(slice)); + return value; +} + +NonnullRefPtr Value::with_slices(NonnullRefPtrVector slices) const& +{ + auto value = clone(); + value->m_slices.append(move(slices)); + return value; +} + ListValue::~ListValue() { } @@ -3101,7 +3277,7 @@ Vector ListValue::resolve_as_list(RefPtr shell) for (auto& value : m_contained_values) values.append(value.resolve_as_list(shell)); - return values; + return resolve_slices(shell, move(values), m_slices); } NonnullRefPtr ListValue::resolve_without_cast(RefPtr shell) @@ -3110,7 +3286,10 @@ NonnullRefPtr ListValue::resolve_without_cast(RefPtr shell) for (auto& value : m_contained_values) values.append(value.resolve_without_cast(shell)); - return create(move(values)); + NonnullRefPtr value = create(move(values)); + if (!m_slices.is_empty()) + value = value->with_slices(m_slices); + return value; } CommandValue::~CommandValue() @@ -3121,9 +3300,9 @@ CommandSequenceValue::~CommandSequenceValue() { } -Vector CommandSequenceValue::resolve_as_list(RefPtr) +Vector CommandSequenceValue::resolve_as_list(RefPtr shell) { - // TODO: Somehow raise an "error". + shell->raise_error(Shell::ShellError::EvaluatedSyntaxError, "Unexpected cast of a command sequence to a list"); return {}; } @@ -3132,9 +3311,9 @@ Vector CommandSequenceValue::resolve_as_commands(RefPtr) return m_contained_values; } -Vector CommandValue::resolve_as_list(RefPtr) +Vector CommandValue::resolve_as_list(RefPtr shell) { - // TODO: Somehow raise an "error". + shell->raise_error(Shell::ShellError::EvaluatedSyntaxError, "Unexpected cast of a command to a list"); return {}; } @@ -3150,7 +3329,7 @@ JobValue::~JobValue() StringValue::~StringValue() { } -Vector StringValue::resolve_as_list(RefPtr) +Vector StringValue::resolve_as_list(RefPtr shell) { if (is_list()) { auto parts = StringView(m_string).split_view(m_split, m_keep_empty); @@ -3158,16 +3337,16 @@ Vector StringValue::resolve_as_list(RefPtr) result.ensure_capacity(parts.size()); for (auto& part : parts) result.append(part); - return result; + return resolve_slices(shell, move(result), m_slices); } - return { m_string }; + return { resolve_slices(shell, String { m_string }, m_slices) }; } NonnullRefPtr StringValue::resolve_without_cast(RefPtr shell) { if (is_list()) - return create(resolve_as_list(shell)); + return create(resolve_as_list(shell)); // No need to reapply the slices. return *this; } @@ -3178,12 +3357,12 @@ GlobValue::~GlobValue() Vector GlobValue::resolve_as_list(RefPtr shell) { if (!shell) - return { m_glob }; + return { resolve_slices(shell, String { m_glob }, m_slices) }; auto results = shell->expand_globs(m_glob, shell->cwd); if (results.is_empty()) shell->raise_error(Shell::ShellError::InvalidGlobError, "Glob did not match anything!", m_generation_position); - return results; + return resolve_slices(shell, move(results), m_slices); } SimpleVariableValue::~SimpleVariableValue() @@ -3192,29 +3371,31 @@ SimpleVariableValue::~SimpleVariableValue() Vector SimpleVariableValue::resolve_as_list(RefPtr shell) { if (!shell) - return {}; + return resolve_slices(shell, Vector {}, m_slices); if (auto value = resolve_without_cast(shell); value != this) return value->resolve_as_list(shell); char* env_value = getenv(m_name.characters()); if (env_value == nullptr) - return { "" }; + return { resolve_slices(shell, "", m_slices) }; - Vector res; - String str_env_value = String(env_value); - const auto& split_text = str_env_value.split_view(' '); - for (auto& part : split_text) - res.append(part); - return res; + return { resolve_slices(shell, String { env_value }, m_slices) }; } NonnullRefPtr SimpleVariableValue::resolve_without_cast(RefPtr shell) { VERIFY(shell); - if (auto value = shell->lookup_local_variable(m_name)) - return value.release_nonnull(); + if (auto value = shell->lookup_local_variable(m_name)) { + auto result = value.release_nonnull(); + // If a slice is applied, add it. + if (!m_slices.is_empty()) + result = result->with_slices(m_slices); + + return result; + } + return *this; } @@ -3229,24 +3410,24 @@ Vector SpecialVariableValue::resolve_as_list(RefPtr shell) switch (m_name) { case '?': - return { String::number(shell->last_return_code) }; + return { resolve_slices(shell, String::number(shell->last_return_code), m_slices) }; case '$': - return { String::number(getpid()) }; + return { resolve_slices(shell, String::number(getpid()), m_slices) }; case '*': if (auto argv = shell->lookup_local_variable("ARGV")) - return argv->resolve_as_list(shell); - return {}; + return resolve_slices(shell, argv->resolve_as_list(shell), m_slices); + return resolve_slices(shell, Vector {}, m_slices); case '#': if (auto argv = shell->lookup_local_variable("ARGV")) { if (argv->is_list()) { auto list_argv = static_cast(argv.ptr()); - return { String::number(list_argv->values().size()) }; + return { resolve_slices(shell, String::number(list_argv->values().size()), m_slices) }; } - return { "1" }; + return { resolve_slices(shell, "1", m_slices) }; } - return { "0" }; + return { resolve_slices(shell, "0", m_slices) }; default: - return { "" }; + return { resolve_slices(shell, "", m_slices) }; } } @@ -3260,9 +3441,9 @@ Vector TildeValue::resolve_as_list(RefPtr shell) builder.append(m_username); if (!shell) - return { builder.to_string() }; + return { resolve_slices(shell, builder.to_string(), m_slices) }; - return { shell->expand_tilde(builder.to_string()) }; + return { resolve_slices(shell, shell->expand_tilde(builder.to_string()), m_slices) }; } Result, String> CloseRedirection::apply() const diff --git a/Userland/Shell/AST.h b/Userland/Shell/AST.h index f013a8e07b..54aad9c4b7 100644 --- a/Userland/Shell/AST.h +++ b/Userland/Shell/AST.h @@ -244,6 +244,9 @@ public: virtual Vector resolve_as_list(RefPtr) = 0; virtual Vector resolve_as_commands(RefPtr); virtual NonnullRefPtr resolve_without_cast(RefPtr) { return *this; } + virtual NonnullRefPtr clone() const = 0; + virtual NonnullRefPtr with_slices(NonnullRefPtr slice) const&; + virtual NonnullRefPtr with_slices(NonnullRefPtrVector slices) const&; virtual ~Value(); virtual bool is_command() const { return false; } virtual bool is_glob() const { return false; } @@ -251,12 +254,21 @@ public: virtual bool is_list() const { return false; } virtual bool is_string() const { return false; } virtual bool is_list_without_resolution() const { return false; } + +protected: + Value& set_slices(NonnullRefPtrVector slices) + { + m_slices = move(slices); + return *this; + } + NonnullRefPtrVector m_slices; }; class CommandValue final : public Value { public: virtual Vector resolve_as_list(RefPtr) override; virtual Vector resolve_as_commands(RefPtr) override; + virtual NonnullRefPtr clone() const override { return create(m_command)->set_slices(m_slices); } virtual ~CommandValue(); virtual bool is_command() const override { return true; } CommandValue(Command command) @@ -277,6 +289,7 @@ class CommandSequenceValue final : public Value { public: virtual Vector resolve_as_list(RefPtr) override; virtual Vector resolve_as_commands(RefPtr) override; + virtual NonnullRefPtr clone() const override { return create(m_contained_values)->set_slices(m_slices); } virtual ~CommandSequenceValue(); virtual bool is_command() const override { return true; } CommandSequenceValue(Vector commands) @@ -292,6 +305,7 @@ class JobValue final : public Value { public: virtual Vector resolve_as_list(RefPtr) override { VERIFY_NOT_REACHED(); } virtual Vector resolve_as_commands(RefPtr) override { VERIFY_NOT_REACHED(); } + virtual NonnullRefPtr clone() const override { return create(m_job)->set_slices(m_slices); } virtual ~JobValue(); virtual bool is_job() const override { return true; } JobValue(RefPtr job) @@ -309,6 +323,7 @@ class ListValue final : public Value { public: virtual Vector resolve_as_list(RefPtr) override; virtual NonnullRefPtr resolve_without_cast(RefPtr) override; + virtual NonnullRefPtr clone() const override { return create(m_contained_values)->set_slices(m_slices); } virtual ~ListValue(); virtual bool is_list() const override { return true; } virtual bool is_list_without_resolution() const override { return true; } @@ -332,6 +347,7 @@ private: class StringValue final : public Value { public: virtual Vector resolve_as_list(RefPtr) override; + virtual NonnullRefPtr clone() const override { return create(m_string, m_split, m_keep_empty)->set_slices(m_slices); } virtual ~StringValue(); virtual bool is_string() const override { return m_split.is_null(); } virtual bool is_list() const override { return !m_split.is_null(); } @@ -352,6 +368,7 @@ private: class GlobValue final : public Value { public: virtual Vector resolve_as_list(RefPtr) override; + virtual NonnullRefPtr clone() const override { return create(m_glob, m_generation_position)->set_slices(m_slices); } virtual ~GlobValue(); virtual bool is_glob() const override { return true; } GlobValue(String glob, Position position) @@ -369,6 +386,7 @@ class SimpleVariableValue final : public Value { public: virtual Vector resolve_as_list(RefPtr) override; virtual NonnullRefPtr resolve_without_cast(RefPtr) override; + virtual NonnullRefPtr clone() const override { return create(m_name)->set_slices(m_slices); } virtual ~SimpleVariableValue(); SimpleVariableValue(String name) : m_name(move(name)) @@ -382,6 +400,7 @@ private: class SpecialVariableValue final : public Value { public: virtual Vector resolve_as_list(RefPtr) override; + virtual NonnullRefPtr clone() const override { return create(m_name)->set_slices(m_slices); } virtual ~SpecialVariableValue(); SpecialVariableValue(char name) : m_name(name) @@ -395,6 +414,7 @@ private: class TildeValue final : public Value { public: virtual Vector resolve_as_list(RefPtr) override; + virtual NonnullRefPtr clone() const override { return create(m_username)->set_slices(m_slices); } virtual ~TildeValue(); virtual bool is_string() const override { return true; } TildeValue(String name) @@ -489,6 +509,7 @@ public: ReadRedirection, ReadWriteRedirection, Sequence, + Slice, SimpleVariable, SpecialVariable, StringLiteral, @@ -1214,7 +1235,48 @@ private: RefPtr m_block; }; -class SimpleVariable final : public Node { +class Slice final : public Node { +public: + Slice(Position, NonnullRefPtr); + virtual ~Slice() override; + + virtual void visit(NodeVisitor& visitor) override { visitor.visit(this); } + + NonnullRefPtr selector() const { return m_selector; } + + virtual void dump(int level) const override; + virtual RefPtr run(RefPtr) override; + virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override; + virtual Vector complete_for_editor(Shell&, size_t, const HitTestResult&) override; + virtual HitTestResult hit_test_position(size_t) const override; + +protected: + NODE(Slice); + NonnullRefPtr m_selector; +}; + +class VariableNode : public Node { +public: + VariableNode(Position position) + : Node(move(position)) + { + } + + void set_slice(NonnullRefPtr&& slice) + { + VERIFY(!m_slice); + m_slice = move(slice); + if (m_slice->is_syntax_error()) + set_is_syntax_error(m_slice->syntax_error_node()); + } + + const Slice* slice() const { return m_slice.ptr(); } + +protected: + RefPtr m_slice; +}; + +class SimpleVariable final : public VariableNode { public: SimpleVariable(Position, String); virtual ~SimpleVariable(); @@ -1234,7 +1296,7 @@ private: String m_name; }; -class SpecialVariable final : public Node { +class SpecialVariable final : public VariableNode { public: SpecialVariable(Position, char); virtual ~SpecialVariable(); diff --git a/Userland/Shell/Formatter.cpp b/Userland/Shell/Formatter.cpp index 2d7e0908d5..22d85d2e8a 100644 --- a/Userland/Shell/Formatter.cpp +++ b/Userland/Shell/Formatter.cpp @@ -195,18 +195,22 @@ void Formatter::visit(const AST::BraceExpansion* node) { will_visit(node); test_and_update_output_cursor(node); - current_builder().append('{'); + if (!m_parent_node || m_parent_node->kind() != AST::Node::Kind::Slice) + 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); + { + 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('}'); + if (!m_parent_node || m_parent_node->kind() != AST::Node::Kind::Slice) + current_builder().append('}'); visited(node); } @@ -615,14 +619,16 @@ void Formatter::visit(const AST::Range* node) { will_visit(node); test_and_update_output_cursor(node); - current_builder().append('{'); + if (!m_parent_node || m_parent_node->kind() != AST::Node::Kind::Slice) + current_builder().append('{'); TemporaryChange parent { m_parent_node, node }; node->start()->visit(*this); current_builder().append(".."); node->end()->visit(*this); - current_builder().append('}'); + if (!m_parent_node || m_parent_node->kind() != AST::Node::Kind::Slice) + current_builder().append('}'); visited(node); } @@ -686,12 +692,27 @@ void Formatter::visit(const AST::Subshell* node) visited(node); } +void Formatter::visit(const AST::Slice* node) +{ + will_visit(node); + test_and_update_output_cursor(node); + TemporaryChange parent { m_parent_node, node }; + + current_builder().append('['); + node->selector()->visit(*this); + current_builder().append(']'); + + visited(node); +} + void Formatter::visit(const AST::SimpleVariable* node) { will_visit(node); test_and_update_output_cursor(node); current_builder().append('$'); current_builder().append(node->name()); + if (const AST::Node* slice = node->slice()) + slice->visit(*this); visited(node); } @@ -701,6 +722,8 @@ void Formatter::visit(const AST::SpecialVariable* node) test_and_update_output_cursor(node); current_builder().append('$'); current_builder().append(node->name()); + if (const AST::Node* slice = node->slice()) + slice->visit(*this); visited(node); } diff --git a/Userland/Shell/Formatter.h b/Userland/Shell/Formatter.h index d7bff364e0..76aab3d912 100644 --- a/Userland/Shell/Formatter.h +++ b/Userland/Shell/Formatter.h @@ -90,6 +90,7 @@ private: virtual void visit(const AST::ReadWriteRedirection*) override; virtual void visit(const AST::Sequence*) override; virtual void visit(const AST::Subshell*) override; + virtual void visit(const AST::Slice*) override; virtual void visit(const AST::SimpleVariable*) override; virtual void visit(const AST::SpecialVariable*) override; virtual void visit(const AST::Juxtaposition*) override; diff --git a/Userland/Shell/Forward.h b/Userland/Shell/Forward.h index c504588405..8a977e13bd 100644 --- a/Userland/Shell/Forward.h +++ b/Userland/Shell/Forward.h @@ -67,6 +67,7 @@ class ReadRedirection; class ReadWriteRedirection; class Sequence; class Subshell; +class Slice; class SimpleVariable; class SpecialVariable; class Juxtaposition; diff --git a/Userland/Shell/NodeVisitor.cpp b/Userland/Shell/NodeVisitor.cpp index c84094c860..9fa64c19d5 100644 --- a/Userland/Shell/NodeVisitor.cpp +++ b/Userland/Shell/NodeVisitor.cpp @@ -202,12 +202,21 @@ void NodeVisitor::visit(const AST::Subshell* node) node->block()->visit(*this); } -void NodeVisitor::visit(const AST::SimpleVariable*) +void NodeVisitor::visit(const AST::Slice* node) { + node->selector()->visit(*this); } -void NodeVisitor::visit(const AST::SpecialVariable*) +void NodeVisitor::visit(const AST::SimpleVariable* node) { + if (const AST::Node* slice = node->slice()) + slice->visit(*this); +} + +void NodeVisitor::visit(const AST::SpecialVariable* node) +{ + if (const AST::Node* slice = node->slice()) + slice->visit(*this); } void NodeVisitor::visit(const AST::Juxtaposition* node) diff --git a/Userland/Shell/NodeVisitor.h b/Userland/Shell/NodeVisitor.h index 6d45bc3ab1..8452d6d968 100644 --- a/Userland/Shell/NodeVisitor.h +++ b/Userland/Shell/NodeVisitor.h @@ -63,6 +63,7 @@ public: virtual void visit(const AST::ReadWriteRedirection*); virtual void visit(const AST::Sequence*); virtual void visit(const AST::Subshell*); + virtual void visit(const AST::Slice*); virtual void visit(const AST::SimpleVariable*); virtual void visit(const AST::SpecialVariable*); virtual void visit(const AST::Juxtaposition*); diff --git a/Userland/Shell/Parser.cpp b/Userland/Shell/Parser.cpp index 6208b0a8fb..690f353f9a 100644 --- a/Userland/Shell/Parser.cpp +++ b/Userland/Shell/Parser.cpp @@ -1329,6 +1329,21 @@ RefPtr Parser::parse_doublequoted_string_inner() } RefPtr Parser::parse_variable() +{ + auto rule_start = push_start(); + auto ref = parse_variable_ref(); + + if (!ref) + return nullptr; + + auto variable = static_ptr_cast(ref); + if (auto slice = parse_slice()) + variable->set_slice(slice.release_nonnull()); + + return variable; +} + +RefPtr Parser::parse_variable_ref() { auto rule_start = push_start(); if (at_end()) @@ -1358,6 +1373,38 @@ RefPtr Parser::parse_variable() return create(move(name)); // Variable Simple } +RefPtr Parser::parse_slice() +{ + auto rule_start = push_start(); + if (!next_is("[")) + return nullptr; + + consume(); // [ + + ScopedValueRollback chars_change { m_extra_chars_not_allowed_in_barewords }; + m_extra_chars_not_allowed_in_barewords.append(']'); + auto spec = parse_brace_expansion_spec(); + + RefPtr error; + + if (peek() != ']') + error = create("Expected a close bracket ']' to end a variable slice"); + else + consume(); + + if (!spec) { + if (error) + spec = move(error); + else + spec = create("Expected either a range, or a comma-seprated list of selectors"); + } + + auto node = create(spec.release_nonnull()); + if (error) + node->set_is_syntax_error(*error); + return node; +} + RefPtr Parser::parse_evaluate() { auto rule_start = push_start(); @@ -1787,7 +1834,9 @@ RefPtr Parser::parse_brace_expansion() RefPtr Parser::parse_brace_expansion_spec() { TemporaryChange is_in_brace_expansion { m_is_in_brace_expansion_spec, true }; - TemporaryChange chars_change { m_extra_chars_not_allowed_in_barewords, { ',' } }; + ScopedValueRollback chars_change { m_extra_chars_not_allowed_in_barewords }; + + m_extra_chars_not_allowed_in_barewords.append(','); auto rule_start = push_start(); auto start_expr = parse_expression(); diff --git a/Userland/Shell/Parser.h b/Userland/Shell/Parser.h index 4d70ec83ae..fa9511a8d2 100644 --- a/Userland/Shell/Parser.h +++ b/Userland/Shell/Parser.h @@ -91,6 +91,8 @@ private: RefPtr parse_string(); RefPtr parse_doublequoted_string_inner(); RefPtr parse_variable(); + RefPtr parse_variable_ref(); + RefPtr parse_slice(); RefPtr parse_evaluate(); RefPtr parse_history_designator(); RefPtr parse_comment(); @@ -261,13 +263,17 @@ dquoted_string_inner :: '\' . dquoted_string_inner? {concat} | '\' 'x' digit digit dquoted_string_inner? | '\' [abefrn] dquoted_string_inner? -variable :: '$' identifier +variable :: variable_ref slice? + +variable_ref :: '$' identifier | '$' '$' | '$' '?' | '$' '*' | '$' '#' | ... +slice :: '[' brace_expansion_spec ']' + comment :: '#' [^\n]* immediate_expression :: '$' '{' immediate_function expression* '}' diff --git a/Userland/Shell/Shell.cpp b/Userland/Shell/Shell.cpp index bd78a9d411..cac2d10c1e 100644 --- a/Userland/Shell/Shell.cpp +++ b/Userland/Shell/Shell.cpp @@ -1885,6 +1885,7 @@ void Shell::possibly_print_error() const case ShellError::EvaluatedSyntaxError: warnln("Shell Syntax Error: {}", m_error_description); break; + case ShellError::InvalidSliceContentsError: case ShellError::InvalidGlobError: case ShellError::NonExhaustiveMatchRules: warnln("Shell: {}", m_error_description); diff --git a/Userland/Shell/Shell.h b/Userland/Shell/Shell.h index 556ee613fd..832bc8d723 100644 --- a/Userland/Shell/Shell.h +++ b/Userland/Shell/Shell.h @@ -235,6 +235,7 @@ public: EvaluatedSyntaxError, NonExhaustiveMatchRules, InvalidGlobError, + InvalidSliceContentsError, OpenFailure, };