mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-12-28 21:54:40 +03:00
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:
parent
2dc1682274
commit
2a276c86d4
Notes:
sideshowbarker
2024-07-17 06:09:44 +09:00
Author: https://github.com/alimpfard Commit: https://github.com/SerenityOS/serenity/commit/2a276c86d4 Pull-request: https://github.com/SerenityOS/serenity/pull/17437 Reviewed-by: https://github.com/kleinesfilmroellchen Reviewed-by: https://github.com/linusg
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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());
|
||||
|
@ -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 {
|
||||
|
@ -12,6 +12,8 @@ set(SOURCES
|
||||
Job.cpp
|
||||
NodeVisitor.cpp
|
||||
Parser.cpp
|
||||
PosixLexer.cpp
|
||||
PosixParser.cpp
|
||||
Shell.cpp
|
||||
)
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 };
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -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) \
|
||||
|
725
Userland/Shell/PosixLexer.cpp
Normal file
725
Userland/Shell/PosixLexer.cpp
Normal 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
413
Userland/Shell/PosixLexer.h
Normal 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 };
|
||||
};
|
||||
|
||||
}
|
1922
Userland/Shell/PosixParser.cpp
Normal file
1922
Userland/Shell/PosixParser.cpp
Normal file
File diff suppressed because it is too large
Load Diff
117
Userland/Shell/PosixParser.h
Normal file
117
Userland/Shell/PosixParser.h
Normal 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 };
|
||||
};
|
||||
|
||||
}
|
@ -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)
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user