Shell: Start implementing a POSIX-compliant parser

The parser is still very much a work-in-progress, but it can currently
parse most of the basic bits, the only *completely* unimplemented things
in the parser are:
- heredocs (io_here)
- alias expansion
- arithmetic expansion

There are a whole suite of bugs, and syntax highlighting is unreliable
at best.
For now, this is not attached anywhere, a future commit will enable it
for /bin/sh or a `Shell --posix` invocation.
This commit is contained in:
Ali Mohammad Pur 2023-02-11 17:59:15 +03:30 committed by Ali Mohammad Pur
parent 2dc1682274
commit 2a276c86d4
Notes: sideshowbarker 2024-07-17 06:09:44 +09:00
14 changed files with 3444 additions and 28 deletions

View File

@ -410,6 +410,10 @@
#cmakedefine01 SHELL_JOB_DEBUG
#endif
#ifndef SHELL_POSIX_PARSER_DEBUG
#cmakedefine01 SHELL_POSIX_PARSER_DEBUG
#endif
#ifndef SOLITAIRE_DEBUG
#cmakedefine01 SOLITAIRE_DEBUG
#endif

View File

@ -167,6 +167,7 @@ set(SH_DEBUG ON)
set(SHELL_JOB_DEBUG ON)
set(SH_LANGUAGE_SERVER_DEBUG ON)
set(SHARED_QUEUE_DEBUG ON)
set(SHELL_POSIX_PARSER_DEBUG ON)
set(SIGNAL_DEBUG ON)
set(SLAVEPTY_DEBUG ON)
set(SMP_DEBUG ON)

View File

