Browse Source

Shell: Add support for enumerating lists in for loops

With some odd syntax to boot:
```sh
$ for index i x in $whatever {}
```
AnotherTest 4 years ago
parent
commit
13b65b632a

+ 9 - 3
Base/usr/share/man/man5/Shell.md

@@ -211,9 +211,12 @@ if A {
 ##### For Loops
 For Loops evaluate a sequence of commands once per element in a given list.
 The shell has two forms of _for loops_, one with an explicitly named iteration variable, and one with an implicitly named one.
-The general syntax follows the form `for name in expr { sequence }`, and allows omitting the `name in` part to implicitly name the variable `it`.
+The general syntax follows the form `for index index_name name in expr { sequence }`, and allows omitting the `index index_name name in` part to implicitly name the variable `it`.
 
-A for-loop evaluates the _sequence_ once per every element in the _expr_, seetting the local variable _name_ to the element being processed.
+It should be noted that the `index index_name` section is optional, but if supplied, will require an explicit iteration variable as well.
+In other words, `for index i in foo` is not valid syntax.
+
+A for-loop evaluates the _sequence_ once per every element in the _expr_, seetting the local variable _name_ to the element being processed, and the local variable _enum name_ to the enumeration index (if set).
 
 The Shell shall cancel the for loop if two consecutive commands are interrupted via SIGINT (\^C), and any other terminating signal aborts the loop entirely.
 
@@ -224,6 +227,9 @@ $ for * { mv $it 1-$it }
 
 # Iterate over a sequence and write each element to a file
 $ for i in $(seq 1 100) { echo $i >> foo }
+
+# Iterate over some files and get their index
+$ for index i x in * { echo file at index $i is named $x }
 ```
 
 ##### Infinite Loops
@@ -365,7 +371,7 @@ control_structure[c] :: for_expr
 continuation_control :: 'break'
                       | 'continue'
 
-for_expr :: 'for' ws+ (identifier ' '+ 'in' ws*)? expression ws+ '{' [c] toplevel '}'
+for_expr :: 'for' ws+ (('enum' ' '+ identifier)? identifier ' '+ 'in' ws*)? expression ws+ '{' [c] toplevel '}'
 
 loop_expr :: 'loop' ws* '{' [c] toplevel '}'
 

+ 27 - 4
Userland/Shell/AST.cpp

@@ -1059,7 +1059,10 @@ FunctionDeclaration::~FunctionDeclaration()
 void ForLoop::dump(int level) const
 {
     Node::dump(level);
-    print_indented(String::format("%s in", m_variable_name.characters()), level + 1);
+    if (m_variable.has_value())
+        print_indented(String::formatted("iterating with {} in", m_variable->name), level + 1);
+    if (m_index_variable.has_value())
+        print_indented(String::formatted("with index name {} in", m_index_variable->name), level + 1);
     if (m_iterated_expression)
         m_iterated_expression->dump(level + 2);
     else
@@ -1110,6 +1113,9 @@ RefPtr<Value> ForLoop::run(RefPtr<Shell> shell)
     };
 
     if (m_iterated_expression) {
+        auto variable_name = m_variable.has_value() ? m_variable->name : "it";
+        Optional<StringView> index_name = m_index_variable.has_value() ? Optional<StringView>(m_index_variable->name) : Optional<StringView>();
+        size_t i = 0;
         m_iterated_expression->for_each_entry(shell, [&](auto value) {
             if (consecutive_interruptions == 2)
                 return IterationDecision::Break;
@@ -1118,10 +1124,16 @@ RefPtr<Value> ForLoop::run(RefPtr<Shell> shell)
 
             {
                 auto frame = shell->push_frame(String::formatted("for ({})", this));
-                shell->set_local_variable(m_variable_name, value, true);
+                shell->set_local_variable(variable_name, value, true);
+
+                if (index_name.has_value())
+                    shell->set_local_variable(index_name.value(), create<AST::StringValue>(String::number(i)), true);
+
+                ++i;
 
                 block_value = m_block->run(shell);
             }
+
             return run(block_value);
         });
     } else {
@@ -1146,10 +1158,19 @@ void ForLoop::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightM
         if (m_in_kw_position.has_value())
             editor.stylize({ m_in_kw_position.value().start_offset, m_in_kw_position.value().end_offset }, { Line::Style::Foreground(Line::Style::XtermColor::Yellow) });
 
+        if (m_index_kw_position.has_value())
+            editor.stylize({ m_index_kw_position.value().start_offset, m_index_kw_position.value().end_offset }, { Line::Style::Foreground(Line::Style::XtermColor::Yellow) });
+
         metadata.is_first_in_list = false;
         m_iterated_expression->highlight_in_editor(editor, shell, metadata);
     }
 
+    if (m_index_variable.has_value())
+        editor.stylize({ m_index_variable->position.start_offset, m_index_variable->position.end_offset }, { Line::Style::Foreground(Line::Style::XtermColor::Blue), Line::Style::Italic });
+
+    if (m_variable.has_value())
+        editor.stylize({ m_variable->position.start_offset, m_variable->position.end_offset }, { Line::Style::Foreground(Line::Style::XtermColor::Blue), Line::Style::Italic });
+
     metadata.is_first_in_list = true;
     if (m_block)
         m_block->highlight_in_editor(editor, shell, metadata);
@@ -1171,12 +1192,14 @@ HitTestResult ForLoop::hit_test_position(size_t offset) const
     return m_block->hit_test_position(offset);
 }
 
-ForLoop::ForLoop(Position position, String variable_name, RefPtr<AST::Node> iterated_expr, RefPtr<AST::Node> block, Optional<Position> in_kw_position)
+ForLoop::ForLoop(Position position, Optional<NameWithPosition> variable, Optional<NameWithPosition> index_variable, RefPtr<AST::Node> iterated_expr, RefPtr<AST::Node> block, Optional<Position> in_kw_position, Optional<Position> index_kw_position)
     : Node(move(position))
-    , m_variable_name(move(variable_name))
+    , m_variable(move(variable))
+    , m_index_variable(move(index_variable))
     , m_iterated_expression(move(iterated_expr))
     , m_block(move(block))
     , m_in_kw_position(move(in_kw_position))
+    , m_index_kw_position(move(index_kw_position))
 {
     if (m_iterated_expression && m_iterated_expression->is_syntax_error())
         set_is_syntax_error(m_iterated_expression->syntax_error_node());

+ 7 - 3
Userland/Shell/AST.h

@@ -846,13 +846,15 @@ private:
 
 class ForLoop final : public Node {
 public:
-    ForLoop(Position, String variable_name, RefPtr<AST::Node> iterated_expr, RefPtr<AST::Node> block, Optional<Position> in_kw_position = {});
+    ForLoop(Position, Optional<NameWithPosition> variable, Optional<NameWithPosition> index_variable, RefPtr<AST::Node> iterated_expr, RefPtr<AST::Node> block, Optional<Position> in_kw_position = {}, Optional<Position> index_kw_position = {});
     virtual ~ForLoop();
     virtual void visit(NodeVisitor& visitor) override { visitor.visit(this); }
 
-    const String& variable_name() const { return m_variable_name; }
+    const Optional<NameWithPosition>& variable() const { return m_variable; }
+    const Optional<NameWithPosition>& index_variable() const { return m_index_variable; }
     const RefPtr<Node>& iterated_expression() const { return m_iterated_expression; }
     const RefPtr<Node>& block() const { return m_block; }
+    const Optional<Position> index_keyword_position() const { return m_index_kw_position; }
     const Optional<Position> in_keyword_position() const { return m_in_kw_position; }
 
 private:
@@ -864,10 +866,12 @@ private:
     virtual bool would_execute() const override { return true; }
     virtual bool should_override_execution_in_current_process() const override { return true; }
 
-    String m_variable_name;
+    Optional<NameWithPosition> m_variable;
+    Optional<NameWithPosition> m_index_variable;
     RefPtr<AST::Node> m_iterated_expression;
     RefPtr<AST::Node> m_block;
     Optional<Position> m_in_kw_position;
+    Optional<Position> m_index_kw_position;
 };
 
 class Glob final : public Node {

+ 7 - 2
Userland/Shell/Formatter.cpp

@@ -341,8 +341,13 @@ void Formatter::visit(const AST::ForLoop* node)
     TemporaryChange<const AST::Node*> parent { m_parent_node, node };
 
     if (!is_loop) {
-        if (node->variable_name() != "it") {
-            current_builder().append(node->variable_name());
+        if (node->index_variable().has_value()) {
+            current_builder().append("index ");
+            current_builder().append(node->index_variable()->name);
+            current_builder().append(" ");
+        }
+        if (node->variable().has_value() && node->variable()->name != "it") {
+            current_builder().append(node->variable()->name);
             current_builder().append(" in ");
         }
 

+ 40 - 10
Userland/Shell/Parser.cpp

@@ -592,16 +592,46 @@ RefPtr<AST::Node> Parser::parse_for_loop()
         return nullptr;
     }
 
-    auto variable_name = consume_while(is_word_character);
-    Optional<AST::Position> in_start_position;
-    if (variable_name.is_empty()) {
-        variable_name = "it";
-    } else {
+    Optional<AST::NameWithPosition> index_variable_name, variable_name;
+    Optional<AST::Position> in_start_position, index_start_position;
+
+    auto offset_before_index = current_position();
+    if (expect("index")) {
+        auto offset = current_position();
+        if (!consume_while(is_whitespace).is_empty()) {
+            auto offset_before_variable = current_position();
+            auto variable = consume_while(is_word_character);
+            if (!variable.is_empty()) {
+                index_start_position = AST::Position { offset_before_index.offset, offset.offset, offset_before_index.line, offset.line };
+
+                auto offset_after_variable = current_position();
+                index_variable_name = AST::NameWithPosition {
+                    variable,
+                    { offset_before_variable.offset, offset_after_variable.offset, offset_before_variable.line, offset_after_variable.line },
+                };
+
+                consume_while(is_whitespace);
+            } else {
+                restore_to(offset_before_index.offset, offset_before_index.line);
+            }
+        } else {
+            restore_to(offset_before_index.offset, offset_before_index.line);
+        }
+    }
+
+    auto variable_name_start_offset = current_position();
+    auto name = consume_while(is_word_character);
+    auto variable_name_end_offset = current_position();
+    if (!name.is_empty()) {
+        variable_name = AST::NameWithPosition {
+            name,
+            { variable_name_start_offset.offset, variable_name_end_offset.offset, variable_name_start_offset.line, variable_name_end_offset.line }
+        };
         consume_while(is_whitespace);
         auto in_error_start = push_start();
         if (!expect("in")) {
             auto syntax_error = create<AST::SyntaxError>("Expected 'in' after a variable name in a 'for' loop", true);
-            return create<AST::ForLoop>(move(variable_name), move(syntax_error), nullptr); // ForLoop Var Iterated Block
+            return create<AST::ForLoop>(move(variable_name), move(index_variable_name), move(syntax_error), nullptr); // ForLoop Var Iterated Block
         }
         in_start_position = AST::Position { in_error_start->offset, m_offset, in_error_start->line, line() };
     }
@@ -620,7 +650,7 @@ RefPtr<AST::Node> Parser::parse_for_loop()
         auto obrace_error_start = push_start();
         if (!expect('{')) {
             auto syntax_error = create<AST::SyntaxError>("Expected an open brace '{' to start a 'for' loop body", true);
-            return create<AST::ForLoop>(move(variable_name), move(iterated_expression), move(syntax_error), move(in_start_position)); // ForLoop Var Iterated Block
+            return create<AST::ForLoop>(move(variable_name), move(index_variable_name), move(iterated_expression), move(syntax_error), move(in_start_position), move(index_start_position)); // ForLoop Var Iterated Block
         }
     }
 
@@ -639,7 +669,7 @@ RefPtr<AST::Node> Parser::parse_for_loop()
         }
     }
 
-    return create<AST::ForLoop>(move(variable_name), move(iterated_expression), move(body), move(in_start_position)); // ForLoop Var Iterated Block
+    return create<AST::ForLoop>(move(variable_name), move(index_variable_name), move(iterated_expression), move(body), move(in_start_position), move(index_start_position)); // ForLoop Var Iterated Block
 }
 
 RefPtr<AST::Node> Parser::parse_loop_loop()
@@ -657,7 +687,7 @@ RefPtr<AST::Node> Parser::parse_loop_loop()
         auto obrace_error_start = push_start();
         if (!expect('{')) {
             auto syntax_error = create<AST::SyntaxError>("Expected an open brace '{' to start a 'loop' loop body", true);
-            return create<AST::ForLoop>(String::empty(), nullptr, move(syntax_error), Optional<AST::Position> {}); // ForLoop null null Block
+            return create<AST::ForLoop>(AST::NameWithPosition {}, AST::NameWithPosition {}, nullptr, move(syntax_error)); // ForLoop null null Block
         }
     }
 
@@ -676,7 +706,7 @@ RefPtr<AST::Node> Parser::parse_loop_loop()
         }
     }
 
-    return create<AST::ForLoop>(String::empty(), nullptr, move(body), Optional<AST::Position> {}); // ForLoop null null Block
+    return create<AST::ForLoop>(AST::NameWithPosition {}, AST::NameWithPosition {}, nullptr, move(body)); // ForLoop null null Block
 }
 
 RefPtr<AST::Node> Parser::parse_if_expr()

+ 1 - 1
Userland/Shell/Parser.h

@@ -207,7 +207,7 @@ control_structure[c] :: for_expr
 continuation_control :: 'break'
                       | 'continue'
 
-for_expr :: 'for' ws+ (identifier ' '+ 'in' ws*)? expression ws+ '{' [c] toplevel '}'
+for_expr :: 'for' ws+ (('index' ' '+ identifier ' '+)? identifier ' '+ 'in' ws*)? expression ws+ '{' [c] toplevel '}'
 
 loop_expr :: 'loop' ws* '{' [c] toplevel '}'
 

+ 29 - 0
Userland/Shell/SyntaxHighlighter.cpp

@@ -261,6 +261,35 @@ private:
             set_offset_range_end(in_span.range, position.end_line);
             in_span.attributes.color = m_palette.syntax_keyword();
         }
+
+        // "index"
+        if (auto maybe_position = node->index_keyword_position(); maybe_position.has_value()) {
+            auto& position = maybe_position.value();
+
+            auto& index_span = span_for_node(node);
+            set_offset_range_start(index_span.range, position.start_line);
+            set_offset_range_end(index_span.range, position.end_line);
+            index_span.attributes.color = m_palette.syntax_keyword();
+        }
+
+        // variables
+        if (auto maybe_variable = node->variable(); maybe_variable.has_value()) {
+            auto& position = maybe_variable->position;
+
+            auto& variable_span = span_for_node(node);
+            set_offset_range_start(variable_span.range, position.start_line);
+            set_offset_range_end(variable_span.range, position.end_line);
+            variable_span.attributes.color = m_palette.syntax_identifier();
+        }
+
+        if (auto maybe_variable = node->index_variable(); maybe_variable.has_value()) {
+            auto& position = maybe_variable->position;
+
+            auto& variable_span = span_for_node(node);
+            set_offset_range_start(variable_span.range, position.start_line);
+            set_offset_range_end(variable_span.range, position.end_line);
+            variable_span.attributes.color = m_palette.syntax_identifier();
+        }
     }
     virtual void visit(const AST::Glob* node) override
     {

+ 13 - 0
Userland/Shell/Tests/loop.sh

@@ -23,6 +23,19 @@ for cmd in ((test 1 = 1) (test 2 = 2)) {
     $cmd || unset singlecommand_ok
 }
 
+# with index
+for index i val in (0 1 2) {
+    if not test "$i" -eq "$val" {
+        unset singlecommand_ok
+    }
+}
+
+for index i val in (1 2 3) {
+    if not test "$i" -ne "$val" {
+        unset singlecommand_ok
+    }
+}
+
  # Multiple commands in block
 for cmd in ((test 1 = 1) (test 2 = 2)) {
     test -z "$cmd"