diff --git a/Base/usr/share/man/man5/Shell.md b/Base/usr/share/man/man5/Shell.md index bdc8618e90a..676fc55de99 100644 --- a/Base/usr/share/man/man5/Shell.md +++ b/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 '}' diff --git a/Userland/Shell/AST.cpp b/Userland/Shell/AST.cpp index b5bcca79be5..e53c2151c1a 100644 --- a/Userland/Shell/AST.cpp +++ b/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 ForLoop::run(RefPtr shell) }; if (m_iterated_expression) { + auto variable_name = m_variable.has_value() ? m_variable->name : "it"; + Optional index_name = m_index_variable.has_value() ? Optional(m_index_variable->name) : Optional(); + 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 ForLoop::run(RefPtr 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(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 iterated_expr, RefPtr block, Optional in_kw_position) +ForLoop::ForLoop(Position position, Optional variable, Optional index_variable, RefPtr iterated_expr, RefPtr block, Optional in_kw_position, Optional 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()); diff --git a/Userland/Shell/AST.h b/Userland/Shell/AST.h index c0169dd7bcb..f013a8e07b4 100644 --- a/Userland/Shell/AST.h +++ b/Userland/Shell/AST.h @@ -846,13 +846,15 @@ private: class ForLoop final : public Node { public: - ForLoop(Position, String variable_name, RefPtr iterated_expr, RefPtr block, Optional in_kw_position = {}); + ForLoop(Position, Optional variable, Optional index_variable, RefPtr iterated_expr, RefPtr block, Optional in_kw_position = {}, Optional 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& variable() const { return m_variable; } + const Optional& index_variable() const { return m_index_variable; } const RefPtr& iterated_expression() const { return m_iterated_expression; } const RefPtr& block() const { return m_block; } + const Optional index_keyword_position() const { return m_index_kw_position; } const Optional 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 m_variable; + Optional m_index_variable; RefPtr m_iterated_expression; RefPtr m_block; Optional m_in_kw_position; + Optional m_index_kw_position; }; class Glob final : public Node { diff --git a/Userland/Shell/Formatter.cpp b/Userland/Shell/Formatter.cpp index bfe065aa407..43f69340117 100644 --- a/Userland/Shell/Formatter.cpp +++ b/Userland/Shell/Formatter.cpp @@ -341,8 +341,13 @@ void Formatter::visit(const AST::ForLoop* node) TemporaryChange 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 "); } diff --git a/Userland/Shell/Parser.cpp b/Userland/Shell/Parser.cpp index 5fcc49d04bd..ab00dee599f 100644 --- a/Userland/Shell/Parser.cpp +++ b/Userland/Shell/Parser.cpp @@ -592,16 +592,46 @@ RefPtr Parser::parse_for_loop() return nullptr; } - auto variable_name = consume_while(is_word_character); - Optional in_start_position; - if (variable_name.is_empty()) { - variable_name = "it"; - } else { + Optional index_variable_name, variable_name; + Optional 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("Expected 'in' after a variable name in a 'for' loop", true); - return create(move(variable_name), move(syntax_error), nullptr); // ForLoop Var Iterated Block + return create(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 Parser::parse_for_loop() auto obrace_error_start = push_start(); if (!expect('{')) { auto syntax_error = create("Expected an open brace '{' to start a 'for' loop body", true); - return create(move(variable_name), move(iterated_expression), move(syntax_error), move(in_start_position)); // ForLoop Var Iterated Block + return create(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 Parser::parse_for_loop() } } - return create(move(variable_name), move(iterated_expression), move(body), move(in_start_position)); // ForLoop Var Iterated Block + return create(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 Parser::parse_loop_loop() @@ -657,7 +687,7 @@ RefPtr Parser::parse_loop_loop() auto obrace_error_start = push_start(); if (!expect('{')) { auto syntax_error = create("Expected an open brace '{' to start a 'loop' loop body", true); - return create(String::empty(), nullptr, move(syntax_error), Optional {}); // ForLoop null null Block + return create(AST::NameWithPosition {}, AST::NameWithPosition {}, nullptr, move(syntax_error)); // ForLoop null null Block } } @@ -676,7 +706,7 @@ RefPtr Parser::parse_loop_loop() } } - return create(String::empty(), nullptr, move(body), Optional {}); // ForLoop null null Block + return create(AST::NameWithPosition {}, AST::NameWithPosition {}, nullptr, move(body)); // ForLoop null null Block } RefPtr Parser::parse_if_expr() diff --git a/Userland/Shell/Parser.h b/Userland/Shell/Parser.h index dfc253cfedc..4d70ec83ae0 100644 --- a/Userland/Shell/Parser.h +++ b/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 '}' diff --git a/Userland/Shell/SyntaxHighlighter.cpp b/Userland/Shell/SyntaxHighlighter.cpp index 893d8e00623..c0f89613a4c 100644 --- a/Userland/Shell/SyntaxHighlighter.cpp +++ b/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 { diff --git a/Userland/Shell/Tests/loop.sh b/Userland/Shell/Tests/loop.sh index 6d81ebf7ef0..e040280cebb 100644 --- a/Userland/Shell/Tests/loop.sh +++ b/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"