@ -3004,6 +3004,24 @@ RefPtr<Value> Juxtaposition::run(RefPtr<Shell> shell)
auto left = left_value->resolve_as_list(shell);
auto right = right_value->resolve_as_list(shell);
if (m_mode == Mode::StringExpand) {
Vector<DeprecatedString> result;
result.ensure_capacity(left.size() + right.size());
for (auto& left_item : left)
result.append(left_item);
if (!result.is_empty() && !right.is_empty()) {
auto& last = result.last();
last = DeprecatedString::formatted("{}{}", last, right.first());
right.take_first();
}
for (auto& right_item : right)
result.append(right_item);
return make_ref_counted<ListValue>(move(result));
}
if (left_value->is_string() && right_value->is_string()) {
VERIFY(left.size() == 1);
@ -3016,7 +3034,7 @@ RefPtr<Value> Juxtaposition::run(RefPtr<Shell> shell)
return make_ref_counted<StringValue>(builder.to_deprecated_string());
}
// Otherwise, treat them as lists and create a list product.
// Otherwise, treat them as lists and create a list product (or just append).
if (left.is_empty() || right.is_empty())
return make_ref_counted<ListValue>({});
@ -3114,10 +3132,11 @@ HitTestResult Juxtaposition::hit_test_position(size_t offset) const
return result;
}
Juxtaposition::Juxtaposition(Position position, NonnullRefPtr<Node> left, NonnullRefPtr<Node> right)
Juxtaposition::Juxtaposition(Position position, NonnullRefPtr<Node> left, NonnullRefPtr<Node> right, Juxtaposition::Mode mode)
: Node(move(position))
, m_left(move(left))
, m_right(move(right))
, m_mode(mode)
{
if (m_left->is_syntax_error())
set_is_syntax_error(m_left->syntax_error_node());

View File

@ -47,6 +47,16 @@ struct Position {
} start_line, end_line;
bool contains(size_t offset) const { return start_offset <= offset && offset <= end_offset; }
Position with_end(Position const& end) const
{
return {
.start_offset = start_offset,
.end_offset = end.end_offset,
.start_line = start_line,
.end_line = end.end_line,
};
}
};
struct NameWithPosition {
@ -989,6 +999,7 @@ public:
NonnullRefPtr<Node> const& condition() const { return m_condition; }
RefPtr<Node> const& true_branch() const { return m_true_branch; }
RefPtr<Node> const& false_branch() const { return m_false_branch; }
RefPtr<Node>& false_branch() { return m_false_branch; }
Optional<Position> const else_position() const { return m_else_position; }
private:
@ -1301,7 +1312,11 @@ private:
class Juxtaposition final : public Node {
public:
Juxtaposition(Position, NonnullRefPtr<Node>, NonnullRefPtr<Node>);
enum class Mode {
ListExpand,
StringExpand,
};
Juxtaposition(Position, NonnullRefPtr<Node>, NonnullRefPtr<Node>, Mode = Mode::ListExpand);
virtual ~Juxtaposition();
virtual void visit(NodeVisitor& visitor) override { visitor.visit(this); }
@ -1318,6 +1333,7 @@ private:
NonnullRefPtr<Node> m_left;
NonnullRefPtr<Node> m_right;
Mode m_mode { Mode::ListExpand };
};
class Heredoc final : public Node {

View File

@ -12,6 +12,8 @@ set(SOURCES
Job.cpp
NodeVisitor.cpp
Parser.cpp
PosixLexer.cpp
PosixParser.cpp
Shell.cpp
)

View File

@ -7,6 +7,7 @@
#include "Formatter.h"
#include "AST.h"
#include "Parser.h"
#include "PosixParser.h"
#include <AK/Hex.h>
#include <AK/ScopedValueRollback.h>
#include <AK/TemporaryChange.h>
@ -15,7 +16,7 @@ namespace Shell {
DeprecatedString Formatter::format()
{
auto node = m_root_node ? m_root_node : Parser(m_source).parse();
auto node = m_root_node ?: (m_parse_as_posix ? Posix::Parser(m_source).parse() : Parser(m_source).parse());
if (m_cursor >= 0)
m_output_cursor = m_cursor;

View File

@ -19,10 +19,11 @@ namespace Shell {
class Formatter final : public AST::NodeVisitor {
public:
Formatter(StringView source, ssize_t cursor = -1)
Formatter(StringView source, ssize_t cursor = -1, bool parse_as_posix = false)
: m_builders({ StringBuilder { round_up_to_power_of_two(source.length(), 16) } })
, m_source(source)
, m_cursor(cursor)
, m_parse_as_posix(parse_as_posix)
{
if (m_source.is_empty())
return;
@ -124,6 +125,8 @@ private:
StringView m_trivia;
Vector<DeprecatedString> m_heredocs_to_append_after_sequence;
bool m_parse_as_posix { false };
};
}

View File

@ -453,6 +453,161 @@ RefPtr<AST::Node> Shell::immediate_join(AST::ImmediateExpression& invoking_node,
return AST::make_ref_counted<AST::StringLiteral>(invoking_node.position(), builder.to_deprecated_string(), AST::StringLiteral::EnclosureType::None);
}
RefPtr<AST::Node> Shell::immediate_value_or_default(AST::ImmediateExpression& invoking_node, NonnullRefPtrVector<AST::Node> const& arguments)
{
if (arguments.size() != 2) {
raise_error(ShellError::EvaluatedSyntaxError, "Expected exactly 2 arguments to value_or_default", invoking_node.position());
return nullptr;
}
auto name = const_cast<AST::Node&>(arguments.first()).run(*this)->resolve_as_string(*this);
if (!local_variable_or(name, ""sv).is_empty())
return make_ref_counted<AST::SimpleVariable>(invoking_node.position(), name);
return arguments.last();
}
RefPtr<AST::Node> Shell::immediate_assign_default(AST::ImmediateExpression& invoking_node, NonnullRefPtrVector<AST::Node> const& arguments)
{
if (arguments.size() != 2) {
raise_error(ShellError::EvaluatedSyntaxError, "Expected exactly 2 arguments to assign_default", invoking_node.position());
return nullptr;
}
auto name = const_cast<AST::Node&>(arguments.first()).run(*this)->resolve_as_string(*this);
if (!local_variable_or(name, ""sv).is_empty())
return make_ref_counted<AST::SimpleVariable>(invoking_node.position(), name);
auto value = const_cast<AST::Node&>(arguments.last()).run(*this)->resolve_without_cast(*this);
set_local_variable(name, value);
return make_ref_counted<AST::SyntheticNode>(invoking_node.position(), value);
}
RefPtr<AST::Node> Shell::immediate_error_if_empty(AST::ImmediateExpression& invoking_node, NonnullRefPtrVector<AST::Node> const& arguments)
{
if (arguments.size() != 2) {
raise_error(ShellError::EvaluatedSyntaxError, "Expected exactly 2 arguments to error_if_empty", invoking_node.position());
return nullptr;
}
auto name = const_cast<AST::Node&>(arguments.first()).run(*this)->resolve_as_string(*this);
if (!local_variable_or(name, ""sv).is_empty())
return make_ref_counted<AST::SimpleVariable>(invoking_node.position(), name);
auto error_value = const_cast<AST::Node&>(arguments.last()).run(*this)->resolve_as_string(*this);
if (error_value.is_empty())
error_value = DeprecatedString::formatted("Expected {} to be non-empty", name);
raise_error(ShellError::EvaluatedSyntaxError, error_value, invoking_node.position());
return nullptr;
}
RefPtr<AST::Node> Shell::immediate_null_or_alternative(AST::ImmediateExpression& invoking_node, NonnullRefPtrVector<AST::Node> const& arguments)
{
if (arguments.size() != 2) {
raise_error(ShellError::EvaluatedSyntaxError, "Expected exactly 2 arguments to null_or_alternative", invoking_node.position());
return nullptr;
}
auto value = const_cast<AST::Node&>(arguments.first()).run(*this)->resolve_without_cast(*this);
if ((value->is_string() && value->resolve_as_string(*this).is_empty()) || (value->is_list() && value->resolve_as_list(*this).is_empty()))
return make_ref_counted<AST::SyntheticNode>(invoking_node.position(), value);
return arguments.last();
}
RefPtr<AST::Node> Shell::immediate_defined_value_or_default(AST::ImmediateExpression& invoking_node, NonnullRefPtrVector<AST::Node> const& arguments)
{
if (arguments.size() != 2) {
raise_error(ShellError::EvaluatedSyntaxError, "Expected exactly 2 arguments to defined_value_or_default", invoking_node.position());
return nullptr;
}
auto name = const_cast<AST::Node&>(arguments.first()).run(*this)->resolve_as_string(*this);
if (!find_frame_containing_local_variable(name))
return arguments.last();
return make_ref_counted<AST::SimpleVariable>(invoking_node.position(), name);
}
RefPtr<AST::Node> Shell::immediate_assign_defined_default(AST::ImmediateExpression& invoking_node, NonnullRefPtrVector<AST::Node> const& arguments)
{
if (arguments.size() != 2) {
raise_error(ShellError::EvaluatedSyntaxError, "Expected exactly 2 arguments to assign_defined_default", invoking_node.position());
return nullptr;
}
auto name = const_cast<AST::Node&>(arguments.first()).run(*this)->resolve_as_string(*this);
if (find_frame_containing_local_variable(name))
return make_ref_counted<AST::SimpleVariable>(invoking_node.position(), name);
auto value = const_cast<AST::Node&>(arguments.last()).run(*this)->resolve_without_cast(*this);
set_local_variable(name, value);
return make_ref_counted<AST::SyntheticNode>(invoking_node.position(), value);
}
RefPtr<AST::Node> Shell::immediate_error_if_unset(AST::ImmediateExpression& invoking_node, NonnullRefPtrVector<AST::Node> const& arguments)
{
if (arguments.size() != 2) {
raise_error(ShellError::EvaluatedSyntaxError, "Expected exactly 2 arguments to error_if_unset", invoking_node.position());
return nullptr;
}
auto name = const_cast<AST::Node&>(arguments.first()).run(*this)->resolve_as_string(*this);
if (find_frame_containing_local_variable(name))
return make_ref_counted<AST::SimpleVariable>(invoking_node.position(), name);
auto error_value = const_cast<AST::Node&>(arguments.last()).run(*this)->resolve_as_string(*this);
if (error_value.is_empty())
error_value = DeprecatedString::formatted("Expected {} to be set", name);
raise_error(ShellError::EvaluatedSyntaxError, error_value, invoking_node.position());
return nullptr;
}
RefPtr<AST::Node> Shell::immediate_null_if_unset_or_alternative(AST::ImmediateExpression& invoking_node, NonnullRefPtrVector<AST::Node> const& arguments)
{
if (arguments.size() != 2) {
raise_error(ShellError::EvaluatedSyntaxError, "Expected exactly 2 arguments to null_if_unset_or_alternative", invoking_node.position());
return nullptr;
}
auto name = const_cast<AST::Node&>(arguments.first()).run(*this)->resolve_as_string(*this);
if (!find_frame_containing_local_variable(name))
return arguments.last();
return make_ref_counted<AST::SimpleVariable>(invoking_node.position(), name);
}
RefPtr<AST::Node> Shell::immediate_reexpand(AST::ImmediateExpression& invoking_node, NonnullRefPtrVector<AST::Node> const& arguments)
{
if (arguments.size() != 1) {
raise_error(ShellError::EvaluatedSyntaxError, "Expected exactly 1 argument to reexpand", invoking_node.position());
return nullptr;
}
auto value = const_cast<AST::Node&>(arguments.first()).run(*this)->resolve_as_string(*this);
return parse(value, m_is_interactive, false);
}
RefPtr<AST::Node> Shell::immediate_length_of_variable(AST::ImmediateExpression& invoking_node, NonnullRefPtrVector<AST::Node> const& arguments)
{
if (arguments.size() != 1) {
raise_error(ShellError::EvaluatedSyntaxError, "Expected exactly 1 argument to length_of_variable", invoking_node.position());
return nullptr;
}
auto name = const_cast<AST::Node&>(arguments.first()).run(*this)->resolve_as_string(*this);
auto variable = make_ref_counted<AST::SimpleVariable>(invoking_node.position(), name);
return immediate_length_impl(
invoking_node,
{ move(variable) },
false);
}
RefPtr<AST::Node> Shell::run_immediate_function(StringView str, AST::ImmediateExpression& invoking_node, NonnullRefPtrVector<AST::Node> const& arguments)
{
#define __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(name) \

View File

@ -0,0 +1,725 @@
/*
* Copyright (c) 2022, Ali Mohammad Pur <mpfard@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/CharacterTypes.h>
#include <Shell/PosixLexer.h>
static bool is_operator(StringView text)
{
return Shell::Posix::Token::operator_from_name(text).has_value();
}
static bool is_part_of_operator(StringView text, char ch)
{
StringBuilder builder;
builder.append(text);
builder.append(ch);
return Shell::Posix::Token::operator_from_name(builder.string_view()).has_value();
}
namespace Shell::Posix {
Vector<Token> Lexer::batch_next()
{
for (; m_next_reduction != Reduction::None;) {
auto result = reduce(m_next_reduction);
m_next_reduction = result.next_reduction;
if (!result.tokens.is_empty())
return result.tokens;
}
return {};
}
ExpansionRange Lexer::range(ssize_t offset) const
{
return {
m_state.position.end_offset - m_state.position.start_offset + offset - 1,
0,
};
}
char Lexer::consume()
{
auto ch = m_lexer.consume();
if (ch == '\n') {
m_state.position.end_line.line_number++;
m_state.position.end_line.line_column = 0;
}
m_state.position.end_offset++;
return ch;
}
bool Lexer::consume_specific(char ch)
{
if (m_lexer.peek() == ch) {
consume();
return true;
}
return false;
}
Lexer::ReductionResult Lexer::reduce(Reduction reduction)
{
switch (reduction) {
case Reduction::None:
return { {}, Reduction::None };
case Reduction::End:
return reduce_end();
case Reduction::Operator:
return reduce_operator();
case Reduction::Comment:
return reduce_comment();
case Reduction::SingleQuotedString:
return reduce_single_quoted_string();
case Reduction::DoubleQuotedString:
return reduce_double_quoted_string();
case Reduction::Expansion:
return reduce_expansion();
case Reduction::CommandExpansion:
return reduce_command_expansion();
case Reduction::Start:
return reduce_start();
case Reduction::ArithmeticExpansion:
return reduce_arithmetic_expansion();
case Reduction::SpecialParameterExpansion:
return reduce_special_parameter_expansion();
case Reduction::ParameterExpansion:
return reduce_parameter_expansion();
case Reduction::CommandOrArithmeticSubstitutionExpansion:
return reduce_command_or_arithmetic_substitution_expansion();
case Reduction::ExtendedParameterExpansion:
return reduce_extended_parameter_expansion();
}
VERIFY_NOT_REACHED();
}
Lexer::ReductionResult Lexer::reduce_end()
{
return {
.tokens = { Token::eof() },
.next_reduction = Reduction::None,
};
}
Lexer::ReductionResult Lexer::reduce_operator()
{
if (m_lexer.is_eof()) {
if (is_operator(m_state.buffer.string_view())) {
auto tokens = Token::operators_from(m_state);
m_state.buffer.clear();
m_state.position.start_offset = m_state.position.end_offset;
m_state.position.start_line = m_state.position.end_line;
return {
.tokens = move(tokens),
.next_reduction = Reduction::End,
};
}
return reduce(Reduction::Start);
}
if (is_part_of_operator(m_state.buffer.string_view(), m_lexer.peek())) {
m_state.buffer.append(consume());
return {
.tokens = {},
.next_reduction = Reduction::Operator,
};
}
auto tokens = Vector<Token> {};
if (is_operator(m_state.buffer.string_view())) {
tokens.extend(Token::operators_from(m_state));
m_state.buffer.clear();
m_state.position.start_offset = m_state.position.end_offset;
m_state.position.start_line = m_state.position.end_line;
}
auto result = reduce(Reduction::Start);
tokens.extend(move(result.tokens));
return {
.tokens = move(tokens),
.next_reduction = result.next_reduction,
};
}
Lexer::ReductionResult Lexer::reduce_comment()
{
if (m_lexer.is_eof()) {
return {
.tokens = {},
.next_reduction = Reduction::End,
};
}
if (consume() == '\n') {
return {
.tokens = { Token::newline() },
.next_reduction = Reduction::Start,
};
}
return {
.tokens = {},
.next_reduction = Reduction::Comment,
};
}
Lexer::ReductionResult Lexer::reduce_single_quoted_string()
{
if (m_lexer.is_eof()) {
auto tokens = Token::maybe_from_state(m_state);
tokens.append(Token::continuation('\''));
return {
.tokens = move(tokens),
.next_reduction = Reduction::End,
};
}
auto ch = consume();
m_state.buffer.append(ch);
if (ch == '\'') {
return {
.tokens = {},
.next_reduction = Reduction::Start,
};
}
return {
.tokens = {},
.next_reduction = Reduction::SingleQuotedString,
};
}
Lexer::ReductionResult Lexer::reduce_double_quoted_string()
{
m_state.previous_reduction = Reduction::DoubleQuotedString;
if (m_lexer.is_eof()) {
auto tokens = Token::maybe_from_state(m_state);
tokens.append(Token::continuation('"'));
return {
.tokens = move(tokens),
.next_reduction = Reduction::End,
};
}
auto ch = consume();
m_state.buffer.append(ch);
if (m_state.escaping) {
m_state.escaping = false;
return {
.tokens = {},
.next_reduction = Reduction::DoubleQuotedString,
};
}
switch (ch) {
case '\\':
m_state.escaping = true;
return {
.tokens = {},
.next_reduction = Reduction::DoubleQuotedString,
};
case '"':
m_state.previous_reduction = Reduction::Start;
return {
.tokens = {},
.next_reduction = Reduction::Start,
};
case '$':
if (m_lexer.next_is("("))
m_state.expansions.empend(CommandExpansion { .command = StringBuilder {}, .range = range() });
else
m_state.expansions.empend(ParameterExpansion { .parameter = StringBuilder {}, .range = range() });
return {
.tokens = {},
.next_reduction = Reduction::Expansion,
};
case '`':
m_state.expansions.empend(CommandExpansion { .command = StringBuilder {}, .range = range() });
return {
.tokens = {},
.next_reduction = Reduction::CommandExpansion,
};
default:
return {
.tokens = {},
.next_reduction = Reduction::DoubleQuotedString,
};
}
}
Lexer::ReductionResult Lexer::reduce_expansion()
{
if (m_lexer.is_eof())
return reduce(m_state.previous_reduction);
auto ch = m_lexer.peek();
switch (ch) {
case '{':
consume();
m_state.buffer.append(ch);
return {
.tokens = {},
.next_reduction = Reduction::ExtendedParameterExpansion,
};
case '(':
consume();
m_state.buffer.append(ch);
return {
.tokens = {},
.next_reduction = Reduction::CommandOrArithmeticSubstitutionExpansion,
};
case 'a' ... 'z':
case 'A' ... 'Z':
case '_': {
consume();
m_state.buffer.append(ch);
auto& expansion = m_state.expansions.last().get<ParameterExpansion>();
expansion.parameter.append(ch);
expansion.range.length = m_state.position.end_offset - expansion.range.start - m_state.position.start_offset;
return {
.tokens = {},
.next_reduction = Reduction::ParameterExpansion,
};
}
case '0' ... '9':
case '-':
case '!':
case '@':
case '#':
case '?':
case '*':
case '$':
return reduce(Reduction::SpecialParameterExpansion);
default:
m_state.buffer.append(ch);
return reduce(m_state.previous_reduction);
}
}
Lexer::ReductionResult Lexer::reduce_command_expansion()
{
if (m_lexer.is_eof()) {
auto& expansion = m_state.expansions.last().get<CommandExpansion>();
expansion.range.length = m_state.position.end_offset - expansion.range.start - m_state.position.start_offset;
return {
.tokens = { Token::continuation('`') },
.next_reduction = m_state.previous_reduction,
};
}
auto ch = consume();
if (!m_state.escaping && ch == '`') {
m_state.buffer.append(ch);
auto& expansion = m_state.expansions.last().get<CommandExpansion>();
expansion.range.length = m_state.position.end_offset - expansion.range.start - m_state.position.start_offset;
return {
.tokens = {},
.next_reduction = m_state.previous_reduction,
};
}
if (!m_state.escaping && ch == '\\') {
m_state.escaping = true;
return {
.tokens = {},
.next_reduction = Reduction::CommandExpansion,
};
}
m_state.escaping = false;
m_state.buffer.append(ch);
m_state.expansions.last().get<CommandExpansion>().command.append(ch);
return {
.tokens = {},
.next_reduction = Reduction::CommandExpansion,
};
}
Lexer::ReductionResult Lexer::reduce_start()
{
if (m_lexer.is_eof()) {
auto tokens = Token::maybe_from_state(m_state);
m_state.buffer.clear();
m_state.position.start_offset = m_state.position.end_offset;
m_state.position.start_line = m_state.position.end_line;
return {
.tokens = move(tokens),
.next_reduction = Reduction::End,
};
}
if (m_state.escaping && consume_specific('\n')) {
m_state.escaping = false;
auto buffer = m_state.buffer.to_deprecated_string().substring(0, m_state.buffer.length() - 1);
m_state.buffer.clear();
m_state.buffer.append(buffer);
return {
.tokens = {},
.next_reduction = Reduction::Start,
};
}
if (!m_state.escaping && m_lexer.peek() == '#' && m_state.buffer.is_empty()) {
consume();
return {
.tokens = {},
.next_reduction = Reduction::Comment,
};
}
if (!m_state.escaping && consume_specific('\n')) {
auto tokens = Token::maybe_from_state(m_state);
tokens.append(Token::newline());
m_state.buffer.clear();
m_state.position.start_offset = m_state.position.end_offset;
m_state.position.start_line = m_state.position.end_line;
return {
.tokens = move(tokens),
.next_reduction = Reduction::Start,
};
}
if (!m_state.escaping && consume_specific('\\')) {
m_state.escaping = true;
m_state.buffer.append('\\');
return {
.tokens = {},
.next_reduction = Reduction::Start,
};
}
if (!m_state.escaping && is_part_of_operator(""sv, m_lexer.peek())) {
auto tokens = Token::maybe_from_state(m_state);
m_state.buffer.clear();
m_state.buffer.append(consume());
m_state.position.start_offset = m_state.position.end_offset;
m_state.position.start_line = m_state.position.end_line;
return {
.tokens = move(tokens),
.next_reduction = Reduction::Operator,
};
}
if (!m_state.escaping && consume_specific('\'')) {
m_state.buffer.append('\'');
return {
.tokens = {},
.next_reduction = Reduction::SingleQuotedString,
};
}
if (!m_state.escaping && consume_specific('"')) {
m_state.buffer.append('"');
return {
.tokens = {},
.next_reduction = Reduction::DoubleQuotedString,
};
}
if (!m_state.escaping && is_ascii_space(m_lexer.peek())) {
consume();
auto tokens = Token::maybe_from_state(m_state);
m_state.buffer.clear();
m_state.expansions.clear();
m_state.position.start_offset = m_state.position.end_offset;
m_state.position.start_line = m_state.position.end_line;
return {
.tokens = move(tokens),
.next_reduction = Reduction::Start,
};
}
if (!m_state.escaping && consume_specific('$')) {
m_state.buffer.append('$');
if (m_lexer.next_is("("))
m_state.expansions.empend(CommandExpansion { .command = StringBuilder {}, .range = range() });
else
m_state.expansions.empend(ParameterExpansion { .parameter = StringBuilder {}, .range = range() });
return {
.tokens = {},
.next_reduction = Reduction::Expansion,
};
}
if (!m_state.escaping && consume_specific('`')) {
m_state.buffer.append('`');
m_state.expansions.empend(CommandExpansion { .command = StringBuilder {}, .range = range() });
return {
.tokens = {},
.next_reduction = Reduction::CommandExpansion,
};
}
m_state.escaping = false;
m_state.buffer.append(consume());
return {
.tokens = {},
.next_reduction = Reduction::Start,
};
}
Lexer::ReductionResult Lexer::reduce_arithmetic_expansion()
{
if (m_lexer.is_eof()) {
auto& expansion = m_state.expansions.last().get<ArithmeticExpansion>();
expansion.range.length = m_state.position.end_offset - expansion.range.start - m_state.position.start_offset;
return {
.tokens = { Token::continuation("$((") },
.next_reduction = m_state.previous_reduction,
};
}
if (m_lexer.peek() == ')' && m_state.buffer.string_view().ends_with(')')) {
m_state.buffer.append(consume());
auto& expansion = m_state.expansions.last().get<ArithmeticExpansion>();
expansion.expression = expansion.value.to_deprecated_string().substring(0, expansion.value.length() - 1);
expansion.value.clear();
expansion.range.length = m_state.position.end_offset - expansion.range.start - m_state.position.start_offset;
return {
.tokens = {},
.next_reduction = m_state.previous_reduction,
};
}
auto ch = consume();
m_state.buffer.append(ch);
m_state.expansions.last().get<ArithmeticExpansion>().value.append(ch);
return {
.tokens = {},
.next_reduction = Reduction::ArithmeticExpansion,
};
}
Lexer::ReductionResult Lexer::reduce_special_parameter_expansion()
{
auto ch = consume();
m_state.buffer.append(ch);
m_state.expansions.last() = ParameterExpansion {
.parameter = StringBuilder {},
.range = range(-1),
};
m_state.expansions.last().get<ParameterExpansion>().parameter.append(ch);
return {
.tokens = {},
.next_reduction = m_state.previous_reduction,
};
}
Lexer::ReductionResult Lexer::reduce_parameter_expansion()
{
auto& expansion = m_state.expansions.last().get<ParameterExpansion>();
if (m_lexer.is_eof()) {
return {
.tokens = {},
.next_reduction = Reduction::Start,
};
}
auto next = m_lexer.peek();
if (is_ascii_alphanumeric(next)) {
m_state.buffer.append(consume());
expansion.parameter.append(next);
expansion.range.length = m_state.position.end_offset - expansion.range.start - m_state.position.start_offset;
return {
.tokens = {},
.next_reduction = Reduction::ParameterExpansion,
};
}
return reduce(m_state.previous_reduction);
}
Lexer::ReductionResult Lexer::reduce_command_or_arithmetic_substitution_expansion()
{
if (m_lexer.is_eof()) {
return {
.tokens = { Token::continuation("$(") },
.next_reduction = m_state.previous_reduction,
};
}
auto ch = m_lexer.peek();
if (ch == '(' && m_state.buffer.string_view().ends_with("$("sv)) {
m_state.buffer.append(consume());
m_state.expansions.last() = ArithmeticExpansion {
.expression = "",
.value = StringBuilder {},
.range = range(-2)
};
return {
.tokens = {},
.next_reduction = Reduction::ArithmeticExpansion,
};
}
if (ch == ')') {
m_state.buffer.append(consume());
m_state.expansions.last().visit([&](auto& expansion) {
expansion.range.length = m_state.position.end_offset - expansion.range.start - m_state.position.start_offset;
});
return {
.tokens = {},
.next_reduction = m_state.previous_reduction,
};
}
m_state.buffer.append(consume());
m_state.expansions.last().get<CommandExpansion>().command.append(ch);
return {
.tokens = {},
.next_reduction = Reduction::CommandOrArithmeticSubstitutionExpansion,
};
}
Lexer::ReductionResult Lexer::reduce_extended_parameter_expansion()
{
auto& expansion = m_state.expansions.last().get<ParameterExpansion>();
if (m_lexer.is_eof()) {
return {
.tokens = { Token::continuation("${") },
.next_reduction = m_state.previous_reduction,
};
}
auto ch = m_lexer.peek();
if (ch == '}') {
m_state.buffer.append(consume());
expansion.range.length = m_state.position.end_offset - expansion.range.start - m_state.position.start_offset;
return {
.tokens = {},
.next_reduction = m_state.previous_reduction,
};
}
m_state.buffer.append(consume());
expansion.parameter.append(ch);
expansion.range.length = m_state.position.end_offset - expansion.range.start - m_state.position.start_offset;
return {
.tokens = {},
.next_reduction = Reduction::ExtendedParameterExpansion,
};
}
StringView Token::type_name() const
{
switch (type) {
case Type::Eof:
return "Eof"sv;
case Type::Newline:
return "Newline"sv;
case Type::Continuation:
return "Continuation"sv;
case Type::Token:
return "Token"sv;
case Type::And:
return "And"sv;
case Type::Pipe:
return "Pipe"sv;
case Type::OpenParen:
return "OpenParen"sv;
case Type::CloseParen:
return "CloseParen"sv;
case Type::Great:
return "Great"sv;
case Type::Less:
return "Less"sv;
case Type::AndIf:
return "AndIf"sv;
case Type::OrIf:
return "OrIf"sv;
case Type::DoubleSemicolon:
return "DoubleSemicolon"sv;
case Type::DoubleLess:
return "DoubleLess"sv;
case Type::DoubleGreat:
return "DoubleGreat"sv;
case Type::LessAnd:
return "LessAnd"sv;
case Type::GreatAnd:
return "GreatAnd"sv;
case Type::LessGreat:
return "LessGreat"sv;
case Type::DoubleLessDash:
return "DoubleLessDash"sv;
case Type::Clobber:
return "Clobber"sv;
case Type::Semicolon:
return "Semicolon"sv;
case Type::AssignmentWord:
return "AssignmentWord"sv;
case Type::Bang:
return "Bang"sv;
case Type::Case:
return "Case"sv;
case Type::CloseBrace:
return "CloseBrace"sv;
case Type::Do:
return "Do"sv;
case Type::Done:
return "Done"sv;
case Type::Elif:
return "Elif"sv;
case Type::Else:
return "Else"sv;
case Type::Esac:
return "Esac"sv;
case Type::Fi:
return "Fi"sv;
case Type::For:
return "For"sv;
case Type::If:
return "If"sv;
case Type::In:
return "In"sv;
case Type::IoNumber:
return "IoNumber"sv;
case Type::OpenBrace:
return "OpenBrace"sv;
case Type::Then:
return "Then"sv;
case Type::Until:
return "Until"sv;
case Type::VariableName:
return "VariableName"sv;
case Type::While:
return "While"sv;
case Type::Word:
return "Word"sv;
}
return "Idk"sv;
}
}

413
Userland/Shell/PosixLexer.h Normal file
View File

@ -0,0 +1,413 @@
/*
* Copyright (c) 2022, Ali Mohammad Pur <mpfard@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/DeprecatedString.h>
#include <AK/GenericLexer.h>
#include <AK/Variant.h>
#include <AK/Vector.h>
#include <Shell/AST.h>
namespace Shell::Posix {
enum class Reduction {
None,
End,
Operator,
Comment,
SingleQuotedString,
DoubleQuotedString,
Expansion,
CommandExpansion,
Start,
ArithmeticExpansion,
SpecialParameterExpansion,
ParameterExpansion,
CommandOrArithmeticSubstitutionExpansion,
ExtendedParameterExpansion,
};
struct ExpansionRange {
size_t start;
size_t length;
};
struct ParameterExpansion {
StringBuilder parameter;
ExpansionRange range;
};
struct CommandExpansion {
StringBuilder command;
ExpansionRange range;
};
struct ArithmeticExpansion {
DeprecatedString expression;
StringBuilder value;
ExpansionRange range;
};
using Expansion = Variant<ParameterExpansion, CommandExpansion, ArithmeticExpansion>;
struct ResolvedParameterExpansion {
DeprecatedString parameter;
DeprecatedString argument;
ExpansionRange range;
enum class Op {
UseDefaultValue, // ${parameter:-word}
AssignDefaultValue, // ${parameter:=word}
IndicateErrorIfEmpty, // ${parameter:?word}
UseAlternativeValue, // ${parameter:+word}
UseDefaultValueIfUnset, // ${parameter-default}
AssignDefaultValueIfUnset, // ${parameter=default}
IndicateErrorIfUnset, // ${parameter?default}
UseAlternativeValueIfUnset, // ${parameter+default}
RemoveLargestSuffixByPattern, // ${parameter%%pattern}
RemoveLargestPrefixByPattern, // ${parameter##pattern}
RemoveSmallestSuffixByPattern, // ${parameter%pattern}
RemoveSmallestPrefixByPattern, // ${parameter#pattern}
StringLength, // ${#parameter}
GetPositionalParameter, // ${parameter}
GetVariable, // ${parameter}
GetLastBackgroundPid, // $!
GetPositionalParameterList, // $*
GetCurrentOptionFlags, // $-
GetPositionalParameterCount, // $#
GetLastExitStatus, // $?
GetPositionalParameterListAsString, // $@
GetShellProcessId, // $$
} op;
enum class Expand {
Nothing,
Word,
} expand { Expand::Nothing };
DeprecatedString to_deprecated_string() const
{
StringBuilder builder;
builder.append("{"sv);
switch (op) {
case Op::UseDefaultValue:
builder.append("UseDefaultValue"sv);
break;
case Op::AssignDefaultValue:
builder.append("AssignDefaultValue"sv);
break;
case Op::IndicateErrorIfEmpty:
builder.append("IndicateErrorIfEmpty"sv);
break;
case Op::UseAlternativeValue:
builder.append("UseAlternativeValue"sv);
break;
case Op::UseDefaultValueIfUnset:
builder.append("UseDefaultValueIfUnset"sv);
break;
case Op::AssignDefaultValueIfUnset:
builder.append("AssignDefaultValueIfUnset"sv);
break;
case Op::IndicateErrorIfUnset:
builder.append("IndicateErrorIfUnset"sv);
break;
case Op::UseAlternativeValueIfUnset:
builder.append("UseAlternativeValueIfUnset"sv);
break;
case Op::RemoveLargestSuffixByPattern:
builder.append("RemoveLargestSuffixByPattern"sv);
break;
case Op::RemoveLargestPrefixByPattern:
builder.append("RemoveLargestPrefixByPattern"sv);
break;
case Op::RemoveSmallestSuffixByPattern:
builder.append("RemoveSmallestSuffixByPattern"sv);
break;
case Op::RemoveSmallestPrefixByPattern:
builder.append("RemoveSmallestPrefixByPattern"sv);
break;
case Op::StringLength:
builder.append("StringLength"sv);
break;
case Op::GetPositionalParameter:
builder.append("GetPositionalParameter"sv);
break;
case Op::GetLastBackgroundPid:
builder.append("GetLastBackgroundPid"sv);
break;
case Op::GetPositionalParameterList:
builder.append("GetPositionalParameterList"sv);
break;
case Op::GetCurrentOptionFlags:
builder.append("GetCurrentOptionFlags"sv);
break;
case Op::GetPositionalParameterCount:
builder.append("GetPositionalParameterCount"sv);
break;
case Op::GetLastExitStatus:
builder.append("GetLastExitStatus"sv);
break;
case Op::GetPositionalParameterListAsString:
builder.append("GetPositionalParameterListAsString"sv);
break;
case Op::GetShellProcessId:
builder.append("GetShellProcessId"sv);
break;
case Op::GetVariable:
builder.append("GetVariable"sv);
break;
}
builder.append(" "sv);
builder.append(parameter);
builder.append(" ("sv);
builder.append(argument);
builder.append(")"sv);
builder.append("}"sv);
return builder.to_deprecated_string();
}
};
struct ResolvedCommandExpansion {
RefPtr<AST::Node> command;
ExpansionRange range;
};
using ResolvedExpansion = Variant<ResolvedParameterExpansion, ResolvedCommandExpansion>;
struct State {
StringBuilder buffer {};
Reduction previous_reduction { Reduction::Start };
bool escaping { false };
AST::Position position {
.start_offset = 0,
.end_offset = 0,
.start_line = {
.line_number = 0,
.line_column = 0,
},
.end_line = {
.line_number = 0,
.line_column = 0,
},
};
Vector<Expansion> expansions {};
};
struct Token {
enum class Type {
Eof,
Newline,
Continuation,
Token,
And,
Pipe,
OpenParen,
CloseParen,
Great,
Less,
AndIf,
OrIf,
DoubleSemicolon,
DoubleLess,
DoubleGreat,
LessAnd,
GreatAnd,
LessGreat,
DoubleLessDash,
Clobber,
Semicolon,
// Not produced by this lexer, but generated in later stages.
AssignmentWord,
Bang,
Case,
CloseBrace,
Do,
Done,
Elif,
Else,
Esac,
Fi,
For,
If,
In,
IoNumber,
OpenBrace,
Then,
Until,
VariableName,
While,
Word,
};
Type type;
DeprecatedString value;
Optional<AST::Position> position;
Vector<Expansion> expansions;
Vector<ResolvedExpansion> resolved_expansions {};
StringView original_text;
bool could_be_start_of_a_simple_command { false };
static Vector<Token> maybe_from_state(State const& state)
{
if (state.buffer.is_empty() || state.buffer.string_view().trim_whitespace().is_empty())
return {};
auto token = Token {
.type = Type::Token,
.value = state.buffer.to_deprecated_string(),
.position = state.position,
.expansions = state.expansions,
.original_text = {},
};
return { move(token) };
}
static Optional<Token::Type> operator_from_name(StringView name)
{
if (name == "&&"sv)
return Token::Type::AndIf;
if (name == "||"sv)
return Token::Type::OrIf;
if (name == ";;"sv)
return Token::Type::DoubleSemicolon;
if (name == "<<"sv)
return Token::Type::DoubleLess;
if (name == ">>"sv)
return Token::Type::DoubleGreat;
if (name == "<&"sv)
return Token::Type::LessAnd;
if (name == ">&"sv)
return Token::Type::GreatAnd;
if (name == "<>"sv)
return Token::Type::LessGreat;
if (name == "<<-"sv)
return Token::Type::DoubleLessDash;
if (name == ">|"sv)
return Token::Type::Clobber;
if (name == ";"sv)
return Token::Type::Semicolon;
if (name == "&"sv)
return Token::Type::And;
if (name == "|"sv)
return Token::Type::Pipe;
if (name == "("sv)
return Token::Type::OpenParen;
if (name == ")"sv)
return Token::Type::CloseParen;
if (name == ">"sv)
return Token::Type::Great;
if (name == "<"sv)
return Token::Type::Less;
return {};
}
static Vector<Token> operators_from(State const& state)
{
auto name = state.buffer.string_view();
auto type = operator_from_name(name);
if (!type.has_value())
return {};
return {
Token {
.type = *type,
.value = name,
.position = state.position,
.expansions = {},
.original_text = {},
}
};
}
static Token eof()
{
return {
.type = Type::Eof,
.value = {},
.position = {},
.expansions = {},
.original_text = {},
};
}
static Token newline()
{
return {
.type = Type::Newline,
.value = "\n",
.position = {},
.expansions = {},
.original_text = {},
};
}
static Token continuation(char expected)
{
return {
.type = Type::Continuation,
.value = DeprecatedString::formatted("{:c}", expected),
.position = {},
.expansions = {},
.original_text = {},
};
}
static Token continuation(DeprecatedString expected)
{
return {
.type = Type::Continuation,
.value = move(expected),
.position = {},
.expansions = {},
.original_text = {},
};
}
StringView type_name() const;
};
class Lexer {
public:
explicit Lexer(StringView input)
: m_lexer(input)
{
}
Vector<Token> batch_next();
private:
struct ReductionResult {
Vector<Token> tokens;
Reduction next_reduction { Reduction::None };
};
ReductionResult reduce(Reduction);
ReductionResult reduce_end();
ReductionResult reduce_operator();
ReductionResult reduce_comment();
ReductionResult reduce_single_quoted_string();
ReductionResult reduce_double_quoted_string();
ReductionResult reduce_expansion();
ReductionResult reduce_command_expansion();
ReductionResult reduce_start();
ReductionResult reduce_arithmetic_expansion();
ReductionResult reduce_special_parameter_expansion();
ReductionResult reduce_parameter_expansion();
ReductionResult reduce_command_or_arithmetic_substitution_expansion();
ReductionResult reduce_extended_parameter_expansion();
char consume();
bool consume_specific(char);
ExpansionRange range(ssize_t offset = 0) const;
GenericLexer m_lexer;
State m_state;
Reduction m_next_reduction { Reduction::Start };
};
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,117 @@
/*
* Copyright (c) 2022, Ali Mohammad Pur <mpfard@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <Shell/AST.h>
#include <Shell/PosixLexer.h>
namespace Shell::Posix {
class Parser {
public:
Parser(StringView input, bool interactive = false)
: m_lexer(input)
, m_in_interactive_mode(interactive)
, m_eof_token(Token::eof())
{
fill_token_buffer();
}
RefPtr<AST::Node> parse();
RefPtr<AST::Node> parse_word_list();
struct Error {
DeprecatedString message;
Optional<AST::Position> position;
};
auto& errors() const { return m_errors; }
private:
Optional<Token> next_expanded_token();
Vector<Token> perform_expansions(Vector<Token> tokens);
void fill_token_buffer();
Token const& peek() const
{
if (eof())
return m_eof_token;
return m_token_buffer[m_token_index];
}
Token const& consume()
{
if (eof())
return m_eof_token;
return m_token_buffer[m_token_index++];
}
void skip()
{
if (eof())
return;
m_token_index++;
}
bool eof() const
{
return m_token_index == m_token_buffer.size() || m_token_buffer[m_token_index].type == Token::Type::Eof;
}
struct CaseItemsResult {
Vector<AST::Position> pipe_positions;
NonnullRefPtrVector<AST::Node> nodes;
};
RefPtr<AST::Node> parse_complete_command();
RefPtr<AST::Node> parse_list();
RefPtr<AST::Node> parse_and_or();
RefPtr<AST::Node> parse_pipeline();
RefPtr<AST::Node> parse_pipe_sequence();
RefPtr<AST::Node> parse_command();
RefPtr<AST::Node> parse_compound_command();
RefPtr<AST::Node> parse_subshell();
RefPtr<AST::Node> parse_compound_list();
RefPtr<AST::Node> parse_term();
RefPtr<AST::Node> parse_for_clause();
RefPtr<AST::Node> parse_case_clause();
CaseItemsResult parse_case_list();
RefPtr<AST::Node> parse_if_clause();
RefPtr<AST::Node> parse_while_clause();
RefPtr<AST::Node> parse_until_clause();
RefPtr<AST::Node> parse_function_definition();
RefPtr<AST::Node> parse_function_body();
RefPtr<AST::Node> parse_brace_group();
RefPtr<AST::Node> parse_do_group();
RefPtr<AST::Node> parse_simple_command();
RefPtr<AST::Node> parse_prefix();
RefPtr<AST::Node> parse_suffix();
RefPtr<AST::Node> parse_io_redirect();
RefPtr<AST::Node> parse_redirect_list();
RefPtr<AST::Node> parse_io_file(AST::Position, Optional<int> fd);
RefPtr<AST::Node> parse_io_here(AST::Position, Optional<int> fd);
RefPtr<AST::Node> parse_word();
template<typename... Ts>
void error(Token const& token, CheckedFormatString<Ts...> fmt, Ts&&... args)
{
m_errors.append(Error {
DeprecatedString::formatted(fmt.view(), forward<Ts>(args)...),
token.position,
});
}
Lexer m_lexer;
bool m_in_interactive_mode { false };
Vector<Token, 2> m_token_buffer;
size_t m_token_index { 0 };
Vector<Token> m_previous_token_buffer;
Vector<Error> m_errors;
Token m_eof_token;
bool m_disallow_command_prefix { true };
};
}

View File

@ -26,6 +26,7 @@
#include <LibCore/System.h>
#include <LibCore/Timer.h>
#include <LibLine/Editor.h>
#include <Shell/PosixParser.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
@ -297,7 +298,7 @@ Vector<AST::Command> Shell::expand_aliases(Vector<AST::Command> initial_commands
auto alias = resolve_alias(command.argv[0]);
if (!alias.is_null()) {
auto argv0 = command.argv.take_first();
auto subcommand_ast = Parser { alias }.parse();
auto subcommand_ast = parse(alias, false);
if (subcommand_ast) {
while (subcommand_ast->is_execute()) {
auto* ast = static_cast<AST::Execute*>(subcommand_ast.ptr());
@ -477,7 +478,7 @@ bool Shell::invoke_function(const AST::Command& command, int& retval)
DeprecatedString Shell::format(StringView source, ssize_t& cursor) const
{
Formatter formatter(source, cursor);
Formatter formatter(source, cursor, m_in_posix_mode);
auto result = formatter.format();
cursor = formatter.cursor();
@ -580,7 +581,7 @@ int Shell::run_command(StringView cmd, Optional<SourcePosition> source_position_
if (cmd.is_empty())
return 0;
auto command = Parser(cmd, m_is_interactive).parse();
auto command = parse(cmd, m_is_interactive);
if (!command)
return 0;
@ -1410,8 +1411,7 @@ void Shell::remove_entry_from_cache(StringView entry)
void Shell::highlight(Line::Editor& editor) const
{
auto line = editor.line();
Parser parser(line, m_is_interactive);
auto ast = parser.parse();
auto ast = parse(line, m_is_interactive);
if (!ast)
return;
ast->highlight_in_editor(editor, const_cast<Shell&>(*this));
@ -1425,9 +1425,7 @@ Vector<Line::CompletionSuggestion> Shell::complete()
Vector<Line::CompletionSuggestion> Shell::complete(StringView line)
{
Parser parser(line, m_is_interactive);
auto ast = parser.parse();
auto ast = parse(line, m_is_interactive);
if (!ast)
return {};
@ -2177,8 +2175,9 @@ Shell::Shell()
cache_path();
}
Shell::Shell(Line::Editor& editor, bool attempt_interactive)
: m_editor(editor)
Shell::Shell(Line::Editor& editor, bool attempt_interactive, bool posix_mode)
: m_in_posix_mode(posix_mode)
, m_editor(editor)
{
uid = getuid();
tcsetpgrp(0, getpgrp());
@ -2224,8 +2223,8 @@ Shell::Shell(Line::Editor& editor, bool attempt_interactive)
cache_path();
}
m_editor->register_key_input_callback('\n', [](Line::Editor& editor) {
auto ast = Parser(editor.line()).parse();
m_editor->register_key_input_callback('\n', [this](Line::Editor& editor) {
auto ast = parse(editor.line(), false);
if (ast && ast->is_syntax_error() && ast->syntax_error_node().is_continuable())
return true;
@ -2484,6 +2483,32 @@ void Shell::timer_event(Core::TimerEvent& event)
m_editor->save_history(get_history_path());
}
RefPtr<AST::Node> Shell::parse(StringView input, bool interactive, bool as_command) const
{
if (m_in_posix_mode) {
Posix::Parser parser(input);
if (as_command) {
auto node = parser.parse();
if constexpr (SHELL_POSIX_PARSER_DEBUG) {
dbgln("Parsed with the POSIX Parser:");
node->dump(0);
}
return node;
}
return parser.parse_word_list();
}
Parser parser { input, interactive };
if (as_command)
return parser.parse();
auto nodes = parser.parse_as_multiple_expressions();
return make_ref_counted<AST::ListConcatenate>(
nodes.is_empty() ? AST::Position { 0, 0, { 0, 0 }, { 0, 0 } } : nodes.first().position(),
move(nodes));
}
void FileDescriptionCollector::collect()
{
for (auto fd : m_fds)

View File

@ -61,16 +61,26 @@
__ENUMERATE_SHELL_OPTION(verbose, false, "Announce every command that is about to be executed") \
__ENUMERATE_SHELL_OPTION(invoke_program_for_autocomplete, false, "Attempt to use the program being completed itself for autocompletion via --complete")
#define ENUMERATE_SHELL_IMMEDIATE_FUNCTIONS() \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(concat_lists) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(length) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(length_across) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(remove_suffix) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(remove_prefix) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(regex_replace) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(filter_glob) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(split) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(join)
#define ENUMERATE_SHELL_IMMEDIATE_FUNCTIONS() \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(concat_lists) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(length) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(length_across) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(remove_suffix) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(remove_prefix) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(regex_replace) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(filter_glob) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(split) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(join) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(value_or_default) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(assign_default) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(error_if_empty) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(null_or_alternative) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(defined_value_or_default) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(assign_defined_default) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(error_if_unset) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(null_if_unset_or_alternative) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(length_of_variable) \
__ENUMERATE_SHELL_IMMEDIATE_FUNCTION(reexpand)
namespace Shell {
@ -376,10 +386,12 @@ public:
#undef __ENUMERATE_SHELL_OPTION
private:
Shell(Line::Editor&, bool attempt_interactive);
Shell(Line::Editor&, bool attempt_interactive, bool posix_mode = false);
Shell();
virtual ~Shell() override;
RefPtr<AST::Node> parse(StringView, bool interactive = false, bool as_command = true) const;
void timer_event(Core::TimerEvent&) override;
bool is_allowed_to_modify_termios(const AST::Command&) const;
@ -450,6 +462,7 @@ private:
bool m_is_interactive { true };
bool m_is_subshell { false };
bool m_should_reinstall_signal_handlers { true };
bool m_in_posix_mode { false };
ShellError m_error { ShellError::None };
DeprecatedString m_error_description;