diff --git a/Base/usr/share/man/man5/Shell.md b/Base/usr/share/man/man5/Shell.md index 3e940589141..77f84300daf 100644 --- a/Base/usr/share/man/man5/Shell.md +++ b/Base/usr/share/man/man5/Shell.md @@ -221,6 +221,28 @@ $ for * { mv $it 1-$it } $ for i in $(seq 1 100) { echo $i >> foo } ``` +##### Infinite Loops +Infinite loops (as denoted by the keyword `loop`) can be used to repeat a block until the block runs `break`, or the loop terminates by external sources (interrupts, program exit, and terminating signals). + +The behaviour regarding SIGINT and other signals is the same as for loops (mentioned above). + +###### Examples +```sh +# Keep deleting a file +loop { + rm -f foo +} +``` + +###### Examples +```sh +# Iterate over every non-hidden file in the current directory, and prepend '1-' to its name. +$ 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 } +``` + ##### Subshells Subshells evaluate a given block in a new instance (fork) of the current shell process. to create a subshell, any valid shell code can be enclosed in braces. @@ -299,7 +321,7 @@ sequence :: variable_decls? or_logical_sequence terminator sequence | variable_decls? function_decl (terminator sequence)? | variable_decls? terminator sequence -function_decl :: identifier '(' (ws* identifier)* ')' ws* '{' toplevel '}' +function_decl :: identifier '(' (ws* identifier)* ')' ws* '{' [!c] toplevel '}' or_logical_sequence :: and_logical_sequence '|' '|' and_logical_sequence | and_logical_sequence @@ -318,12 +340,19 @@ pipe_sequence :: command '|' pipe_sequence | control_structure '|' pipe_sequence | control_structure -control_structure :: for_expr - | if_expr - | subshell - | match_expr +control_structure[c] :: for_expr + | loop_expr + | if_expr + | subshell + | match_expr + | ?c: continuation_control -for_expr :: 'for' ws+ (identifier ' '+ 'in' ws*)? expression ws+ '{' toplevel '}' +continuation_control :: 'break' + | 'continue' + +for_expr :: 'for' ws+ (identifier ' '+ 'in' ws*)? expression ws+ '{' [c] toplevel '}' + +loop_expr :: 'loop' ws* '{' [c] toplevel '}' if_expr :: 'if' ws+ or_logical_sequence ws+ '{' toplevel '}' else_clause? diff --git a/Libraries/LibGUI/ShellSyntaxHighlighter.cpp b/Libraries/LibGUI/ShellSyntaxHighlighter.cpp index 3fe1fd4738e..42472468ca3 100644 --- a/Libraries/LibGUI/ShellSyntaxHighlighter.cpp +++ b/Libraries/LibGUI/ShellSyntaxHighlighter.cpp @@ -190,6 +190,13 @@ private: auto& span = span_for_node(node); span.color = m_palette.syntax_comment(); } + virtual void visit(const AST::ContinuationControl* node) override + { + NodeVisitor::visit(node); + + auto& span = span_for_node(node); + span.color = m_palette.syntax_control_keyword(); + } virtual void visit(const AST::DynamicEvaluate* node) override { NodeVisitor::visit(node); diff --git a/Shell/AST.cpp b/Shell/AST.cpp index a0350fa755a..2bd877624d5 100644 --- a/Shell/AST.cpp +++ b/Shell/AST.cpp @@ -792,6 +792,28 @@ Comment::~Comment() { } +void ContinuationControl::dump(int level) const +{ + Node::dump(level); + print_indented(m_kind == Continue ? "(Continue)" : "(Break)", level + 1); +} + +RefPtr ContinuationControl::run(RefPtr shell) +{ + if (m_kind == Break) + shell->raise_error(Shell::ShellError::InternalControlFlowBreak, {}); + else if (m_kind == Continue) + shell->raise_error(Shell::ShellError::InternalControlFlowContinue, {}); + else + ASSERT_NOT_REACHED(); + return create({}); +} + +void ContinuationControl::highlight_in_editor(Line::Editor& editor, Shell&, HighlightMetadata) +{ + editor.stylize({ m_position.start_offset, m_position.end_offset }, { Line::Style::Foreground(Line::Style::XtermColor::Yellow) }); +} + void DoubleQuotedString::dump(int level) const { Node::dump(level); @@ -1011,7 +1033,10 @@ void ForLoop::dump(int level) const { Node::dump(level); print_indented(String::format("%s in\n", m_variable_name.characters()), level + 1); - m_iterated_expression->dump(level + 2); + if (m_iterated_expression) + m_iterated_expression->dump(level + 2); + else + print_indented("(ever)", level + 2); print_indented("Running", level + 1); if (m_block) m_block->dump(level + 2); @@ -1025,18 +1050,15 @@ RefPtr ForLoop::run(RefPtr shell) return create({}); size_t consecutive_interruptions = 0; - - m_iterated_expression->for_each_entry(shell, [&](auto value) { - if (consecutive_interruptions == 2) + auto run = [&](auto& block_value) { + if (shell->has_error(Shell::ShellError::InternalControlFlowBreak)) { + shell->take_error(); return IterationDecision::Break; + } - RefPtr block_value; - - { - auto frame = shell->push_frame(String::formatted("for ({})", this)); - shell->set_local_variable(m_variable_name, value, true); - - block_value = m_block->run(shell); + if (shell->has_error(Shell::ShellError::InternalControlFlowContinue)) { + shell->take_error(); + return IterationDecision::Continue; } if (block_value->is_job()) { @@ -1055,19 +1077,48 @@ RefPtr ForLoop::run(RefPtr shell) } } return IterationDecision::Continue; - }); + }; + + if (m_iterated_expression) { + m_iterated_expression->for_each_entry(shell, [&](auto value) { + if (consecutive_interruptions == 2) + return IterationDecision::Break; + + RefPtr block_value; + + { + auto frame = shell->push_frame(String::formatted("for ({})", this)); + shell->set_local_variable(m_variable_name, value, true); + + block_value = m_block->run(shell); + } + return run(block_value); + }); + } else { + for (;;) { + if (consecutive_interruptions == 2) + break; + + RefPtr block_value = m_block->run(shell); + if (run(block_value) == IterationDecision::Break) + break; + } + } return create({}); } void ForLoop::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata) { - editor.stylize({ m_position.start_offset, m_position.start_offset + 3 }, { Line::Style::Foreground(Line::Style::XtermColor::Yellow) }); - 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) }); + auto is_loop = m_iterated_expression.is_null(); + editor.stylize({ m_position.start_offset, m_position.start_offset + (is_loop ? 4 : 3) }, { Line::Style::Foreground(Line::Style::XtermColor::Yellow) }); + if (!is_loop) { + 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) }); - metadata.is_first_in_list = false; - m_iterated_expression->highlight_in_editor(editor, shell, metadata); + metadata.is_first_in_list = false; + m_iterated_expression->highlight_in_editor(editor, shell, metadata); + } metadata.is_first_in_list = true; if (m_block) @@ -1079,8 +1130,10 @@ HitTestResult ForLoop::hit_test_position(size_t offset) if (!position().contains(offset)) return {}; - if (auto result = m_iterated_expression->hit_test_position(offset); result.matching_node) - return result; + if (m_iterated_expression) { + if (auto result = m_iterated_expression->hit_test_position(offset); result.matching_node) + return result; + } if (!m_block) return {}; @@ -1088,14 +1141,14 @@ HitTestResult ForLoop::hit_test_position(size_t offset) return m_block->hit_test_position(offset); } -ForLoop::ForLoop(Position position, String variable_name, NonnullRefPtr iterated_expr, RefPtr block, Optional in_kw_position) +ForLoop::ForLoop(Position position, String variable_name, RefPtr iterated_expr, RefPtr block, Optional in_kw_position) : Node(move(position)) , m_variable_name(move(variable_name)) , m_iterated_expression(move(iterated_expr)) , m_block(move(block)) , m_in_kw_position(move(in_kw_position)) { - if (m_iterated_expression->is_syntax_error()) + if (m_iterated_expression && m_iterated_expression->is_syntax_error()) set_is_syntax_error(m_iterated_expression->syntax_error_node()); else if (m_block && m_block->is_syntax_error()) set_is_syntax_error(m_block->syntax_error_node()); @@ -1221,8 +1274,10 @@ void Execute::for_each_entry(RefPtr shell, Functionset_event_mask(Core::Notifier::Read); } }; - if (check_and_call() == Break) + if (check_and_call() == Break) { + loop.quit(Break); return; + } auto read_size = read(pipefd[0], buffer, remaining_size); if (read_size < 0) { @@ -1243,12 +1298,20 @@ void Execute::for_each_entry(RefPtr shell, Functionrun_commands(commands); + auto jobs = shell->run_commands(commands); + ScopeGuard kill_jobs_if_around { [&] { + for (auto& job : jobs) { + if (job.is_running_in_background() && !job.exited() && !job.signaled()) { + job.set_should_announce_signal(false); // We're explicitly killing it here. + shell->kill_job(&job, SIGTERM); + } + } + } }; - loop.exec(); + auto exit_reason = loop.exec(); notifier->on_ready_to_read = nullptr; @@ -1256,7 +1319,7 @@ void Execute::for_each_entry(RefPtr shell, Function MatchExpr::run(RefPtr shell) } } - // FIXME: Somehow raise an error in the shell. - dbgln("Non-exhaustive match rules!"); + shell->raise_error(Shell::ShellError::EvaluatedSyntaxError, "Non-exhaustive match rules!"); return create({}); } @@ -1927,7 +1989,7 @@ RefPtr Range::run(RefPtr shell) } } else { yield_start_end:; - warnln("Shell: Cannot interpolate between '{}' and '{}'!", start_str, end_str); + shell->raise_error(Shell::ShellError::EvaluatedSyntaxError, String::formatted("Cannot interpolate between '{}' and '{}'!", start_str, end_str)); // We can't really interpolate between the two, so just yield both. values.append(create(move(start_str))); values.append(create(move(end_str))); @@ -2471,10 +2533,9 @@ void SyntaxError::dump(int level) const print_indented(String::formatted("{}", m_is_continuable), level + 2); } -RefPtr SyntaxError::run(RefPtr) +RefPtr SyntaxError::run(RefPtr shell) { - dbgln("SYNTAX ERROR executed from"); - dump(1); + shell->raise_error(Shell::ShellError::EvaluatedSyntaxError, m_syntax_error_text); return create(""); } @@ -2786,7 +2847,10 @@ Vector GlobValue::resolve_as_list(RefPtr shell) if (!shell) return { m_glob }; - return shell->expand_globs(m_glob, shell->cwd); + auto results = shell->expand_globs(m_glob, shell->cwd); + if (results.is_empty()) + shell->raise_error(Shell::ShellError::InvalidGlobError, "Glob did not match anything!"); + return results; } SimpleVariableValue::~SimpleVariableValue() diff --git a/Shell/AST.h b/Shell/AST.h index 0554c24d570..48043a6f444 100644 --- a/Shell/AST.h +++ b/Shell/AST.h @@ -447,6 +447,7 @@ public: CloseFdRedirection, CommandLiteral, Comment, + ContinuationControl, DynamicEvaluate, DoubleQuotedString, Fd2FdRedirection, @@ -700,6 +701,32 @@ private: String m_text; }; +class ContinuationControl final : public Node { +public: + enum ContinuationKind { + Break, + Continue, + }; + ContinuationControl(Position position, ContinuationKind kind) + : Node(move(position)) + , m_kind(kind) + { + } + virtual ~ContinuationControl() { } + virtual void visit(NodeVisitor& visitor) override { visitor.visit(this); } + + ContinuationKind continuation_kind() const { return m_kind; } + +private: + NODE(ContinuationControl); + + virtual void dump(int level) const override; + virtual RefPtr run(RefPtr) override; + virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override; + + ContinuationKind m_kind { ContinuationKind::Break }; +}; + class DynamicEvaluate final : public Node { public: DynamicEvaluate(Position, NonnullRefPtr); @@ -795,12 +822,12 @@ private: class ForLoop final : public Node { public: - ForLoop(Position, String variable_name, NonnullRefPtr iterated_expr, RefPtr block, Optional in_kw_position = {}); + ForLoop(Position, String variable_name, RefPtr iterated_expr, RefPtr block, Optional in_kw_position = {}); virtual ~ForLoop(); virtual void visit(NodeVisitor& visitor) override { visitor.visit(this); } const String& variable_name() const { return m_variable_name; } - const NonnullRefPtr& iterated_expression() const { return m_iterated_expression; } + const RefPtr& iterated_expression() const { return m_iterated_expression; } const RefPtr& block() const { return m_block; } const Optional in_keyword_position() const { return m_in_kw_position; } @@ -813,7 +840,7 @@ private: virtual bool would_execute() const override { return true; } String m_variable_name; - NonnullRefPtr m_iterated_expression; + RefPtr m_iterated_expression; RefPtr m_block; Optional m_in_kw_position; }; diff --git a/Shell/Formatter.cpp b/Shell/Formatter.cpp index 05975c4aca8..e12b7b95515 100644 --- a/Shell/Formatter.cpp +++ b/Shell/Formatter.cpp @@ -261,6 +261,19 @@ void Formatter::visit(const AST::Comment* node) visited(node); } +void Formatter::visit(const AST::ContinuationControl* node) +{ + will_visit(node); + test_and_update_output_cursor(node); + if (node->continuation_kind() == AST::ContinuationControl::Break) + current_builder().append("break"); + else if (node->continuation_kind() == AST::ContinuationControl::Continue) + current_builder().append("continue"); + else + ASSERT_NOT_REACHED(); + visited(node); +} + void Formatter::visit(const AST::DynamicEvaluate* node) { will_visit(node); @@ -327,15 +340,18 @@ void Formatter::visit(const AST::ForLoop* node) { will_visit(node); test_and_update_output_cursor(node); - current_builder().append("for "); + auto is_loop = node->iterated_expression().is_null(); + current_builder().append(is_loop ? "loop" : "for "); TemporaryChange parent { m_parent_node, node }; - if (node->variable_name() != "it") { - current_builder().append(node->variable_name()); - current_builder().append(" in "); - } + if (!is_loop) { + if (node->variable_name() != "it") { + current_builder().append(node->variable_name()); + current_builder().append(" in "); + } - node->iterated_expression()->visit(*this); + node->iterated_expression()->visit(*this); + } current_builder().append(' '); in_new_block([&] { diff --git a/Shell/Formatter.h b/Shell/Formatter.h index 3f6c2c423a7..a4e0318b8c5 100644 --- a/Shell/Formatter.h +++ b/Shell/Formatter.h @@ -64,6 +64,7 @@ private: virtual void visit(const AST::CloseFdRedirection*) override; virtual void visit(const AST::CommandLiteral*) override; virtual void visit(const AST::Comment*) override; + virtual void visit(const AST::ContinuationControl*) override; virtual void visit(const AST::DynamicEvaluate*) override; virtual void visit(const AST::DoubleQuotedString*) override; virtual void visit(const AST::Fd2FdRedirection*) override; diff --git a/Shell/Forward.h b/Shell/Forward.h index d869c02fbf4..b30448da52c 100644 --- a/Shell/Forward.h +++ b/Shell/Forward.h @@ -47,6 +47,7 @@ class CastToList; class CloseFdRedirection; class CommandLiteral; class Comment; +class ContinuationControl; class DynamicEvaluate; class DoubleQuotedString; class Fd2FdRedirection; diff --git a/Shell/NodeVisitor.cpp b/Shell/NodeVisitor.cpp index 01ae2819d14..af55be40d1d 100644 --- a/Shell/NodeVisitor.cpp +++ b/Shell/NodeVisitor.cpp @@ -84,6 +84,10 @@ void NodeVisitor::visit(const AST::Comment*) { } +void NodeVisitor::visit(const AST::ContinuationControl*) +{ +} + void NodeVisitor::visit(const AST::DynamicEvaluate* node) { node->inner()->visit(*this); @@ -107,7 +111,8 @@ void NodeVisitor::visit(const AST::FunctionDeclaration* node) void NodeVisitor::visit(const AST::ForLoop* node) { - node->iterated_expression()->visit(*this); + if (node->iterated_expression()) + node->iterated_expression()->visit(*this); if (node->block()) node->block()->visit(*this); } diff --git a/Shell/NodeVisitor.h b/Shell/NodeVisitor.h index 41d0ece5e5a..4f08c927210 100644 --- a/Shell/NodeVisitor.h +++ b/Shell/NodeVisitor.h @@ -43,6 +43,7 @@ public: virtual void visit(const AST::CloseFdRedirection*); virtual void visit(const AST::CommandLiteral*); virtual void visit(const AST::Comment*); + virtual void visit(const AST::ContinuationControl*); virtual void visit(const AST::DynamicEvaluate*); virtual void visit(const AST::DoubleQuotedString*); virtual void visit(const AST::Fd2FdRedirection*); diff --git a/Shell/Parser.cpp b/Shell/Parser.cpp index cb5b8496ba8..993b6df478e 100644 --- a/Shell/Parser.cpp +++ b/Shell/Parser.cpp @@ -362,6 +362,7 @@ RefPtr Parser::parse_function_decl() } } + TemporaryChange controls { m_continuation_controls_allowed, false }; auto body = parse_toplevel(); { @@ -499,9 +500,15 @@ RefPtr Parser::parse_control_structure() { auto rule_start = push_start(); consume_while(is_whitespace); + if (auto control = parse_continuation_control()) + return control; + if (auto for_loop = parse_for_loop()) return for_loop; + if (auto loop = parse_loop_loop()) + return loop; + if (auto if_expr = parse_if_expr()) return if_expr; @@ -514,6 +521,40 @@ RefPtr Parser::parse_control_structure() return nullptr; } +RefPtr Parser::parse_continuation_control() +{ + if (!m_continuation_controls_allowed) + return nullptr; + + auto rule_start = push_start(); + + if (expect("break")) { + { + auto break_end = push_start(); + if (consume_while(is_any_of(" \t\n;")).is_empty()) { + restore_to(*rule_start); + return nullptr; + } + restore_to(*break_end); + } + return create(AST::ContinuationControl::Break); + } + + if (expect("continue")) { + { + auto continue_end = push_start(); + if (consume_while(is_any_of(" \t\n;")).is_empty()) { + restore_to(*rule_start); + return nullptr; + } + restore_to(*continue_end); + } + return create(AST::ContinuationControl::Continue); + } + + return nullptr; +} + RefPtr Parser::parse_for_loop() { auto rule_start = push_start(); @@ -544,10 +585,8 @@ RefPtr Parser::parse_for_loop() { auto iter_error_start = push_start(); iterated_expression = parse_expression(); - if (!iterated_expression) { - auto syntax_error = create("Expected an expression in 'for' loop", true); - return create(move(variable_name), move(syntax_error), nullptr, move(in_start_position)); // ForLoop Var Iterated Block - } + if (!iterated_expression) + iterated_expression = create("Expected an expression in 'for' loop", true); } consume_while(is_any_of(" \t\n")); @@ -555,10 +594,11 @@ 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), iterated_expression.release_nonnull(), move(syntax_error), move(in_start_position)); // ForLoop Var Iterated Block + return create(move(variable_name), move(iterated_expression), move(syntax_error), move(in_start_position)); // ForLoop Var Iterated Block } } + TemporaryChange controls { m_continuation_controls_allowed, true }; auto body = parse_toplevel(); { @@ -573,7 +613,44 @@ RefPtr Parser::parse_for_loop() } } - return create(move(variable_name), iterated_expression.release_nonnull(), move(body), move(in_start_position)); // ForLoop Var Iterated Block + return create(move(variable_name), move(iterated_expression), move(body), move(in_start_position)); // ForLoop Var Iterated Block +} + +RefPtr Parser::parse_loop_loop() +{ + auto rule_start = push_start(); + if (!expect("loop")) + return nullptr; + + if (consume_while(is_any_of(" \t\n")).is_empty()) { + restore_to(*rule_start); + return nullptr; + } + + { + 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 + } + } + + TemporaryChange controls { m_continuation_controls_allowed, true }; + auto body = parse_toplevel(); + + { + auto cbrace_error_start = push_start(); + if (!expect('}')) { + auto error_start = push_start(); + auto syntax_error = create("Expected a close brace '}' to end a 'loop' loop body", true); + if (body) + body->set_is_syntax_error(*syntax_error); + else + body = syntax_error; + } + } + + return create(String::empty(), nullptr, move(body), Optional {}); // ForLoop null null Block } RefPtr Parser::parse_if_expr() diff --git a/Shell/Parser.h b/Shell/Parser.h index 5fe34308b56..c0d92e3a55f 100644 --- a/Shell/Parser.h +++ b/Shell/Parser.h @@ -61,7 +61,9 @@ private: RefPtr parse_pipe_sequence(); RefPtr parse_command(); RefPtr parse_control_structure(); + RefPtr parse_continuation_control(); RefPtr parse_for_loop(); + RefPtr parse_loop_loop(); RefPtr parse_if_expr(); RefPtr parse_subshell(); RefPtr parse_match_expr(); @@ -139,6 +141,7 @@ private: Vector m_rule_start_lines; bool m_is_in_brace_expansion_spec { false }; + bool m_continuation_controls_allowed { false }; }; #if 0 @@ -151,7 +154,7 @@ sequence :: variable_decls? or_logical_sequence terminator sequence | variable_decls? function_decl (terminator sequence)? | variable_decls? terminator sequence -function_decl :: identifier '(' (ws* identifier)* ')' ws* '{' toplevel '}' +function_decl :: identifier '(' (ws* identifier)* ')' ws* '{' [!c] toplevel '}' or_logical_sequence :: and_logical_sequence '|' '|' and_logical_sequence | and_logical_sequence @@ -170,12 +173,19 @@ pipe_sequence :: command '|' pipe_sequence | control_structure '|' pipe_sequence | control_structure -control_structure :: for_expr - | if_expr - | subshell - | match_expr +control_structure[c] :: for_expr + | loop_expr + | if_expr + | subshell + | match_expr + | ?c: continuation_control -for_expr :: 'for' ws+ (identifier ' '+ 'in' ws*)? expression ws+ '{' toplevel '}' +continuation_control :: 'break' + | 'continue' + +for_expr :: 'for' ws+ (identifier ' '+ 'in' ws*)? expression ws+ '{' [c] toplevel '}' + +loop_expr :: 'loop' ws* '{' [c] toplevel '}' if_expr :: 'if' ws+ or_logical_sequence ws+ '{' toplevel '}' else_clause? diff --git a/Shell/Shell.cpp b/Shell/Shell.cpp index 9f079a80048..06279fb0320 100644 --- a/Shell/Shell.cpp +++ b/Shell/Shell.cpp @@ -554,6 +554,8 @@ int Shell::run_command(const StringView& cmd) // should not be used for execution! ASSERT(!m_default_constructed); + take_error(); + if (cmd.is_empty()) return 0; @@ -891,6 +893,10 @@ RefPtr Shell::run_command(const AST::Command& command) void Shell::run_tail(const AST::Command& invoking_command, const AST::NodeWithAction& next_in_chain, int head_exit_code) { + if (m_error != ShellError::None) { + possibly_print_error(); + return; + } auto evaluate = [&] { if (next_in_chain.node->would_execute()) { next_in_chain.node->run(*this); @@ -929,6 +935,11 @@ void Shell::run_tail(RefPtr job) NonnullRefPtrVector Shell::run_commands(Vector& commands) { + if (m_error != ShellError::None) { + possibly_print_error(); + return {}; + } + NonnullRefPtrVector spawned_jobs; for (auto& command : commands) { @@ -1722,6 +1733,24 @@ void Shell::save_to(JsonObject& object) object.set("jobs", move(job_objects)); } +void Shell::possibly_print_error() const +{ + switch (m_error) { + case ShellError::EvaluatedSyntaxError: + warnln("Shell Syntax Error: {}", m_error_description); + return; + case ShellError::InvalidGlobError: + case ShellError::NonExhaustiveMatchRules: + warnln("Shell: {}", m_error_description); + return; + case ShellError::InternalControlFlowBreak: + case ShellError::InternalControlFlowContinue: + return; + case ShellError::None: + return; + } +} + void FileDescriptionCollector::collect() { for (auto fd : m_fds) diff --git a/Shell/Shell.h b/Shell/Shell.h index f552595814e..05b87971387 100644 --- a/Shell/Shell.h +++ b/Shell/Shell.h @@ -201,6 +201,31 @@ public: ReadLine, }; + enum class ShellError { + None, + InternalControlFlowBreak, + InternalControlFlowContinue, + EvaluatedSyntaxError, + NonExhaustiveMatchRules, + InvalidGlobError, + }; + + void raise_error(ShellError kind, String description) + { + m_error = kind; + m_error_description = move(description); + } + bool has_error(ShellError err) const { return m_error == err; } + const String& error_description() const { return m_error_description; } + ShellError take_error() + { + auto err = m_error; + m_error = ShellError::None; + m_error_description = {}; + return err; + } + void possibly_print_error() const; + #define __ENUMERATE_SHELL_OPTION(name, default_, description) \ bool name { default_ }; @@ -263,6 +288,9 @@ private: bool m_is_subshell { false }; bool m_should_reinstall_signal_handlers { true }; + ShellError m_error { ShellError::None }; + String m_error_description; + bool m_should_format_live { false }; RefPtr m_editor; diff --git a/Shell/Tests/loop.sh b/Shell/Tests/loop.sh index dc7a25b1303..e4a985db87d 100644 --- a/Shell/Tests/loop.sh +++ b/Shell/Tests/loop.sh @@ -4,6 +4,10 @@ singlecommand_ok=yes multicommand_ok=yes inlineexec_ok=yes implicit_ok=yes +infinite_ok='' +break_ok=yes +continue_ok=yes +break_in_infinite_ok='' # Full form # Empty @@ -42,9 +46,32 @@ for ((test 1 = 1) (test 2 = 2)) { $it || unset implicit_ok } +# Infinite loop +loop { + infinite_ok=yes + break + unset break_ok +} + +# 'Continue' +for (1 2 3) { + continue + unset continue_ok +} + +# 'break' in infinite external loop +for $(yes) { + break_in_infinite_ok=yes + break +} + test $singlecommand_ok || echo Fail: Single command inside for body test $multicommand_ok || echo Fail: Multiple commands inside for body test $inlineexec_ok || echo Fail: Inline Exec test $implicit_ok || echo Fail: implicit iter variable +test $infinite_ok || echo Fail: infinite loop +test $break_ok || echo Fail: break +test $continue_ok || echo Fail: continue +test $break_in_infinite_ok || echo Fail: break from external infinite loop -test "$singlecommand_ok $multicommand_ok $inlineexec_ok $implicit_ok" = "yes yes yes yes" || exit 1 +test "$singlecommand_ok $multicommand_ok $inlineexec_ok $implicit_ok $infinite_ok $break_ok $continue_ok $break_in_infinite_ok" = "yes yes yes yes yes yes yes yes" || exit 1