mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-09-30 08:41:15 +00:00
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.
This commit is contained in:
parent
9bd81f34a5
commit
5e5eb615ec
Notes:
sideshowbarker
2024-07-19 00:26:20 +09:00
Author: https://github.com/alimpfard Commit: https://github.com/SerenityOS/serenity/commit/5e5eb615ec1 Pull-request: https://github.com/SerenityOS/serenity/pull/4629
|
@ -221,6 +221,28 @@ $ for * { mv $it 1-$it }
|
||||||
$ for i in $(seq 1 100) { echo $i >> foo }
|
$ 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
|
||||||
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.
|
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? function_decl (terminator sequence)?
|
||||||
| variable_decls? 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
|
or_logical_sequence :: and_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 '|' pipe_sequence
|
||||||
| control_structure
|
| control_structure
|
||||||
|
|
||||||
control_structure :: for_expr
|
control_structure[c] :: for_expr
|
||||||
|
| loop_expr
|
||||||
| if_expr
|
| if_expr
|
||||||
| subshell
|
| subshell
|
||||||
| match_expr
|
| 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?
|
if_expr :: 'if' ws+ or_logical_sequence ws+ '{' toplevel '}' else_clause?
|
||||||
|
|
||||||
|
|
|
@ -190,6 +190,13 @@ private:
|
||||||
auto& span = span_for_node(node);
|
auto& span = span_for_node(node);
|
||||||
span.color = m_palette.syntax_comment();
|
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
|
virtual void visit(const AST::DynamicEvaluate* node) override
|
||||||
{
|
{
|
||||||
NodeVisitor::visit(node);
|
NodeVisitor::visit(node);
|
||||||
|
|
114
Shell/AST.cpp
114
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
|
void DoubleQuotedString::dump(int level) const
|
||||||
{
|
{
|
||||||
Node::dump(level);
|
Node::dump(level);
|
||||||
|
@ -1011,7 +1033,10 @@ void ForLoop::dump(int level) const
|
||||||
{
|
{
|
||||||
Node::dump(level);
|
Node::dump(level);
|
||||||
print_indented(String::format("%s in\n", m_variable_name.characters()), level + 1);
|
print_indented(String::format("%s in\n", m_variable_name.characters()), level + 1);
|
||||||
|
if (m_iterated_expression)
|
||||||
m_iterated_expression->dump(level + 2);
|
m_iterated_expression->dump(level + 2);
|
||||||
|
else
|
||||||
|
print_indented("(ever)", level + 2);
|
||||||
print_indented("Running", level + 1);
|
print_indented("Running", level + 1);
|
||||||
if (m_block)
|
if (m_block)
|
||||||
m_block->dump(level + 2);
|
m_block->dump(level + 2);
|
||||||
|
@ -1025,18 +1050,15 @@ RefPtr<Value> ForLoop::run(RefPtr<Shell> shell)
|
||||||
return create<ListValue>({});
|
return create<ListValue>({});
|
||||||
|
|
||||||
size_t consecutive_interruptions = 0;
|
size_t consecutive_interruptions = 0;
|
||||||
|
auto run = [&](auto& block_value) {
|
||||||
m_iterated_expression->for_each_entry(shell, [&](auto value) {
|
if (shell->has_error(Shell::ShellError::InternalControlFlowBreak)) {
|
||||||
if (consecutive_interruptions == 2)
|
shell->take_error();
|
||||||
return IterationDecision::Break;
|
return IterationDecision::Break;
|
||||||
|
}
|
||||||
|
|
||||||
RefPtr<Value> block_value;
|
if (shell->has_error(Shell::ShellError::InternalControlFlowContinue)) {
|
||||||
|
shell->take_error();
|
||||||
{
|
return IterationDecision::Continue;
|
||||||
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 (block_value->is_job()) {
|
if (block_value->is_job()) {
|
||||||
|
@ -1055,19 +1077,48 @@ RefPtr<Value> ForLoop::run(RefPtr<Shell> shell)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return IterationDecision::Continue;
|
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>({});
|
return create<ListValue>({});
|
||||||
}
|
}
|
||||||
|
|
||||||
void ForLoop::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata)
|
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) });
|
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())
|
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) });
|
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;
|
metadata.is_first_in_list = false;
|
||||||
m_iterated_expression->highlight_in_editor(editor, shell, metadata);
|
m_iterated_expression->highlight_in_editor(editor, shell, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
metadata.is_first_in_list = true;
|
metadata.is_first_in_list = true;
|
||||||
if (m_block)
|
if (m_block)
|
||||||
|
@ -1079,8 +1130,10 @@ HitTestResult ForLoop::hit_test_position(size_t offset)
|
||||||
if (!position().contains(offset))
|
if (!position().contains(offset))
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
|
if (m_iterated_expression) {
|
||||||
if (auto result = m_iterated_expression->hit_test_position(offset); result.matching_node)
|
if (auto result = m_iterated_expression->hit_test_position(offset); result.matching_node)
|
||||||
return result;
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
if (!m_block)
|
if (!m_block)
|
||||||
return {};
|
return {};
|
||||||
|
@ -1088,14 +1141,14 @@ HitTestResult ForLoop::hit_test_position(size_t offset)
|
||||||
return m_block->hit_test_position(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))
|
: Node(move(position))
|
||||||
, m_variable_name(move(variable_name))
|
, m_variable_name(move(variable_name))
|
||||||
, m_iterated_expression(move(iterated_expr))
|
, m_iterated_expression(move(iterated_expr))
|
||||||
, m_block(move(block))
|
, m_block(move(block))
|
||||||
, m_in_kw_position(move(in_kw_position))
|
, 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());
|
set_is_syntax_error(m_iterated_expression->syntax_error_node());
|
||||||
else if (m_block && m_block->is_syntax_error())
|
else if (m_block && m_block->is_syntax_error())
|
||||||
set_is_syntax_error(m_block->syntax_error_node());
|
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);
|
notifier->set_event_mask(Core::Notifier::Read);
|
||||||
} };
|
} };
|
||||||
|
|
||||||
if (check_and_call() == Break)
|
if (check_and_call() == Break) {
|
||||||
|
loop.quit(Break);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto read_size = read(pipefd[0], buffer, remaining_size);
|
auto read_size = read(pipefd[0], buffer, remaining_size);
|
||||||
if (read_size < 0) {
|
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 });
|
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;
|
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));
|
dbgln("close() failed: {}", strerror(errno));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stream.eof()) {
|
if (exit_reason != Break && !stream.eof()) {
|
||||||
auto action = Continue;
|
auto action = Continue;
|
||||||
do {
|
do {
|
||||||
action = check_and_call();
|
action = check_and_call();
|
||||||
|
@ -1610,8 +1673,7 @@ RefPtr<Value> MatchExpr::run(RefPtr<Shell> shell)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Somehow raise an error in the shell.
|
shell->raise_error(Shell::ShellError::EvaluatedSyntaxError, "Non-exhaustive match rules!");
|
||||||
dbgln("Non-exhaustive match rules!");
|
|
||||||
return create<AST::ListValue>({});
|
return create<AST::ListValue>({});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1927,7 +1989,7 @@ RefPtr<Value> Range::run(RefPtr<Shell> shell)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
yield_start_end:;
|
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.
|
// We can't really interpolate between the two, so just yield both.
|
||||||
values.append(create<StringValue>(move(start_str)));
|
values.append(create<StringValue>(move(start_str)));
|
||||||
values.append(create<StringValue>(move(end_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);
|
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");
|
shell->raise_error(Shell::ShellError::EvaluatedSyntaxError, m_syntax_error_text);
|
||||||
dump(1);
|
|
||||||
return create<StringValue>("");
|
return create<StringValue>("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2786,7 +2847,10 @@ Vector<String> GlobValue::resolve_as_list(RefPtr<Shell> shell)
|
||||||
if (!shell)
|
if (!shell)
|
||||||
return { m_glob };
|
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()
|
SimpleVariableValue::~SimpleVariableValue()
|
||||||
|
|
33
Shell/AST.h
33
Shell/AST.h
|
@ -447,6 +447,7 @@ public:
|
||||||
CloseFdRedirection,
|
CloseFdRedirection,
|
||||||
CommandLiteral,
|
CommandLiteral,
|
||||||
Comment,
|
Comment,
|
||||||
|
ContinuationControl,
|
||||||
DynamicEvaluate,
|
DynamicEvaluate,
|
||||||
DoubleQuotedString,
|
DoubleQuotedString,
|
||||||
Fd2FdRedirection,
|
Fd2FdRedirection,
|
||||||
|
@ -700,6 +701,32 @@ private:
|
||||||
String m_text;
|
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 {
|
class DynamicEvaluate final : public Node {
|
||||||
public:
|
public:
|
||||||
DynamicEvaluate(Position, NonnullRefPtr<Node>);
|
DynamicEvaluate(Position, NonnullRefPtr<Node>);
|
||||||
|
@ -795,12 +822,12 @@ private:
|
||||||
|
|
||||||
class ForLoop final : public Node {
|
class ForLoop final : public Node {
|
||||||
public:
|
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 ~ForLoop();
|
||||||
virtual void visit(NodeVisitor& visitor) override { visitor.visit(this); }
|
virtual void visit(NodeVisitor& visitor) override { visitor.visit(this); }
|
||||||
|
|
||||||
const String& variable_name() const { return m_variable_name; }
|
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 RefPtr<Node>& block() const { return m_block; }
|
||||||
const Optional<Position> in_keyword_position() const { return m_in_kw_position; }
|
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; }
|
virtual bool would_execute() const override { return true; }
|
||||||
|
|
||||||
String m_variable_name;
|
String m_variable_name;
|
||||||
NonnullRefPtr<AST::Node> m_iterated_expression;
|
RefPtr<AST::Node> m_iterated_expression;
|
||||||
RefPtr<AST::Node> m_block;
|
RefPtr<AST::Node> m_block;
|
||||||
Optional<Position> m_in_kw_position;
|
Optional<Position> m_in_kw_position;
|
||||||
};
|
};
|
||||||
|
|
|
@ -261,6 +261,19 @@ void Formatter::visit(const AST::Comment* node)
|
||||||
visited(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)
|
void Formatter::visit(const AST::DynamicEvaluate* node)
|
||||||
{
|
{
|
||||||
will_visit(node);
|
will_visit(node);
|
||||||
|
@ -327,15 +340,18 @@ void Formatter::visit(const AST::ForLoop* node)
|
||||||
{
|
{
|
||||||
will_visit(node);
|
will_visit(node);
|
||||||
test_and_update_output_cursor(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 };
|
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||||
|
|
||||||
|
if (!is_loop) {
|
||||||
if (node->variable_name() != "it") {
|
if (node->variable_name() != "it") {
|
||||||
current_builder().append(node->variable_name());
|
current_builder().append(node->variable_name());
|
||||||
current_builder().append(" in ");
|
current_builder().append(" in ");
|
||||||
}
|
}
|
||||||
|
|
||||||
node->iterated_expression()->visit(*this);
|
node->iterated_expression()->visit(*this);
|
||||||
|
}
|
||||||
|
|
||||||
current_builder().append(' ');
|
current_builder().append(' ');
|
||||||
in_new_block([&] {
|
in_new_block([&] {
|
||||||
|
|
|
@ -64,6 +64,7 @@ private:
|
||||||
virtual void visit(const AST::CloseFdRedirection*) override;
|
virtual void visit(const AST::CloseFdRedirection*) override;
|
||||||
virtual void visit(const AST::CommandLiteral*) override;
|
virtual void visit(const AST::CommandLiteral*) override;
|
||||||
virtual void visit(const AST::Comment*) 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::DynamicEvaluate*) override;
|
||||||
virtual void visit(const AST::DoubleQuotedString*) override;
|
virtual void visit(const AST::DoubleQuotedString*) override;
|
||||||
virtual void visit(const AST::Fd2FdRedirection*) override;
|
virtual void visit(const AST::Fd2FdRedirection*) override;
|
||||||
|
|
|
@ -47,6 +47,7 @@ class CastToList;
|
||||||
class CloseFdRedirection;
|
class CloseFdRedirection;
|
||||||
class CommandLiteral;
|
class CommandLiteral;
|
||||||
class Comment;
|
class Comment;
|
||||||
|
class ContinuationControl;
|
||||||
class DynamicEvaluate;
|
class DynamicEvaluate;
|
||||||
class DoubleQuotedString;
|
class DoubleQuotedString;
|
||||||
class Fd2FdRedirection;
|
class Fd2FdRedirection;
|
||||||
|
|
|
@ -84,6 +84,10 @@ void NodeVisitor::visit(const AST::Comment*)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NodeVisitor::visit(const AST::ContinuationControl*)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
void NodeVisitor::visit(const AST::DynamicEvaluate* node)
|
void NodeVisitor::visit(const AST::DynamicEvaluate* node)
|
||||||
{
|
{
|
||||||
node->inner()->visit(*this);
|
node->inner()->visit(*this);
|
||||||
|
@ -107,6 +111,7 @@ void NodeVisitor::visit(const AST::FunctionDeclaration* node)
|
||||||
|
|
||||||
void NodeVisitor::visit(const AST::ForLoop* node)
|
void NodeVisitor::visit(const AST::ForLoop* node)
|
||||||
{
|
{
|
||||||
|
if (node->iterated_expression())
|
||||||
node->iterated_expression()->visit(*this);
|
node->iterated_expression()->visit(*this);
|
||||||
if (node->block())
|
if (node->block())
|
||||||
node->block()->visit(*this);
|
node->block()->visit(*this);
|
||||||
|
|
|
@ -43,6 +43,7 @@ public:
|
||||||
virtual void visit(const AST::CloseFdRedirection*);
|
virtual void visit(const AST::CloseFdRedirection*);
|
||||||
virtual void visit(const AST::CommandLiteral*);
|
virtual void visit(const AST::CommandLiteral*);
|
||||||
virtual void visit(const AST::Comment*);
|
virtual void visit(const AST::Comment*);
|
||||||
|
virtual void visit(const AST::ContinuationControl*);
|
||||||
virtual void visit(const AST::DynamicEvaluate*);
|
virtual void visit(const AST::DynamicEvaluate*);
|
||||||
virtual void visit(const AST::DoubleQuotedString*);
|
virtual void visit(const AST::DoubleQuotedString*);
|
||||||
virtual void visit(const AST::Fd2FdRedirection*);
|
virtual void visit(const AST::Fd2FdRedirection*);
|
||||||
|
|
|
@ -362,6 +362,7 @@ RefPtr<AST::Node> Parser::parse_function_decl()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TemporaryChange controls { m_continuation_controls_allowed, false };
|
||||||
auto body = parse_toplevel();
|
auto body = parse_toplevel();
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -499,9 +500,15 @@ RefPtr<AST::Node> Parser::parse_control_structure()
|
||||||
{
|
{
|
||||||
auto rule_start = push_start();
|
auto rule_start = push_start();
|
||||||
consume_while(is_whitespace);
|
consume_while(is_whitespace);
|
||||||
|
if (auto control = parse_continuation_control())
|
||||||
|
return control;
|
||||||
|
|
||||||
if (auto for_loop = parse_for_loop())
|
if (auto for_loop = parse_for_loop())
|
||||||
return for_loop;
|
return for_loop;
|
||||||
|
|
||||||
|
if (auto loop = parse_loop_loop())
|
||||||
|
return loop;
|
||||||
|
|
||||||
if (auto if_expr = parse_if_expr())
|
if (auto if_expr = parse_if_expr())
|
||||||
return if_expr;
|
return if_expr;
|
||||||
|
|
||||||
|
@ -514,6 +521,40 @@ RefPtr<AST::Node> Parser::parse_control_structure()
|
||||||
return nullptr;
|
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()
|
RefPtr<AST::Node> Parser::parse_for_loop()
|
||||||
{
|
{
|
||||||
auto rule_start = push_start();
|
auto rule_start = push_start();
|
||||||
|
@ -544,10 +585,8 @@ RefPtr<AST::Node> Parser::parse_for_loop()
|
||||||
{
|
{
|
||||||
auto iter_error_start = push_start();
|
auto iter_error_start = push_start();
|
||||||
iterated_expression = parse_expression();
|
iterated_expression = parse_expression();
|
||||||
if (!iterated_expression) {
|
if (!iterated_expression)
|
||||||
auto syntax_error = create<AST::SyntaxError>("Expected an expression in 'for' loop", true);
|
iterated_expression = 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
consume_while(is_any_of(" \t\n"));
|
consume_while(is_any_of(" \t\n"));
|
||||||
|
@ -555,10 +594,11 @@ RefPtr<AST::Node> Parser::parse_for_loop()
|
||||||
auto obrace_error_start = push_start();
|
auto obrace_error_start = push_start();
|
||||||
if (!expect('{')) {
|
if (!expect('{')) {
|
||||||
auto syntax_error = create<AST::SyntaxError>("Expected an open brace '{' to start a 'for' loop body", true);
|
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();
|
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()
|
RefPtr<AST::Node> Parser::parse_if_expr()
|
||||||
|
|
|
@ -61,7 +61,9 @@ private:
|
||||||
RefPtr<AST::Node> parse_pipe_sequence();
|
RefPtr<AST::Node> parse_pipe_sequence();
|
||||||
RefPtr<AST::Node> parse_command();
|
RefPtr<AST::Node> parse_command();
|
||||||
RefPtr<AST::Node> parse_control_structure();
|
RefPtr<AST::Node> parse_control_structure();
|
||||||
|
RefPtr<AST::Node> parse_continuation_control();
|
||||||
RefPtr<AST::Node> parse_for_loop();
|
RefPtr<AST::Node> parse_for_loop();
|
||||||
|
RefPtr<AST::Node> parse_loop_loop();
|
||||||
RefPtr<AST::Node> parse_if_expr();
|
RefPtr<AST::Node> parse_if_expr();
|
||||||
RefPtr<AST::Node> parse_subshell();
|
RefPtr<AST::Node> parse_subshell();
|
||||||
RefPtr<AST::Node> parse_match_expr();
|
RefPtr<AST::Node> parse_match_expr();
|
||||||
|
@ -139,6 +141,7 @@ private:
|
||||||
Vector<AST::Position::Line> m_rule_start_lines;
|
Vector<AST::Position::Line> m_rule_start_lines;
|
||||||
|
|
||||||
bool m_is_in_brace_expansion_spec { false };
|
bool m_is_in_brace_expansion_spec { false };
|
||||||
|
bool m_continuation_controls_allowed { false };
|
||||||
};
|
};
|
||||||
|
|
||||||
#if 0
|
#if 0
|
||||||
|
@ -151,7 +154,7 @@ sequence :: variable_decls? or_logical_sequence terminator sequence
|
||||||
| variable_decls? function_decl (terminator sequence)?
|
| variable_decls? function_decl (terminator sequence)?
|
||||||
| variable_decls? 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
|
or_logical_sequence :: and_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 '|' pipe_sequence
|
||||||
| control_structure
|
| control_structure
|
||||||
|
|
||||||
control_structure :: for_expr
|
control_structure[c] :: for_expr
|
||||||
|
| loop_expr
|
||||||
| if_expr
|
| if_expr
|
||||||
| subshell
|
| subshell
|
||||||
| match_expr
|
| 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?
|
if_expr :: 'if' ws+ or_logical_sequence ws+ '{' toplevel '}' else_clause?
|
||||||
|
|
||||||
|
|
|
@ -554,6 +554,8 @@ int Shell::run_command(const StringView& cmd)
|
||||||
// should not be used for execution!
|
// should not be used for execution!
|
||||||
ASSERT(!m_default_constructed);
|
ASSERT(!m_default_constructed);
|
||||||
|
|
||||||
|
take_error();
|
||||||
|
|
||||||
if (cmd.is_empty())
|
if (cmd.is_empty())
|
||||||
return 0;
|
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)
|
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 = [&] {
|
auto evaluate = [&] {
|
||||||
if (next_in_chain.node->would_execute()) {
|
if (next_in_chain.node->would_execute()) {
|
||||||
next_in_chain.node->run(*this);
|
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)
|
NonnullRefPtrVector<Job> Shell::run_commands(Vector<AST::Command>& commands)
|
||||||
{
|
{
|
||||||
|
if (m_error != ShellError::None) {
|
||||||
|
possibly_print_error();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
NonnullRefPtrVector<Job> spawned_jobs;
|
NonnullRefPtrVector<Job> spawned_jobs;
|
||||||
|
|
||||||
for (auto& command : commands) {
|
for (auto& command : commands) {
|
||||||
|
@ -1722,6 +1733,24 @@ void Shell::save_to(JsonObject& object)
|
||||||
object.set("jobs", move(job_objects));
|
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()
|
void FileDescriptionCollector::collect()
|
||||||
{
|
{
|
||||||
for (auto fd : m_fds)
|
for (auto fd : m_fds)
|
||||||
|
|
|
@ -201,6 +201,31 @@ public:
|
||||||
ReadLine,
|
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) \
|
#define __ENUMERATE_SHELL_OPTION(name, default_, description) \
|
||||||
bool name { default_ };
|
bool name { default_ };
|
||||||
|
|
||||||
|
@ -263,6 +288,9 @@ private:
|
||||||
bool m_is_subshell { false };
|
bool m_is_subshell { false };
|
||||||
bool m_should_reinstall_signal_handlers { true };
|
bool m_should_reinstall_signal_handlers { true };
|
||||||
|
|
||||||
|
ShellError m_error { ShellError::None };
|
||||||
|
String m_error_description;
|
||||||
|
|
||||||
bool m_should_format_live { false };
|
bool m_should_format_live { false };
|
||||||
|
|
||||||
RefPtr<Line::Editor> m_editor;
|
RefPtr<Line::Editor> m_editor;
|
||||||
|
|
|
@ -4,6 +4,10 @@ singlecommand_ok=yes
|
||||||
multicommand_ok=yes
|
multicommand_ok=yes
|
||||||
inlineexec_ok=yes
|
inlineexec_ok=yes
|
||||||
implicit_ok=yes
|
implicit_ok=yes
|
||||||
|
infinite_ok=''
|
||||||
|
break_ok=yes
|
||||||
|
continue_ok=yes
|
||||||
|
break_in_infinite_ok=''
|
||||||
|
|
||||||
# Full form
|
# Full form
|
||||||
# Empty
|
# Empty
|
||||||
|
@ -42,9 +46,32 @@ for ((test 1 = 1) (test 2 = 2)) {
|
||||||
$it || unset implicit_ok
|
$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 $singlecommand_ok || echo Fail: Single command inside for body
|
||||||
test $multicommand_ok || echo Fail: Multiple commands inside for body
|
test $multicommand_ok || echo Fail: Multiple commands inside for body
|
||||||
test $inlineexec_ok || echo Fail: Inline Exec
|
test $inlineexec_ok || echo Fail: Inline Exec
|
||||||
test $implicit_ok || echo Fail: implicit iter variable
|
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
|
||||||
|
|
Loading…
Reference in a new issue