Shell: Add support for enumerating lists in for loops

With some odd syntax to boot:
```sh
$ for index i x in $whatever {}
```
This commit is contained in:
AnotherTest 2021-03-05 18:25:09 +03:30 committed by Andreas Kling
parent a45b2ea6fb
commit 13b65b632a
Notes: sideshowbarker 2024-07-18 21:39:42 +09:00
8 changed files with 133 additions and 23 deletions

View file

@ -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 '}'

View file

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

View file

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

View file

@ -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 ");
}

View file

@ -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()

View file

@ -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 '}'

View file

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

View file

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