summaryrefslogtreecommitdiff
path: root/Shell
diff options
context:
space:
mode:
authorAnotherTest <ali.mpfard@gmail.com>2020-12-10 18:25:13 +0330
committerAndreas Kling <kling@serenityos.org>2020-12-29 16:55:43 +0100
commit5e5eb615ec1b7020548420afd7084f91674ace3d (patch)
tree089b6aa3a4ca18983200aac006e37a5c5b0b4618 /Shell
parent9bd81f34a522b1d5e48e1f1e8c308a2ac38f415d (diff)
downloadserenity-5e5eb615ec1b7020548420afd7084f91674ace3d.zip
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.
Diffstat (limited to 'Shell')
-rw-r--r--Shell/AST.cpp130
-rw-r--r--Shell/AST.h33
-rw-r--r--Shell/Formatter.cpp28
-rw-r--r--Shell/Formatter.h1
-rw-r--r--Shell/Forward.h1
-rw-r--r--Shell/NodeVisitor.cpp7
-rw-r--r--Shell/NodeVisitor.h1
-rw-r--r--Shell/Parser.cpp89
-rw-r--r--Shell/Parser.h22
-rw-r--r--Shell/Shell.cpp29
-rw-r--r--Shell/Shell.h28
-rw-r--r--Shell/Tests/loop.sh29
12 files changed, 342 insertions, 56 deletions
diff --git a/Shell/AST.cpp b/Shell/AST.cpp
index a0350fa755..2bd877624d 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<Value> ContinuationControl::run(RefPtr<Shell> 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<ListValue>({});
+}
+
+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<Value> ForLoop::run(RefPtr<Shell> shell)
return create<ListValue>({});
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<Value> 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<Value> ForLoop::run(RefPtr<Shell> 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<Value> 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<Value> block_value = m_block->run(shell);
+ if (run(block_value) == IterationDecision::Break)
+ break;
+ }
+ }
return create<ListValue>({});
}
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<AST::Node> iterated_expr, RefPtr<AST::Node> block, Optional<Position> in_kw_position)
+ForLoop::ForLoop(Position position, String variable_name, RefPtr<AST::Node> iterated_expr, RefPtr<AST::Node> block, Optional<Position> 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> shell, Function<IterationDecision(Non
notifier->set_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> shell, Function<IterationDecision(Non
stream.write({ buffer, (size_t)read_size });
}
- loop.quit(Break);
+ loop.quit(NothingLeft);
};
- shell->run_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> shell, Function<IterationDecision(Non
dbgln("close() failed: {}", strerror(errno));
}
- if (!stream.eof()) {
+ if (exit_reason != Break && !stream.eof()) {
auto action = Continue;
do {
action = check_and_call();
@@ -1610,8 +1673,7 @@ RefPtr<Value> MatchExpr::run(RefPtr<Shell> 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<AST::ListValue>({});
}
@@ -1927,7 +1989,7 @@ RefPtr<Value> Range::run(RefPtr<Shell> 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<StringValue>(move(start_str)));
values.append(create<StringValue>(move(end_str)));
@@ -2471,10 +2533,9 @@ void SyntaxError::dump(int level) const
print_indented(String::formatted("{}", m_is_continuable), level + 2);
}
-RefPtr<Value> SyntaxError::run(RefPtr<Shell>)
+RefPtr<Value> SyntaxError::run(RefPtr<Shell> shell)
{
- dbgln("SYNTAX ERROR executed from");
- dump(1);
+ shell->raise_error(Shell::ShellError::EvaluatedSyntaxError, m_syntax_error_text);
return create<StringValue>("");
}
@@ -2786,7 +2847,10 @@ Vector<String> GlobValue::resolve_as_list(RefPtr<Shell> 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 0554c24d57..48043a6f44 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<Value> run(RefPtr<Shell>) 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<Node>);
@@ -795,12 +822,12 @@ private:
class ForLoop final : public Node {
public:
- ForLoop(Position, String variable_name, NonnullRefPtr<AST::Node> iterated_expr, RefPtr<AST::Node> block, Optional<Position> in_kw_position = {});
+ ForLoop(Position, String variable_name, RefPtr<AST::Node> iterated_expr, RefPtr<AST::Node> block, Optional<Position> 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<Node>& iterated_expression() const { return m_iterated_expression; }
+ const RefPtr<Node>& iterated_expression() const { return m_iterated_expression; }
const RefPtr<Node>& block() const { return m_block; }
const Optional<Position> 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<AST::Node> m_iterated_expression;
+ RefPtr<AST::Node> m_iterated_expression;
RefPtr<AST::Node> m_block;
Optional<Position> m_in_kw_position;
};
diff --git a/Shell/Formatter.cpp b/Shell/Formatter.cpp
index 05975c4aca..e12b7b9551 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<const AST::Node*> 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 3f6c2c423a..a4e0318b8c 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 d869c02fbf..b30448da52 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 01ae2819d1..af55be40d1 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 41d0ece5e5..4f08c92721 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 cb5b8496ba..993b6df478 100644
--- a/Shell/Parser.cpp
+++ b/Shell/Parser.cpp
@@ -362,6 +362,7 @@ RefPtr<AST::Node> Parser::parse_function_decl()
}
}
+ TemporaryChange controls { m_continuation_controls_allowed, false };
auto body = parse_toplevel();
{
@@ -499,9 +500,15 @@ RefPtr<AST::Node> 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<AST::Node> Parser::parse_control_structure()
return nullptr;
}
+RefPtr<AST::Node> 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>(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>(AST::ContinuationControl::Continue);
+ }
+
+ return nullptr;
+}
+
RefPtr<AST::Node> Parser::parse_for_loop()
{
auto rule_start = push_start();
@@ -544,10 +585,8 @@ RefPtr<AST::Node> Parser::parse_for_loop()
{
auto iter_error_start = push_start();
iterated_expression = parse_expression();
- if (!iterated_expression) {
- auto syntax_error = create<AST::SyntaxError>("Expected an expression in 'for' loop", true);
- return create<AST::ForLoop>(move(variable_name), move(syntax_error), nullptr, move(in_start_position)); // ForLoop Var Iterated Block
- }
+ if (!iterated_expression)
+ iterated_expression = create<AST::SyntaxError>("Expected an expression in 'for' loop", true);
}
consume_while(is_any_of(" \t\n"));
@@ -555,10 +594,11 @@ 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), iterated_expression.release_nonnull(), move(syntax_error), move(in_start_position)); // ForLoop Var Iterated Block
+ return create<AST::ForLoop>(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<AST::Node> Parser::parse_for_loop()
}
}
- return create<AST::ForLoop>(move(variable_name), iterated_expression.release_nonnull(), move(body), move(in_start_position)); // ForLoop Var Iterated Block
+ return create<AST::ForLoop>(move(variable_name), move(iterated_expression), move(body), move(in_start_position)); // ForLoop Var Iterated Block
+}
+
+RefPtr<AST::Node> 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<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
+ }
+ }
+
+ 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<AST::SyntaxError>("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<AST::ForLoop>(String::empty(), nullptr, move(body), Optional<AST::Position> {}); // ForLoop null null Block
}
RefPtr<AST::Node> Parser::parse_if_expr()
diff --git a/Shell/Parser.h b/Shell/Parser.h
index 5fe34308b5..c0d92e3a55 100644
--- a/Shell/Parser.h
+++ b/Shell/Parser.h
@@ -61,7 +61,9 @@ private:
RefPtr<AST::Node> parse_pipe_sequence();
RefPtr<AST::Node> parse_command();
RefPtr<AST::Node> parse_control_structure();
+ RefPtr<AST::Node> parse_continuation_control();
RefPtr<AST::Node> parse_for_loop();
+ RefPtr<AST::Node> parse_loop_loop();
RefPtr<AST::Node> parse_if_expr();
RefPtr<AST::Node> parse_subshell();
RefPtr<AST::Node> parse_match_expr();
@@ -139,6 +141,7 @@ private:
Vector<AST::Position::Line> 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 9f079a8004..06279fb032 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<Job> 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> job)
NonnullRefPtrVector<Job> Shell::run_commands(Vector<AST::Command>& commands)
{
+ if (m_error != ShellError::None) {
+ possibly_print_error();
+ return {};
+ }
+
NonnullRefPtrVector<Job> 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 f552595814..05b8797138 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<Line::Editor> m_editor;
diff --git a/Shell/Tests/loop.sh b/Shell/Tests/loop.sh
index dc7a25b130..e4a985db87 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