From 5e5eb615ec1b7020548420afd7084f91674ace3d Mon Sep 17 00:00:00 2001 From: AnotherTest Date: Thu, 10 Dec 2020 18:25:13 +0330 Subject: [PATCH] Shell: Add runtime errors and implement break/continue Such errors are raised when SyntaxError nodes are executed, and are also used for internal control flow. The 'break' and 'continue' commands are currently only allowed inside for loops, and outside function bodies. This also adds a 'loop' keyword for infinite loops. --- Base/usr/share/man/man5/Shell.md | 41 +++++- Libraries/LibGUI/ShellSyntaxHighlighter.cpp | 7 ++ Shell/AST.cpp | 130 +++++++++++++++----- Shell/AST.h | 33 ++++- Shell/Formatter.cpp | 28 ++++- Shell/Formatter.h | 1 + Shell/Forward.h | 1 + Shell/NodeVisitor.cpp | 7 +- Shell/NodeVisitor.h | 1 + Shell/Parser.cpp | 89 +++++++++++++- Shell/Parser.h | 22 +++- Shell/Shell.cpp | 29 +++++ Shell/Shell.h | 28 +++++ Shell/Tests/loop.sh | 29 ++++- 14 files changed, 384 insertions(+), 62 deletions(-) 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