mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-12-30 14:43:38 +03:00
8666b4fa87
This allows the formatter to generate a correct immediate invocation without a missing closing brace. This commit also removes a useless code comment.
2208 lines
78 KiB
C++
2208 lines
78 KiB
C++
/*
|
|
* Copyright (c) 2022, Ali Mohammad Pur <mpfard@serenityos.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/CharacterTypes.h>
|
|
#include <AK/Debug.h>
|
|
#include <AK/StringUtils.h>
|
|
#include <Shell/Parser.h>
|
|
#include <Shell/PosixParser.h>
|
|
|
|
#define TRY_OR_THROW_PARSE_ERROR_AT(expr, position) ({ \
|
|
/* Ignore -Wshadow to allow nesting the macro. */ \
|
|
AK_IGNORE_DIAGNOSTIC("-Wshadow", \
|
|
auto&& _value_or_error = expr;) \
|
|
if (_value_or_error.is_error()) { \
|
|
AK_IGNORE_DIAGNOSTIC("-Wshadow", \
|
|
auto _error = _value_or_error.release_error();) \
|
|
if (_error.is_errno() && _error.code() == ENOMEM) \
|
|
return make_ref_counted<AST::SyntaxError>(position, "OOM"_string); \
|
|
return make_ref_counted<AST::SyntaxError>(position, MUST(String::formatted("Error: {}", _error))); \
|
|
} \
|
|
_value_or_error.release_value(); \
|
|
})
|
|
|
|
static RefPtr<Shell::AST::Node> strip_execute(RefPtr<Shell::AST::Node> node)
|
|
{
|
|
while (node && node->is_execute())
|
|
node = static_ptr_cast<Shell::AST::Execute>(node)->command();
|
|
return node;
|
|
}
|
|
|
|
static Shell::AST::Position empty_position()
|
|
{
|
|
return { 0, 0, { 0, 0 }, { 0, 0 } };
|
|
}
|
|
|
|
template<typename T, typename... Ts>
|
|
static inline bool is_one_of(T const& value, Ts const&... values)
|
|
{
|
|
return ((value == values) || ...);
|
|
}
|
|
|
|
static inline bool is_io_operator(Shell::Posix::Token const& token)
|
|
{
|
|
using namespace Shell::Posix;
|
|
return is_one_of(token.type,
|
|
Token::Type::Less, Token::Type::Great,
|
|
Token::Type::LessAnd, Token::Type::GreatAnd,
|
|
Token::Type::DoubleLess, Token::Type::DoubleGreat,
|
|
Token::Type::DoubleLessDash, Token::Type::LessGreat,
|
|
Token::Type::Clobber);
|
|
}
|
|
|
|
static inline bool is_separator(Shell::Posix::Token const& token)
|
|
{
|
|
using namespace Shell::Posix;
|
|
return is_one_of(token.type,
|
|
Token::Type::Semicolon, Token::Type::Newline,
|
|
Token::Type::AndIf, Token::Type::OrIf,
|
|
Token::Type::Pipe,
|
|
Token::Type::And);
|
|
}
|
|
|
|
static inline bool is_a_reserved_word_position(Shell::Posix::Token const& token, Optional<Shell::Posix::Token> const& previous_token, Optional<Shell::Posix::Token> const& previous_previous_token)
|
|
{
|
|
using namespace Shell::Posix;
|
|
|
|
auto is_start_of_command = !previous_token.has_value()
|
|
|| previous_token->value.is_empty()
|
|
|| is_separator(*previous_token)
|
|
|| is_one_of(previous_token->type,
|
|
Token::Type::OpenParen, Token::Type::CloseParen, Token::Type::Newline, Token::Type::DoubleSemicolon,
|
|
Token::Type::Semicolon, Token::Type::Pipe, Token::Type::OrIf, Token::Type::AndIf);
|
|
if (is_start_of_command)
|
|
return true;
|
|
|
|
if (!previous_token.has_value())
|
|
return false;
|
|
|
|
auto previous_is_reserved_word = is_one_of(previous_token->value,
|
|
"for"sv, "in"sv, "case"sv, "if"sv, "then"sv, "else"sv,
|
|
"elif"sv, "while"sv, "until"sv, "do"sv, "done"sv, "esac"sv,
|
|
"fi"sv, "!"sv, "{"sv, "}"sv);
|
|
|
|
if (previous_is_reserved_word)
|
|
return true;
|
|
|
|
if (!previous_previous_token.has_value())
|
|
return false;
|
|
|
|
auto is_third_in_case = previous_previous_token->value == "case"sv
|
|
&& token.type == Token::Type::Token && token.value == "in"sv;
|
|
|
|
if (is_third_in_case)
|
|
return true;
|
|
|
|
auto is_third_in_for = previous_previous_token->value == "for"sv
|
|
&& token.type == Token::Type::Token && is_one_of(token.value, "in"sv, "do"sv);
|
|
|
|
return is_third_in_for;
|
|
}
|
|
|
|
static inline bool is_reserved(Shell::Posix::Token const& token)
|
|
{
|
|
using namespace Shell::Posix;
|
|
return is_one_of(token.type,
|
|
Token::Type::If, Token::Type::Then, Token::Type::Else,
|
|
Token::Type::Elif, Token::Type::Fi, Token::Type::Do,
|
|
Token::Type::Done, Token::Type::Case, Token::Type::Esac,
|
|
Token::Type::While, Token::Type::Until, Token::Type::For,
|
|
Token::Type::In, Token::Type::OpenBrace, Token::Type::CloseBrace,
|
|
Token::Type::Bang);
|
|
}
|
|
|
|
static inline bool is_valid_name(StringView word)
|
|
{
|
|
// Dr.POSIX: a word consisting solely of underscores, digits, and alphabetics from the portable character set. The first character of a name is not a digit.
|
|
return !word.is_empty()
|
|
&& !is_ascii_digit(word[0])
|
|
&& all_of(word, [](auto ch) { return is_ascii_alphanumeric(ch) || ch == '_'; });
|
|
}
|
|
|
|
namespace Shell::Posix {
|
|
|
|
template<typename... Args>
|
|
static NonnullRefPtr<AST::Node> immediate(String name, AST::Position position, Args&&... args)
|
|
{
|
|
return make_ref_counted<AST::ImmediateExpression>(
|
|
position,
|
|
AST::NameWithPosition {
|
|
move(name),
|
|
position,
|
|
},
|
|
Vector<NonnullRefPtr<AST::Node>> { forward<Args>(args)... },
|
|
empty_position());
|
|
}
|
|
|
|
template<typename... Args>
|
|
static NonnullRefPtr<AST::Node> reexpand(AST::Position position, Args&&... args)
|
|
{
|
|
return immediate("reexpand"_string, position, forward<Args>(args)...);
|
|
}
|
|
|
|
ErrorOr<void> Parser::fill_token_buffer(Optional<Reduction> starting_reduction)
|
|
{
|
|
for (;;) {
|
|
auto token = TRY(next_expanded_token(starting_reduction));
|
|
if (!token.has_value())
|
|
break;
|
|
#if SHELL_POSIX_PARSER_DEBUG
|
|
ByteString position = "(~)";
|
|
if (token->position.has_value())
|
|
position = ByteString::formatted("{}:{}", token->position->start_offset, token->position->end_offset);
|
|
ByteString expansions = "";
|
|
for (auto& exp : token->resolved_expansions)
|
|
exp.visit(
|
|
[&](ResolvedParameterExpansion& x) { expansions = ByteString::formatted("{}param({}),", expansions, x.to_byte_string()); },
|
|
[&](ResolvedCommandExpansion& x) { expansions = ByteString::formatted("{}command({:p})", expansions, x.command.ptr()); },
|
|
[&](ResolvedArithmeticExpansion& x) { expansions = ByteString::formatted("{}arith({})", expansions, x.source_expression); });
|
|
ByteString rexpansions = "";
|
|
for (auto& exp : token->expansions)
|
|
exp.visit(
|
|
[&](ParameterExpansion& x) { rexpansions = ByteString::formatted("{}param({}) from {} to {},", rexpansions, x.parameter.string_view(), x.range.start, x.range.length); },
|
|
[&](auto&) { rexpansions = ByteString::formatted("{}...,", rexpansions); });
|
|
dbgln("Token @ {}: '{}' (type {}) - parsed expansions: {} - raw expansions: {}", position, token->value.replace("\n"sv, "\\n"sv, ReplaceMode::All), token->type_name(), expansions, rexpansions);
|
|
#endif
|
|
}
|
|
m_token_index = 0;
|
|
|
|
// Detect Assignment words, bash-like lists extension
|
|
for (size_t i = 1; i < m_token_buffer.size(); ++i) {
|
|
// Treat 'ASSIGNMENT_WORD OPEN_PAREN' where ASSIGNMENT_WORD is `word=' and OPEN_PAREN has no preceding trivia as a bash-like list assignment.
|
|
auto& token = m_token_buffer[i - 1];
|
|
auto& next_token = m_token_buffer[i];
|
|
|
|
if (token.type != Token::Type::AssignmentWord)
|
|
continue;
|
|
|
|
if (!token.value.ends_with('='))
|
|
continue;
|
|
|
|
if (next_token.type != Token::Type::OpenParen)
|
|
continue;
|
|
|
|
if (token.position.map([](auto& x) { return x.end_offset + 1; }) != next_token.position.map([](auto& x) { return x.start_offset; }))
|
|
continue;
|
|
|
|
token.type = Token::Type::ListAssignmentWord;
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
RefPtr<AST::Node> Parser::parse()
|
|
{
|
|
auto start_position = peek().position.value_or(empty_position());
|
|
return TRY_OR_THROW_PARSE_ERROR_AT(parse_complete_command(), start_position);
|
|
}
|
|
|
|
void Parser::handle_heredoc_contents()
|
|
{
|
|
while (!eof() && m_token_buffer[m_token_index].type == Token::Type::HeredocContents) {
|
|
auto& token = m_token_buffer[m_token_index++];
|
|
auto entry = m_unprocessed_heredoc_entries.get(token.relevant_heredoc_key.value());
|
|
if (!entry.has_value()) {
|
|
error(token, "Discarding unexpected heredoc contents for key '{}'", *token.relevant_heredoc_key);
|
|
continue;
|
|
}
|
|
|
|
auto& heredoc = **entry;
|
|
|
|
auto contents_or_error = [&]() -> ErrorOr<RefPtr<AST::Node>> {
|
|
if (heredoc.allow_interpolation()) {
|
|
Parser parser { token.value, m_in_interactive_mode, Reduction::HeredocContents };
|
|
return parser.parse_word();
|
|
}
|
|
|
|
return make_ref_counted<AST::StringLiteral>(
|
|
token.position.value_or(empty_position()),
|
|
token.value,
|
|
AST::StringLiteral::EnclosureType::None);
|
|
}();
|
|
|
|
if (contents_or_error.is_error()) {
|
|
warnln("Shell: Failed to parse heredoc contents: {}", contents_or_error.error());
|
|
continue;
|
|
}
|
|
|
|
if (auto contents = contents_or_error.release_value())
|
|
heredoc.set_contents(contents);
|
|
|
|
m_unprocessed_heredoc_entries.remove(*token.relevant_heredoc_key);
|
|
}
|
|
}
|
|
|
|
ErrorOr<Optional<Token>> Parser::next_expanded_token(Optional<Reduction> starting_reduction)
|
|
{
|
|
while (m_token_buffer.is_empty() || m_token_buffer.last().type != Token::Type::Eof) {
|
|
auto tokens = TRY(m_lexer.batch_next(starting_reduction));
|
|
auto expanded = perform_expansions(move(tokens));
|
|
m_token_buffer.extend(expanded);
|
|
}
|
|
|
|
if (m_token_buffer.size() == m_token_index)
|
|
return OptionalNone {};
|
|
|
|
return m_token_buffer[m_token_index++];
|
|
}
|
|
|
|
Vector<Token> Parser::perform_expansions(Vector<Token> tokens)
|
|
{
|
|
if (tokens.is_empty())
|
|
return {};
|
|
|
|
Vector<Token> expanded_tokens;
|
|
auto previous_token = Optional<Token>();
|
|
auto previous_previous_token = Optional<Token>();
|
|
auto tokens_taken_from_buffer = 0;
|
|
|
|
expanded_tokens.ensure_capacity(tokens.size());
|
|
|
|
auto swap_expansions = [&] {
|
|
if (previous_previous_token.has_value())
|
|
expanded_tokens.append(previous_previous_token.release_value());
|
|
|
|
if (previous_token.has_value())
|
|
expanded_tokens.append(previous_token.release_value());
|
|
|
|
for (; tokens_taken_from_buffer > 0; tokens_taken_from_buffer--)
|
|
m_token_buffer.append(expanded_tokens.take_first());
|
|
|
|
swap(tokens, expanded_tokens);
|
|
expanded_tokens.clear_with_capacity();
|
|
};
|
|
|
|
// (1) join all consecutive newlines (this works around a grammar ambiguity)
|
|
auto previous_was_newline = !m_token_buffer.is_empty() && m_token_buffer.last().type == Token::Type::Newline;
|
|
for (auto& token : tokens) {
|
|
if (token.type == Token::Type::Newline) {
|
|
if (previous_was_newline)
|
|
continue;
|
|
previous_was_newline = true;
|
|
} else {
|
|
previous_was_newline = false;
|
|
}
|
|
expanded_tokens.append(move(token));
|
|
}
|
|
|
|
swap_expansions();
|
|
|
|
// (2) Detect reserved words
|
|
if (m_token_buffer.size() >= 1) {
|
|
previous_token = m_token_buffer.take_last();
|
|
tokens_taken_from_buffer++;
|
|
}
|
|
if (m_token_buffer.size() >= 1) {
|
|
previous_previous_token = m_token_buffer.take_last();
|
|
tokens_taken_from_buffer++;
|
|
}
|
|
|
|
auto check_reserved_word = [&](auto& token) {
|
|
if (is_a_reserved_word_position(token, previous_token, previous_previous_token)) {
|
|
if (token.value == "if"sv)
|
|
token.type = Token::Type::If;
|
|
else if (token.value == "then"sv)
|
|
token.type = Token::Type::Then;
|
|
else if (token.value == "else"sv)
|
|
token.type = Token::Type::Else;
|
|
else if (token.value == "elif"sv)
|
|
token.type = Token::Type::Elif;
|
|
else if (token.value == "fi"sv)
|
|
token.type = Token::Type::Fi;
|
|
else if (token.value == "while"sv)
|
|
token.type = Token::Type::While;
|
|
else if (token.value == "until"sv)
|
|
token.type = Token::Type::Until;
|
|
else if (token.value == "do"sv)
|
|
token.type = Token::Type::Do;
|
|
else if (token.value == "done"sv)
|
|
token.type = Token::Type::Done;
|
|
else if (token.value == "case"sv)
|
|
token.type = Token::Type::Case;
|
|
else if (token.value == "esac"sv)
|
|
token.type = Token::Type::Esac;
|
|
else if (token.value == "for"sv)
|
|
token.type = Token::Type::For;
|
|
else if (token.value == "in"sv)
|
|
token.type = Token::Type::In;
|
|
else if (token.value == "!"sv)
|
|
token.type = Token::Type::Bang;
|
|
else if (token.value == "{"sv)
|
|
token.type = Token::Type::OpenBrace;
|
|
else if (token.value == "}"sv)
|
|
token.type = Token::Type::CloseBrace;
|
|
else if (token.type == Token::Type::Token)
|
|
token.type = Token::Type::Word;
|
|
} else if (token.type == Token::Type::Token) {
|
|
token.type = Token::Type::Word;
|
|
}
|
|
};
|
|
|
|
for (auto& token : tokens) {
|
|
if (!previous_token.has_value()) {
|
|
check_reserved_word(token);
|
|
previous_token = token;
|
|
continue;
|
|
}
|
|
if (!previous_previous_token.has_value()) {
|
|
check_reserved_word(token);
|
|
previous_previous_token = move(previous_token);
|
|
previous_token = token;
|
|
continue;
|
|
}
|
|
|
|
check_reserved_word(token);
|
|
expanded_tokens.append(exchange(*previous_previous_token, exchange(*previous_token, move(token))));
|
|
}
|
|
|
|
swap_expansions();
|
|
|
|
// (3) Detect io_number tokens
|
|
previous_token = Optional<Token>();
|
|
tokens_taken_from_buffer = 0;
|
|
if (m_token_buffer.size() >= 1) {
|
|
previous_token = m_token_buffer.take_last();
|
|
tokens_taken_from_buffer++;
|
|
}
|
|
|
|
for (auto& token : tokens) {
|
|
if (!previous_token.has_value()) {
|
|
previous_token = token;
|
|
continue;
|
|
}
|
|
|
|
if (is_io_operator(token) && previous_token->type == Token::Type::Word && all_of(previous_token->value.bytes_as_string_view(), is_ascii_digit)) {
|
|
previous_token->type = Token::Type::IoNumber;
|
|
}
|
|
|
|
expanded_tokens.append(exchange(*previous_token, move(token)));
|
|
}
|
|
|
|
swap_expansions();
|
|
|
|
// (4) Try to identify simple commands
|
|
previous_token = Optional<Token>();
|
|
tokens_taken_from_buffer = 0;
|
|
|
|
if (m_token_buffer.size() >= 1) {
|
|
previous_token = m_token_buffer.take_last();
|
|
tokens_taken_from_buffer++;
|
|
}
|
|
|
|
for (auto& token : tokens) {
|
|
if (!previous_token.has_value()) {
|
|
token.could_be_start_of_a_simple_command = true;
|
|
previous_token = token;
|
|
continue;
|
|
}
|
|
|
|
token.could_be_start_of_a_simple_command = is_one_of(previous_token->type, Token::Type::OpenParen, Token::Type::CloseParen, Token::Type::Newline)
|
|
|| is_separator(*previous_token)
|
|
|| (!is_reserved(*previous_token) && is_reserved(token));
|
|
|
|
expanded_tokens.append(exchange(*previous_token, move(token)));
|
|
}
|
|
|
|
swap_expansions();
|
|
|
|
// (5) Detect assignment words
|
|
for (auto& token : tokens) {
|
|
if (token.could_be_start_of_a_simple_command)
|
|
m_disallow_command_prefix = false;
|
|
|
|
// Check if we're in a command prefix (could be an assignment)
|
|
if (!m_disallow_command_prefix && token.type == Token::Type::Word && token.value.contains('=')) {
|
|
// If the word before '=' is a valid name, this is an assignment
|
|
auto equal_offset = *token.value.find_byte_offset('=');
|
|
if (is_valid_name(token.value.bytes_as_string_view().substring_view(0, equal_offset)))
|
|
token.type = Token::Type::AssignmentWord;
|
|
else
|
|
m_disallow_command_prefix = true;
|
|
} else {
|
|
m_disallow_command_prefix = true;
|
|
}
|
|
|
|
expanded_tokens.append(move(token));
|
|
}
|
|
|
|
swap_expansions();
|
|
|
|
// (6) Parse expansions
|
|
for (auto& token : tokens) {
|
|
if (!is_one_of(token.type, Token::Type::Word, Token::Type::AssignmentWord)) {
|
|
expanded_tokens.append(move(token));
|
|
continue;
|
|
}
|
|
|
|
Vector<ResolvedExpansion> resolved_expansions;
|
|
for (auto& expansion : token.expansions) {
|
|
auto resolved = expansion.visit(
|
|
[&](ParameterExpansion const& expansion) -> ResolvedExpansion {
|
|
auto text = expansion.parameter.string_view();
|
|
// ${NUMBER}
|
|
if (all_of(text, is_ascii_digit)) {
|
|
return ResolvedParameterExpansion {
|
|
.parameter = expansion.parameter.to_string().release_value_but_fixme_should_propagate_errors(),
|
|
.argument = {},
|
|
.range = expansion.range,
|
|
.op = ResolvedParameterExpansion::Op::GetPositionalParameter,
|
|
.expand = ResolvedParameterExpansion::Expand::Word,
|
|
};
|
|
}
|
|
|
|
if (text.length() == 1) {
|
|
ResolvedParameterExpansion::Op op;
|
|
switch (text[0]) {
|
|
case '!':
|
|
op = ResolvedParameterExpansion::Op::GetLastBackgroundPid;
|
|
break;
|
|
case '@':
|
|
op = ResolvedParameterExpansion::Op::GetPositionalParameterList;
|
|
break;
|
|
case '-':
|
|
op = ResolvedParameterExpansion::Op::GetCurrentOptionFlags;
|
|
break;
|
|
case '#':
|
|
op = ResolvedParameterExpansion::Op::GetPositionalParameterCount;
|
|
break;
|
|
case '?':
|
|
op = ResolvedParameterExpansion::Op::GetLastExitStatus;
|
|
break;
|
|
case '*':
|
|
op = ResolvedParameterExpansion::Op::GetPositionalParameterListAsString;
|
|
break;
|
|
case '$':
|
|
op = ResolvedParameterExpansion::Op::GetShellProcessId;
|
|
break;
|
|
default:
|
|
if (is_valid_name(text)) {
|
|
op = ResolvedParameterExpansion::Op::GetVariable;
|
|
} else {
|
|
error(token, "Unknown parameter expansion: {}", text);
|
|
return ResolvedParameterExpansion {
|
|
.parameter = expansion.parameter.to_string().release_value_but_fixme_should_propagate_errors(),
|
|
.argument = {},
|
|
.range = expansion.range,
|
|
.op = ResolvedParameterExpansion::Op::StringLength,
|
|
};
|
|
}
|
|
}
|
|
|
|
return ResolvedParameterExpansion {
|
|
.parameter = String::from_code_point(text[0]),
|
|
.argument = {},
|
|
.range = expansion.range,
|
|
.op = op,
|
|
.expand = ResolvedParameterExpansion::Expand::Word,
|
|
};
|
|
}
|
|
|
|
if (text.starts_with('#')) {
|
|
return ResolvedParameterExpansion {
|
|
.parameter = String::from_utf8(text.substring_view(1)).release_value_but_fixme_should_propagate_errors(),
|
|
.argument = {},
|
|
.range = expansion.range,
|
|
.op = ResolvedParameterExpansion::Op::StringLength,
|
|
};
|
|
}
|
|
|
|
GenericLexer lexer { text };
|
|
auto parameter = lexer.consume_while([first = true](char c) mutable {
|
|
if (first) {
|
|
first = false;
|
|
return is_ascii_alpha(c) || c == '_';
|
|
}
|
|
return is_ascii_alphanumeric(c) || c == '_';
|
|
});
|
|
|
|
StringView argument;
|
|
ResolvedParameterExpansion::Op op;
|
|
switch (lexer.peek()) {
|
|
case ':':
|
|
lexer.ignore();
|
|
switch (lexer.is_eof() ? 0 : lexer.consume()) {
|
|
case '-':
|
|
argument = lexer.consume_all();
|
|
op = ResolvedParameterExpansion::Op::UseDefaultValue;
|
|
break;
|
|
case '=':
|
|
argument = lexer.consume_all();
|
|
op = ResolvedParameterExpansion::Op::AssignDefaultValue;
|
|
break;
|
|
case '?':
|
|
argument = lexer.consume_all();
|
|
op = ResolvedParameterExpansion::Op::IndicateErrorIfEmpty;
|
|
break;
|
|
case '+':
|
|
argument = lexer.consume_all();
|
|
op = ResolvedParameterExpansion::Op::UseAlternativeValue;
|
|
break;
|
|
default:
|
|
error(token, "Unknown parameter expansion: {}", text);
|
|
return ResolvedParameterExpansion {
|
|
.parameter = String::from_utf8(parameter).release_value_but_fixme_should_propagate_errors(),
|
|
.argument = {},
|
|
.range = expansion.range,
|
|
.op = ResolvedParameterExpansion::Op::StringLength,
|
|
};
|
|
}
|
|
break;
|
|
case '-':
|
|
lexer.ignore();
|
|
argument = lexer.consume_all();
|
|
op = ResolvedParameterExpansion::Op::UseDefaultValueIfUnset;
|
|
break;
|
|
case '=':
|
|
lexer.ignore();
|
|
argument = lexer.consume_all();
|
|
op = ResolvedParameterExpansion::Op::AssignDefaultValueIfUnset;
|
|
break;
|
|
case '?':
|
|
lexer.ignore();
|
|
argument = lexer.consume_all();
|
|
op = ResolvedParameterExpansion::Op::IndicateErrorIfUnset;
|
|
break;
|
|
case '+':
|
|
lexer.ignore();
|
|
argument = lexer.consume_all();
|
|
op = ResolvedParameterExpansion::Op::UseAlternativeValueIfUnset;
|
|
break;
|
|
case '%':
|
|
if (lexer.consume_specific('%'))
|
|
op = ResolvedParameterExpansion::Op::RemoveLargestSuffixByPattern;
|
|
else
|
|
op = ResolvedParameterExpansion::Op::RemoveSmallestSuffixByPattern;
|
|
argument = lexer.consume_all();
|
|
break;
|
|
case '#':
|
|
if (lexer.consume_specific('#'))
|
|
op = ResolvedParameterExpansion::Op::RemoveLargestPrefixByPattern;
|
|
else
|
|
op = ResolvedParameterExpansion::Op::RemoveSmallestPrefixByPattern;
|
|
argument = lexer.consume_all();
|
|
break;
|
|
default:
|
|
if (is_valid_name(text)) {
|
|
op = ResolvedParameterExpansion::Op::GetVariable;
|
|
} else {
|
|
error(token, "Unknown parameter expansion: {}", text);
|
|
return ResolvedParameterExpansion {
|
|
.parameter = String::from_utf8(parameter).release_value_but_fixme_should_propagate_errors(),
|
|
.argument = {},
|
|
.range = expansion.range,
|
|
.op = ResolvedParameterExpansion::Op::StringLength,
|
|
};
|
|
}
|
|
}
|
|
VERIFY(lexer.is_eof());
|
|
|
|
return ResolvedParameterExpansion {
|
|
.parameter = String::from_utf8(parameter).release_value_but_fixme_should_propagate_errors(),
|
|
.argument = String::from_utf8(argument).release_value_but_fixme_should_propagate_errors(),
|
|
.range = expansion.range,
|
|
.op = op,
|
|
.expand = ResolvedParameterExpansion::Expand::Word,
|
|
};
|
|
},
|
|
[&](ArithmeticExpansion const& expansion) -> ResolvedExpansion {
|
|
return ResolvedArithmeticExpansion { expansion.expression, expansion.range };
|
|
},
|
|
[&](CommandExpansion const& expansion) -> ResolvedExpansion {
|
|
Parser parser { expansion.command.string_view() };
|
|
auto node = parser.parse();
|
|
m_errors.extend(move(parser.m_errors));
|
|
return ResolvedCommandExpansion {
|
|
move(node),
|
|
expansion.range,
|
|
};
|
|
});
|
|
|
|
resolved_expansions.append(move(resolved));
|
|
}
|
|
|
|
token.resolved_expansions = move(resolved_expansions);
|
|
expanded_tokens.append(move(token));
|
|
}
|
|
|
|
swap_expansions();
|
|
|
|
// (7) Loop variables
|
|
previous_token = {};
|
|
tokens_taken_from_buffer = 0;
|
|
if (m_token_buffer.size() >= 1) {
|
|
previous_token = m_token_buffer.take_last();
|
|
tokens_taken_from_buffer++;
|
|
}
|
|
|
|
for (auto& token : tokens) {
|
|
if (!previous_token.has_value()) {
|
|
previous_token = token;
|
|
continue;
|
|
}
|
|
|
|
if (previous_token->type == Token::Type::For && token.type == Token::Type::Word && is_valid_name(token.value)) {
|
|
token.type = Token::Type::VariableName;
|
|
}
|
|
|
|
expanded_tokens.append(exchange(*previous_token, token));
|
|
}
|
|
|
|
swap_expansions();
|
|
|
|
// (8) Function names
|
|
previous_token = {};
|
|
previous_previous_token = {};
|
|
tokens_taken_from_buffer = 0;
|
|
if (m_token_buffer.size() >= 1) {
|
|
previous_token = m_token_buffer.take_last();
|
|
tokens_taken_from_buffer++;
|
|
}
|
|
if (m_token_buffer.size() >= 1) {
|
|
previous_previous_token = m_token_buffer.take_last();
|
|
tokens_taken_from_buffer++;
|
|
}
|
|
|
|
for (auto& token : tokens) {
|
|
if (!previous_token.has_value()) {
|
|
previous_token = token;
|
|
continue;
|
|
}
|
|
if (!previous_previous_token.has_value()) {
|
|
previous_previous_token = move(previous_token);
|
|
previous_token = token;
|
|
continue;
|
|
}
|
|
|
|
// NAME ( )
|
|
if (previous_previous_token->could_be_start_of_a_simple_command
|
|
&& previous_previous_token->type == Token::Type::Word
|
|
&& previous_token->type == Token::Type::OpenParen
|
|
&& token.type == Token::Type::CloseParen) {
|
|
|
|
previous_previous_token->type = Token::Type::VariableName;
|
|
}
|
|
|
|
expanded_tokens.append(exchange(*previous_previous_token, exchange(*previous_token, token)));
|
|
}
|
|
|
|
swap_expansions();
|
|
|
|
return tokens;
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_complete_command()
|
|
{
|
|
auto list = TRY([&]() -> ErrorOr<RefPtr<AST::Node>> {
|
|
// separator...
|
|
while (is_separator(peek()))
|
|
skip();
|
|
|
|
// list EOF
|
|
auto list = TRY(parse_list());
|
|
if (eof())
|
|
return list;
|
|
|
|
// list separator EOF
|
|
while (is_separator(peek()))
|
|
skip();
|
|
|
|
if (eof())
|
|
return list;
|
|
|
|
auto position = peek().position;
|
|
auto syntax_error = make_ref_counted<AST::SyntaxError>(
|
|
position.value_or(empty_position()),
|
|
"Extra tokens after complete command"_string);
|
|
|
|
if (list)
|
|
list->set_is_syntax_error(*syntax_error);
|
|
else
|
|
list = syntax_error;
|
|
|
|
return list;
|
|
}());
|
|
|
|
if (!list)
|
|
return nullptr;
|
|
|
|
return make_ref_counted<AST::Execute>(list->position(), *list);
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_list()
|
|
{
|
|
Vector<NonnullRefPtr<AST::Node>> nodes;
|
|
Vector<AST::Position> positions;
|
|
|
|
auto start_position = peek().position.value_or(empty_position());
|
|
|
|
for (;;) {
|
|
auto new_node = TRY(parse_and_or());
|
|
if (!new_node)
|
|
break;
|
|
|
|
if (peek().type == Token::Type::And) {
|
|
new_node = make_ref_counted<AST::Background>(
|
|
new_node->position(),
|
|
*new_node);
|
|
}
|
|
|
|
nodes.append(new_node.release_nonnull());
|
|
|
|
if (!is_separator(peek()) || eof())
|
|
break;
|
|
|
|
auto position = consume().position;
|
|
if (position.has_value())
|
|
positions.append(position.release_value());
|
|
}
|
|
|
|
auto end_position = peek().position.value_or(empty_position());
|
|
|
|
return make_ref_counted<AST::Sequence>(
|
|
AST::Position {
|
|
start_position.start_offset,
|
|
end_position.end_offset,
|
|
start_position.start_line,
|
|
end_position.end_line,
|
|
},
|
|
move(nodes),
|
|
move(positions));
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_and_or()
|
|
{
|
|
while (peek().type == Token::Type::Newline)
|
|
skip();
|
|
|
|
auto node = TRY(parse_pipeline());
|
|
if (!node)
|
|
return RefPtr<AST::Node> {};
|
|
|
|
for (;;) {
|
|
if (peek().type == Token::Type::AndIf) {
|
|
auto and_token = consume();
|
|
while (peek().type == Token::Type::Newline)
|
|
skip();
|
|
|
|
auto rhs = TRY(parse_pipeline());
|
|
if (!rhs)
|
|
return RefPtr<AST::Node> {};
|
|
node = make_ref_counted<AST::And>(
|
|
node->position(),
|
|
*node,
|
|
rhs.release_nonnull(),
|
|
and_token.position.value_or(empty_position()));
|
|
continue;
|
|
}
|
|
if (peek().type == Token::Type::OrIf) {
|
|
auto or_token = consume();
|
|
while (peek().type == Token::Type::Newline)
|
|
skip();
|
|
|
|
auto rhs = TRY(parse_pipeline());
|
|
if (!rhs)
|
|
return RefPtr<AST::Node> {};
|
|
node = make_ref_counted<AST::Or>(
|
|
node->position(),
|
|
*node,
|
|
rhs.release_nonnull(),
|
|
or_token.position.value_or(empty_position()));
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_pipeline()
|
|
{
|
|
while (peek().type == Token::Type::Newline)
|
|
skip();
|
|
|
|
auto is_negated = false;
|
|
if (peek().type == Token::Type::Bang) {
|
|
is_negated = true;
|
|
skip();
|
|
}
|
|
|
|
return parse_pipe_sequence(is_negated);
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_pipe_sequence(bool is_negated)
|
|
{
|
|
auto node = TRY(parse_command());
|
|
if (!node)
|
|
return RefPtr<AST::Node> {};
|
|
|
|
if (is_negated) {
|
|
if (is<AST::CastToCommand>(node.ptr())) {
|
|
node = make_ref_counted<AST::CastToCommand>(
|
|
node->position(),
|
|
make_ref_counted<AST::ListConcatenate>(
|
|
node->position(),
|
|
Vector<NonnullRefPtr<AST::Node>> {
|
|
make_ref_counted<AST::BarewordLiteral>(
|
|
node->position(),
|
|
"not"_string),
|
|
*static_cast<AST::CastToCommand&>(*node).inner() }));
|
|
}
|
|
}
|
|
|
|
for (;;) {
|
|
if (peek().type != Token::Type::Pipe)
|
|
break;
|
|
|
|
consume();
|
|
while (peek().type == Token::Type::Newline)
|
|
skip();
|
|
|
|
auto rhs = TRY(parse_command());
|
|
if (!rhs)
|
|
return RefPtr<AST::Node> {};
|
|
node = make_ref_counted<AST::Pipe>(
|
|
node->position(),
|
|
*node,
|
|
rhs.release_nonnull());
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_command()
|
|
{
|
|
auto node = TRY([this]() -> ErrorOr<RefPtr<AST::Node>> {
|
|
if (auto node = TRY(parse_function_definition()))
|
|
return node;
|
|
|
|
if (auto node = TRY(parse_simple_command()))
|
|
return make_ref_counted<AST::CastToCommand>(node->position(), *node);
|
|
|
|
auto node = TRY(parse_compound_command());
|
|
if (!node)
|
|
return node;
|
|
|
|
if (auto list = TRY(parse_redirect_list())) {
|
|
auto position = list->position();
|
|
node = make_ref_counted<AST::Join>(
|
|
node->position().with_end(position),
|
|
*node,
|
|
list.release_nonnull());
|
|
}
|
|
|
|
return node;
|
|
}());
|
|
|
|
if (!node)
|
|
return nullptr;
|
|
|
|
return node;
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_function_definition()
|
|
{
|
|
// NAME OPEN_PAREN CLOSE_PAREN newline* function_body
|
|
|
|
auto start_index = m_token_index;
|
|
ArmedScopeGuard reset = [&] {
|
|
m_token_index = start_index;
|
|
};
|
|
|
|
if (peek().type != Token::Type::VariableName) {
|
|
return nullptr;
|
|
}
|
|
|
|
auto name = consume();
|
|
|
|
if (consume().type != Token::Type::OpenParen)
|
|
return nullptr;
|
|
|
|
if (consume().type != Token::Type::CloseParen)
|
|
return nullptr;
|
|
|
|
while (peek().type == Token::Type::Newline)
|
|
skip();
|
|
|
|
auto body = TRY(parse_function_body());
|
|
|
|
if (!body)
|
|
return nullptr;
|
|
|
|
reset.disarm();
|
|
|
|
return make_ref_counted<AST::FunctionDeclaration>(
|
|
name.position.value_or(empty_position()).with_end(peek().position.value_or(empty_position())),
|
|
AST::NameWithPosition { name.value, name.position.value_or(empty_position()) },
|
|
Vector<AST::NameWithPosition> {},
|
|
body.release_nonnull());
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_function_body()
|
|
{
|
|
// compound_command redirect_list?
|
|
|
|
auto node = TRY(parse_compound_command());
|
|
if (!node)
|
|
return nullptr;
|
|
|
|
if (auto list = TRY(parse_redirect_list())) {
|
|
auto position = list->position();
|
|
node = make_ref_counted<AST::Join>(
|
|
node->position().with_end(position),
|
|
*node,
|
|
list.release_nonnull());
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_redirect_list()
|
|
{
|
|
// io_redirect*
|
|
|
|
RefPtr<AST::Node> node;
|
|
|
|
for (;;) {
|
|
auto new_node = TRY(parse_io_redirect());
|
|
if (!new_node)
|
|
break;
|
|
|
|
if (node) {
|
|
node = make_ref_counted<AST::Join>(
|
|
node->position().with_end(new_node->position()),
|
|
*node,
|
|
new_node.release_nonnull());
|
|
} else {
|
|
node = new_node;
|
|
}
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_compound_command()
|
|
{
|
|
if (auto node = TRY(parse_brace_group()))
|
|
return node;
|
|
|
|
if (auto node = TRY(parse_subshell()))
|
|
return node;
|
|
|
|
if (auto node = TRY(parse_if_clause()))
|
|
return node;
|
|
|
|
if (auto node = TRY(parse_for_clause()))
|
|
return node;
|
|
|
|
if (auto node = TRY(parse_case_clause()))
|
|
return node;
|
|
|
|
if (auto node = TRY(parse_while_clause()))
|
|
return node;
|
|
|
|
if (auto node = TRY(parse_until_clause()))
|
|
return node;
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_while_clause()
|
|
{
|
|
if (peek().type != Token::Type::While)
|
|
return nullptr;
|
|
|
|
auto start_position = consume().position.value_or(empty_position());
|
|
auto condition = TRY(parse_compound_list());
|
|
if (!condition)
|
|
condition = make_ref_counted<AST::SyntaxError>(
|
|
peek().position.value_or(empty_position()),
|
|
"Expected condition after 'while'"_string);
|
|
|
|
auto do_group = TRY(parse_do_group());
|
|
if (!do_group)
|
|
do_group = make_ref_counted<AST::SyntaxError>(
|
|
peek().position.value_or(empty_position()),
|
|
"Expected 'do' after 'while'"_string);
|
|
|
|
// while foo; bar -> loop { if foo { bar } else { break } }
|
|
auto position = start_position.with_end(peek().position.value_or(empty_position()));
|
|
return make_ref_counted<AST::ForLoop>(
|
|
position,
|
|
Optional<AST::NameWithPosition> {},
|
|
Optional<AST::NameWithPosition> {},
|
|
nullptr,
|
|
make_ref_counted<AST::Execute>(position,
|
|
make_ref_counted<AST::IfCond>(
|
|
position,
|
|
Optional<AST::Position> {},
|
|
condition.release_nonnull(),
|
|
do_group.release_nonnull(),
|
|
make_ref_counted<AST::ContinuationControl>(
|
|
start_position,
|
|
AST::ContinuationControl::ContinuationKind::Break))));
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_until_clause()
|
|
{
|
|
if (peek().type != Token::Type::Until)
|
|
return nullptr;
|
|
|
|
auto start_position = consume().position.value_or(empty_position());
|
|
auto condition = TRY(parse_compound_list());
|
|
if (!condition)
|
|
condition = make_ref_counted<AST::SyntaxError>(
|
|
peek().position.value_or(empty_position()),
|
|
"Expected condition after 'until'"_string);
|
|
|
|
auto do_group = TRY(parse_do_group());
|
|
if (!do_group)
|
|
do_group = make_ref_counted<AST::SyntaxError>(
|
|
peek().position.value_or(empty_position()),
|
|
"Expected 'do' after 'until'"_string);
|
|
|
|
// until foo; bar -> loop { if foo { break } else { bar } }
|
|
auto position = start_position.with_end(peek().position.value_or(empty_position()));
|
|
return make_ref_counted<AST::ForLoop>(
|
|
position,
|
|
Optional<AST::NameWithPosition> {},
|
|
Optional<AST::NameWithPosition> {},
|
|
nullptr,
|
|
make_ref_counted<AST::Execute>(position,
|
|
make_ref_counted<AST::IfCond>(
|
|
position,
|
|
Optional<AST::Position> {},
|
|
condition.release_nonnull(),
|
|
make_ref_counted<AST::ContinuationControl>(
|
|
start_position,
|
|
AST::ContinuationControl::ContinuationKind::Break),
|
|
do_group.release_nonnull())));
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_brace_group()
|
|
{
|
|
if (peek().type != Token::Type::OpenBrace)
|
|
return nullptr;
|
|
|
|
consume();
|
|
|
|
auto list = TRY(parse_compound_list());
|
|
|
|
RefPtr<AST::SyntaxError> error;
|
|
if (peek().type != Token::Type::CloseBrace) {
|
|
error = make_ref_counted<AST::SyntaxError>(
|
|
peek().position.value_or(empty_position()),
|
|
TRY(String::formatted("Expected '}}', not {}", peek().type_name())));
|
|
} else {
|
|
consume();
|
|
}
|
|
|
|
if (error) {
|
|
if (list)
|
|
list->set_is_syntax_error(*error);
|
|
else
|
|
list = error;
|
|
}
|
|
|
|
return make_ref_counted<AST::Execute>(list->position(), *list);
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_case_clause()
|
|
{
|
|
auto start_position = peek().position.value_or(empty_position());
|
|
if (peek().type != Token::Type::Case)
|
|
return nullptr;
|
|
|
|
skip();
|
|
|
|
RefPtr<AST::SyntaxError> syntax_error;
|
|
auto expr = TRY(parse_word());
|
|
if (!expr)
|
|
expr = make_ref_counted<AST::SyntaxError>(
|
|
peek().position.value_or(empty_position()),
|
|
TRY(String::formatted("Expected a word, not {}", peek().type_name())));
|
|
|
|
if (peek().type != Token::Type::In) {
|
|
syntax_error = make_ref_counted<AST::SyntaxError>(
|
|
peek().position.value_or(empty_position()),
|
|
TRY(String::formatted("Expected 'in', not {}", peek().type_name())));
|
|
} else {
|
|
skip();
|
|
}
|
|
|
|
while (peek().type == Token::Type::Newline)
|
|
skip();
|
|
|
|
Vector<AST::MatchEntry> entries;
|
|
|
|
for (;;) {
|
|
if (eof() || peek().type == Token::Type::Esac)
|
|
break;
|
|
|
|
if (peek().type == Token::Type::Newline) {
|
|
skip();
|
|
continue;
|
|
}
|
|
|
|
// Parse a pattern list
|
|
auto needs_dsemi = true;
|
|
if (peek().type == Token::Type::OpenParen) {
|
|
skip();
|
|
needs_dsemi = false;
|
|
}
|
|
|
|
auto result = TRY(parse_case_list());
|
|
|
|
if (peek().type == Token::Type::CloseParen) {
|
|
skip();
|
|
} else {
|
|
if (!syntax_error)
|
|
syntax_error = make_ref_counted<AST::SyntaxError>(
|
|
peek().position.value_or(empty_position()),
|
|
TRY(String::formatted("Expected ')', not {}", peek().type_name())));
|
|
break;
|
|
}
|
|
|
|
while (peek().type == Token::Type::Newline)
|
|
skip();
|
|
|
|
auto compound_list = TRY(parse_compound_list());
|
|
|
|
if (peek().type == Token::Type::DoubleSemicolon) {
|
|
skip();
|
|
} else if (needs_dsemi) {
|
|
if (!syntax_error)
|
|
syntax_error = make_ref_counted<AST::SyntaxError>(
|
|
peek().position.value_or(empty_position()),
|
|
TRY(String::formatted("Expected ';;', not {}", peek().type_name())));
|
|
}
|
|
|
|
if (syntax_error) {
|
|
if (compound_list)
|
|
compound_list->set_is_syntax_error(*syntax_error);
|
|
else
|
|
compound_list = syntax_error;
|
|
syntax_error = nullptr;
|
|
}
|
|
|
|
auto position = compound_list->position();
|
|
entries.append(AST::MatchEntry {
|
|
.options = move(result.nodes),
|
|
.match_names = {},
|
|
.match_as_position = {},
|
|
.pipe_positions = move(result.pipe_positions),
|
|
.body = make_ref_counted<AST::Execute>(position, compound_list.release_nonnull()),
|
|
});
|
|
}
|
|
|
|
if (peek().type != Token::Type::Esac) {
|
|
syntax_error = make_ref_counted<AST::SyntaxError>(
|
|
peek().position.value_or(empty_position()),
|
|
TRY(String::formatted("Expected 'esac', not {}", peek().type_name())));
|
|
} else {
|
|
skip();
|
|
}
|
|
|
|
auto node = make_ref_counted<AST::MatchExpr>(
|
|
start_position.with_end(peek().position.value_or(empty_position())),
|
|
expr.release_nonnull(),
|
|
String {},
|
|
Optional<AST::Position> {},
|
|
move(entries));
|
|
|
|
if (syntax_error)
|
|
node->set_is_syntax_error(*syntax_error);
|
|
|
|
return node;
|
|
}
|
|
|
|
ErrorOr<Parser::CaseItemsResult> Parser::parse_case_list()
|
|
{
|
|
// Just a list of words split by '|', delimited by ')'
|
|
Vector<NonnullRefPtr<AST::Node>> nodes;
|
|
Vector<AST::Position> pipes;
|
|
|
|
for (;;) {
|
|
if (eof() || peek().type == Token::Type::CloseParen)
|
|
break;
|
|
|
|
if (peek().type != Token::Type::Word)
|
|
break;
|
|
|
|
auto node = TRY(parse_word());
|
|
if (!node)
|
|
node = make_ref_counted<AST::SyntaxError>(
|
|
peek().position.value_or(empty_position()),
|
|
TRY(String::formatted("Expected a word, not {}", peek().type_name())));
|
|
|
|
nodes.append(node.release_nonnull());
|
|
|
|
if (peek().type == Token::Type::Pipe) {
|
|
pipes.append(peek().position.value_or(empty_position()));
|
|
skip();
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (nodes.is_empty())
|
|
nodes.append(make_ref_counted<AST::SyntaxError>(
|
|
peek().position.value_or(empty_position()),
|
|
TRY(String::formatted("Expected a word, not {}", peek().type_name()))));
|
|
|
|
return CaseItemsResult { move(pipes), move(nodes) };
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_if_clause()
|
|
{
|
|
// If compound_list Then compound_list {Elif compound_list Then compound_list (Fi|Else)?} [(?=Else) compound_list] (?!=Fi) Fi
|
|
auto start_position = peek().position.value_or(empty_position());
|
|
if (peek().type != Token::Type::If)
|
|
return nullptr;
|
|
|
|
skip();
|
|
auto main_condition = TRY(parse_compound_list());
|
|
if (!main_condition)
|
|
main_condition = make_ref_counted<AST::SyntaxError>(empty_position(), "Expected compound list after 'if'"_string);
|
|
|
|
RefPtr<AST::SyntaxError> syntax_error;
|
|
if (peek().type != Token::Type::Then) {
|
|
syntax_error = make_ref_counted<AST::SyntaxError>(
|
|
peek().position.value_or(empty_position()),
|
|
TRY(String::formatted("Expected 'then', not {}", peek().type_name())));
|
|
} else {
|
|
skip();
|
|
}
|
|
|
|
auto main_consequence = TRY(parse_compound_list());
|
|
if (!main_consequence)
|
|
main_consequence = make_ref_counted<AST::SyntaxError>(empty_position(), "Expected compound list after 'then'"_string);
|
|
|
|
auto node = make_ref_counted<AST::IfCond>(start_position, Optional<AST::Position>(), main_condition.release_nonnull(), main_consequence.release_nonnull(), nullptr);
|
|
auto active_node = node;
|
|
|
|
while (peek().type == Token::Type::Elif) {
|
|
skip();
|
|
auto condition = TRY(parse_compound_list());
|
|
if (!condition)
|
|
condition = make_ref_counted<AST::SyntaxError>(empty_position(), "Expected compound list after 'elif'"_string);
|
|
|
|
if (peek().type != Token::Type::Then) {
|
|
if (!syntax_error)
|
|
syntax_error = make_ref_counted<AST::SyntaxError>(
|
|
peek().position.value_or(empty_position()),
|
|
TRY(String::formatted("Expected 'then', not {}", peek().type_name())));
|
|
} else {
|
|
skip();
|
|
}
|
|
|
|
auto consequence = TRY(parse_compound_list());
|
|
if (!consequence)
|
|
consequence = make_ref_counted<AST::SyntaxError>(empty_position(), "Expected compound list after 'then'"_string);
|
|
|
|
auto new_node = make_ref_counted<AST::IfCond>(start_position, Optional<AST::Position>(), condition.release_nonnull(), consequence.release_nonnull(), nullptr);
|
|
|
|
active_node->false_branch() = new_node;
|
|
active_node = move(new_node);
|
|
}
|
|
|
|
auto needs_fi = true;
|
|
switch (peek().type) {
|
|
case Token::Type::Else:
|
|
skip();
|
|
active_node->false_branch() = TRY(parse_compound_list());
|
|
if (!active_node->false_branch())
|
|
active_node->false_branch() = make_ref_counted<AST::SyntaxError>(empty_position(), "Expected compound list after 'else'"_string);
|
|
break;
|
|
case Token::Type::Fi:
|
|
skip();
|
|
needs_fi = false;
|
|
break;
|
|
default:
|
|
if (!syntax_error)
|
|
syntax_error = make_ref_counted<AST::SyntaxError>(
|
|
peek().position.value_or(empty_position()),
|
|
TRY(String::formatted("Expected 'else' or 'fi', not {}", peek().type_name())));
|
|
break;
|
|
}
|
|
|
|
if (needs_fi) {
|
|
if (peek().type != Token::Type::Fi) {
|
|
if (!syntax_error)
|
|
syntax_error = make_ref_counted<AST::SyntaxError>(
|
|
peek().position.value_or(empty_position()),
|
|
TRY(String::formatted("Expected 'fi', not {}", peek().type_name())));
|
|
} else {
|
|
skip();
|
|
}
|
|
}
|
|
|
|
if (syntax_error)
|
|
node->set_is_syntax_error(*syntax_error);
|
|
|
|
return node;
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_subshell()
|
|
{
|
|
auto start_position = peek().position.value_or(empty_position());
|
|
if (peek().type != Token::Type::OpenParen)
|
|
return nullptr;
|
|
|
|
skip();
|
|
RefPtr<AST::SyntaxError> error;
|
|
|
|
auto list = TRY(parse_compound_list());
|
|
if (!list)
|
|
error = make_ref_counted<AST::SyntaxError>(peek().position.value_or(empty_position()), "Expected compound list after ("_string);
|
|
|
|
if (peek().type != Token::Type::CloseParen)
|
|
error = make_ref_counted<AST::SyntaxError>(peek().position.value_or(empty_position()), "Expected ) after compound list"_string);
|
|
else
|
|
skip();
|
|
|
|
if (!list)
|
|
return error;
|
|
|
|
return make_ref_counted<AST::Subshell>(
|
|
start_position.with_end(peek().position.value_or(empty_position())),
|
|
list.release_nonnull());
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_compound_list()
|
|
{
|
|
while (peek().type == Token::Type::Newline)
|
|
skip();
|
|
|
|
auto term = TRY(parse_term());
|
|
if (!term)
|
|
return term;
|
|
|
|
if (is_separator(peek())) {
|
|
if (consume().type == Token::Type::And) {
|
|
term = make_ref_counted<AST::Background>(
|
|
term->position().with_end(peek().position.value_or(empty_position())),
|
|
*term);
|
|
}
|
|
}
|
|
|
|
return term;
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_term()
|
|
{
|
|
Vector<NonnullRefPtr<AST::Node>> nodes;
|
|
Vector<AST::Position> positions;
|
|
|
|
auto start_position = peek().position.value_or(empty_position());
|
|
|
|
for (;;) {
|
|
auto new_node = TRY(parse_and_or());
|
|
if (!new_node)
|
|
break;
|
|
|
|
nodes.append(new_node.release_nonnull());
|
|
|
|
if (!is_separator(peek()))
|
|
break;
|
|
|
|
auto position = consume().position;
|
|
if (position.has_value())
|
|
positions.append(position.release_value());
|
|
}
|
|
|
|
auto end_position = peek().position.value_or(empty_position());
|
|
|
|
return make_ref_counted<AST::Sequence>(
|
|
start_position.with_end(end_position),
|
|
move(nodes),
|
|
move(positions));
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_for_clause()
|
|
{
|
|
// FOR NAME newline+ do_group //-> FOR NAME IN "$@" newline+ do_group
|
|
// FOR NAME newline+ IN separator do_group
|
|
// FOR NAME IN separator do_group
|
|
// FOR NAME IN wordlist separator do_group
|
|
|
|
if (peek().type != Token::Type::For)
|
|
return nullptr;
|
|
|
|
auto start_position = consume().position.value_or(empty_position());
|
|
|
|
String name;
|
|
Optional<AST::Position> name_position;
|
|
if (peek().type == Token::Type::VariableName) {
|
|
name_position = peek().position;
|
|
name = consume().value;
|
|
} else {
|
|
name = "it"_string;
|
|
error(peek(), "Expected a variable name, not {}", peek().type_name());
|
|
}
|
|
|
|
auto saw_newline = false;
|
|
while (peek().type == Token::Type::Newline) {
|
|
saw_newline = true;
|
|
skip();
|
|
}
|
|
|
|
auto saw_in = false;
|
|
Optional<AST::Position> in_kw_position;
|
|
RefPtr<AST::Node> iterated_expression;
|
|
|
|
if (peek().type == Token::Type::In) {
|
|
saw_in = true;
|
|
in_kw_position = peek().position;
|
|
skip();
|
|
} else if (!saw_newline) {
|
|
error(peek(), "Expected 'in' or a newline, not {}", peek().type_name());
|
|
} else {
|
|
// FOR NAME newline+ do_group //-> FOR NAME IN "$@" newline+ do_group
|
|
iterated_expression = TRY(Parser { "\"$@\""_string }.parse_word());
|
|
}
|
|
|
|
if (saw_in && !saw_newline) {
|
|
if (auto list = parse_word_list())
|
|
iterated_expression = reexpand(peek().position.value_or(empty_position()), list.release_nonnull());
|
|
}
|
|
|
|
if (saw_in) {
|
|
if (peek().type == Token::Type::Semicolon || peek().type == Token::Type::Newline)
|
|
skip();
|
|
else
|
|
error(peek(), "Expected a semicolon, not {}", peek().type_name());
|
|
}
|
|
|
|
while (peek().type == Token::Type::Newline)
|
|
skip();
|
|
|
|
auto body = TRY(parse_do_group());
|
|
return AST::make_ref_counted<AST::ForLoop>(
|
|
start_position.with_end(peek().position.value_or(empty_position())),
|
|
AST::NameWithPosition { name, name_position.value_or(empty_position()) },
|
|
Optional<AST::NameWithPosition> {},
|
|
move(iterated_expression),
|
|
move(body),
|
|
move(in_kw_position),
|
|
Optional<AST::Position> {});
|
|
}
|
|
|
|
RefPtr<AST::Node> Parser::parse_word_list(AllowNewlines allow_newlines)
|
|
{
|
|
Vector<NonnullRefPtr<AST::Node>> nodes;
|
|
|
|
auto start_position = peek().position.value_or(empty_position());
|
|
|
|
if (allow_newlines == AllowNewlines::Yes) {
|
|
while (peek().type == Token::Type::Newline)
|
|
skip();
|
|
}
|
|
|
|
for (; peek().type == Token::Type::Word;) {
|
|
auto word = TRY_OR_THROW_PARSE_ERROR_AT(parse_word(), start_position);
|
|
nodes.append(word.release_nonnull());
|
|
if (allow_newlines == AllowNewlines::Yes) {
|
|
while (peek().type == Token::Type::Newline)
|
|
skip();
|
|
}
|
|
}
|
|
|
|
return make_ref_counted<AST::ListConcatenate>(
|
|
start_position.with_end(peek().position.value_or(empty_position())),
|
|
move(nodes));
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_word()
|
|
{
|
|
if (peek().type != Token::Type::Word)
|
|
return nullptr;
|
|
|
|
auto token = consume();
|
|
RefPtr<AST::Node> word;
|
|
|
|
enum class Quote {
|
|
None,
|
|
Single,
|
|
Double,
|
|
} in_quote { Quote::None };
|
|
|
|
auto append_bareword = [&](StringView string) -> ErrorOr<void> {
|
|
if (!word && string.starts_with('~')) {
|
|
GenericLexer lexer { string };
|
|
lexer.ignore();
|
|
auto user = lexer.consume_while(is_ascii_alphanumeric);
|
|
string = lexer.remaining();
|
|
|
|
word = make_ref_counted<AST::Tilde>(token.position.value_or(empty_position()), TRY(String::from_utf8(user)));
|
|
}
|
|
|
|
if (string.is_empty())
|
|
return {};
|
|
|
|
auto node = make_ref_counted<AST::BarewordLiteral>(
|
|
token.position.value_or(empty_position()),
|
|
TRY(String::from_utf8(string)));
|
|
|
|
if (word) {
|
|
word = make_ref_counted<AST::Juxtaposition>(
|
|
word->position().with_end(token.position.value_or(empty_position())),
|
|
*word,
|
|
move(node),
|
|
AST::Juxtaposition::Mode::StringExpand);
|
|
} else {
|
|
word = move(node);
|
|
}
|
|
|
|
return {};
|
|
};
|
|
|
|
auto append_string_literal = [&](StringView string) -> ErrorOr<void> {
|
|
auto node = make_ref_counted<AST::StringLiteral>(
|
|
token.position.value_or(empty_position()),
|
|
TRY(String::from_utf8(string)),
|
|
AST::StringLiteral::EnclosureType::SingleQuotes);
|
|
|
|
if (word) {
|
|
word = make_ref_counted<AST::Juxtaposition>(
|
|
word->position().with_end(token.position.value_or(empty_position())),
|
|
*word,
|
|
move(node),
|
|
AST::Juxtaposition::Mode::StringExpand);
|
|
} else {
|
|
word = move(node);
|
|
}
|
|
|
|
return {};
|
|
};
|
|
|
|
auto append_string_part = [&](StringView string) -> ErrorOr<void> {
|
|
auto node = make_ref_counted<AST::StringLiteral>(
|
|
token.position.value_or(empty_position()),
|
|
TRY(String::from_utf8(string)),
|
|
AST::StringLiteral::EnclosureType::DoubleQuotes);
|
|
|
|
if (word) {
|
|
word = make_ref_counted<AST::Juxtaposition>(
|
|
word->position().with_end(token.position.value_or(empty_position())),
|
|
*word,
|
|
move(node),
|
|
AST::Juxtaposition::Mode::StringExpand);
|
|
} else {
|
|
word = move(node);
|
|
}
|
|
|
|
return {};
|
|
};
|
|
|
|
auto append_arithmetic_expansion = [&](ResolvedArithmeticExpansion const& x) -> ErrorOr<void> {
|
|
auto node = immediate(
|
|
"math"_string,
|
|
token.position.value_or(empty_position()),
|
|
reexpand(
|
|
token.position.value_or(empty_position()),
|
|
make_ref_counted<AST::StringLiteral>(
|
|
token.position.value_or(empty_position()),
|
|
x.source_expression,
|
|
AST::StringLiteral::EnclosureType::DoubleQuotes)));
|
|
|
|
if (word) {
|
|
word = make_ref_counted<AST::Juxtaposition>(
|
|
word->position().with_end(token.position.value_or(empty_position())),
|
|
*word,
|
|
move(node),
|
|
AST::Juxtaposition::Mode::StringExpand);
|
|
} else {
|
|
word = move(node);
|
|
}
|
|
|
|
return {};
|
|
};
|
|
|
|
auto append_parameter_expansion = [&](ResolvedParameterExpansion const& x) -> ErrorOr<void> {
|
|
StringView immediate_function_name;
|
|
RefPtr<AST::Node> node;
|
|
switch (x.op) {
|
|
case ResolvedParameterExpansion::Op::UseDefaultValue:
|
|
immediate_function_name = "value_or_default"sv;
|
|
break;
|
|
case ResolvedParameterExpansion::Op::AssignDefaultValue:
|
|
immediate_function_name = "assign_default"sv;
|
|
break;
|
|
case ResolvedParameterExpansion::Op::IndicateErrorIfEmpty:
|
|
immediate_function_name = "error_if_empty"sv;
|
|
break;
|
|
case ResolvedParameterExpansion::Op::UseAlternativeValue:
|
|
immediate_function_name = "null_or_alternative"sv;
|
|
break;
|
|
case ResolvedParameterExpansion::Op::UseDefaultValueIfUnset:
|
|
immediate_function_name = "defined_value_or_default"sv;
|
|
break;
|
|
case ResolvedParameterExpansion::Op::AssignDefaultValueIfUnset:
|
|
immediate_function_name = "assign_defined_default"sv;
|
|
break;
|
|
case ResolvedParameterExpansion::Op::IndicateErrorIfUnset:
|
|
immediate_function_name = "error_if_unset"sv;
|
|
break;
|
|
case ResolvedParameterExpansion::Op::UseAlternativeValueIfUnset:
|
|
immediate_function_name = "null_if_unset_or_alternative"sv;
|
|
break;
|
|
case ResolvedParameterExpansion::Op::RemoveLargestSuffixByPattern:
|
|
// FIXME: Implement this
|
|
case ResolvedParameterExpansion::Op::RemoveSmallestSuffixByPattern:
|
|
immediate_function_name = "remove_suffix"sv;
|
|
break;
|
|
case ResolvedParameterExpansion::Op::RemoveLargestPrefixByPattern:
|
|
// FIXME: Implement this
|
|
case ResolvedParameterExpansion::Op::RemoveSmallestPrefixByPattern:
|
|
immediate_function_name = "remove_prefix"sv;
|
|
break;
|
|
case ResolvedParameterExpansion::Op::StringLength:
|
|
immediate_function_name = "length_of_variable"sv;
|
|
break;
|
|
case ResolvedParameterExpansion::Op::GetPositionalParameter:
|
|
case ResolvedParameterExpansion::Op::GetVariable:
|
|
node = make_ref_counted<AST::SimpleVariable>(
|
|
token.position.value_or(empty_position()),
|
|
x.parameter);
|
|
break;
|
|
case ResolvedParameterExpansion::Op::GetLastBackgroundPid:
|
|
node = make_ref_counted<AST::SyntaxError>(
|
|
token.position.value_or(empty_position()),
|
|
"$! not implemented"_string);
|
|
break;
|
|
case ResolvedParameterExpansion::Op::GetPositionalParameterList:
|
|
node = make_ref_counted<AST::SpecialVariable>(
|
|
token.position.value_or(empty_position()),
|
|
'*');
|
|
break;
|
|
case ResolvedParameterExpansion::Op::GetCurrentOptionFlags:
|
|
node = make_ref_counted<AST::SyntaxError>(
|
|
token.position.value_or(empty_position()),
|
|
"The current option flags are not available in parameter expansions"_string);
|
|
break;
|
|
case ResolvedParameterExpansion::Op::GetPositionalParameterCount:
|
|
node = make_ref_counted<AST::SpecialVariable>(
|
|
token.position.value_or(empty_position()),
|
|
'#');
|
|
break;
|
|
case ResolvedParameterExpansion::Op::GetLastExitStatus:
|
|
node = make_ref_counted<AST::SpecialVariable>(
|
|
token.position.value_or(empty_position()),
|
|
'?');
|
|
break;
|
|
case ResolvedParameterExpansion::Op::GetPositionalParameterListAsString:
|
|
node = strip_execute(::Shell::Parser { "${join \"${defined_value_or_default IFS ' '}\" $*}"sv }.parse());
|
|
break;
|
|
case ResolvedParameterExpansion::Op::GetShellProcessId:
|
|
node = make_ref_counted<AST::SpecialVariable>(
|
|
token.position.value_or(empty_position()),
|
|
'$');
|
|
break;
|
|
}
|
|
|
|
if (!node) {
|
|
Vector<NonnullRefPtr<AST::Node>> arguments;
|
|
arguments.append(make_ref_counted<AST::BarewordLiteral>(
|
|
token.position.value_or(empty_position()),
|
|
x.parameter));
|
|
|
|
if (!x.argument.is_empty()) {
|
|
// dbgln("Will parse {}", x.argument);
|
|
arguments.append(*TRY(Parser { x.argument }.parse_word()));
|
|
}
|
|
|
|
node = immediate(TRY(String::from_utf8(immediate_function_name)), token.position.value_or(empty_position()), move(arguments));
|
|
}
|
|
|
|
if (x.expand == ResolvedParameterExpansion::Expand::Word)
|
|
node = reexpand(token.position.value_or(empty_position()), node.release_nonnull());
|
|
|
|
if (word) {
|
|
word = make_ref_counted<AST::Juxtaposition>(
|
|
word->position().with_end(token.position.value_or(empty_position())),
|
|
*word,
|
|
node.release_nonnull(),
|
|
AST::Juxtaposition::Mode::StringExpand);
|
|
} else {
|
|
word = move(node);
|
|
}
|
|
|
|
return {};
|
|
};
|
|
|
|
auto append_command_expansion = [&](ResolvedCommandExpansion const& x) -> ErrorOr<void> {
|
|
if (!x.command)
|
|
return {};
|
|
|
|
RefPtr<AST::Execute> execute_node;
|
|
|
|
if (x.command->is_execute()) {
|
|
execute_node = const_cast<AST::Execute&>(static_cast<AST::Execute const&>(*x.command));
|
|
execute_node->capture_stdout();
|
|
} else {
|
|
execute_node = make_ref_counted<AST::Execute>(
|
|
word ? word->position() : empty_position(),
|
|
*x.command,
|
|
true);
|
|
}
|
|
|
|
if (word) {
|
|
word = make_ref_counted<AST::Juxtaposition>(
|
|
word->position(),
|
|
*word,
|
|
execute_node.release_nonnull(),
|
|
AST::Juxtaposition::Mode::StringExpand);
|
|
} else {
|
|
word = move(execute_node);
|
|
}
|
|
|
|
return {};
|
|
};
|
|
|
|
auto append_string = [&](StringView string) -> ErrorOr<void> {
|
|
if (string.is_empty())
|
|
return {};
|
|
|
|
Optional<size_t> run_start;
|
|
auto escape = false;
|
|
for (size_t i = 0; i < string.length(); ++i) {
|
|
auto ch = string[i];
|
|
switch (ch) {
|
|
case '\\':
|
|
if (!escape && i + 1 < string.length()) {
|
|
if (run_start.has_value())
|
|
TRY(append_string_literal(string.substring_view(*run_start, i - *run_start)));
|
|
run_start = i + 1;
|
|
|
|
if (is_one_of(string[i + 1], '"', '\'', '$', '`', '\\')) {
|
|
escape = in_quote != Quote::Single;
|
|
continue;
|
|
}
|
|
}
|
|
break;
|
|
case '\'':
|
|
if (in_quote == Quote::Single) {
|
|
in_quote = Quote::None;
|
|
if (run_start.has_value())
|
|
TRY(append_string_literal(string.substring_view(*run_start, i - *run_start)));
|
|
run_start = i + 1;
|
|
continue;
|
|
}
|
|
if (in_quote == Quote::Double) {
|
|
escape = false;
|
|
continue;
|
|
}
|
|
[[fallthrough]];
|
|
case '"':
|
|
if (ch == '\'' && in_quote == Quote::Single) {
|
|
escape = false;
|
|
continue;
|
|
}
|
|
if (!escape) {
|
|
if (ch == '"' && in_quote == Quote::Double) {
|
|
in_quote = Quote::None;
|
|
if (run_start.has_value())
|
|
TRY(append_string_part(string.substring_view(*run_start, i - *run_start)));
|
|
run_start = i + 1;
|
|
continue;
|
|
}
|
|
if (run_start.has_value())
|
|
TRY(append_bareword(string.substring_view(*run_start, i - *run_start)));
|
|
in_quote = ch == '\'' ? Quote::Single : Quote::Double;
|
|
run_start = i + 1;
|
|
}
|
|
escape = false;
|
|
[[fallthrough]];
|
|
default:
|
|
if (!run_start.has_value())
|
|
run_start = i;
|
|
escape = false;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (run_start.has_value())
|
|
TRY(append_bareword(string.substring_view(*run_start, string.length() - *run_start)));
|
|
|
|
return {};
|
|
};
|
|
|
|
if (!token.resolved_expansions.is_empty())
|
|
dbgln_if(SHELL_POSIX_PARSER_DEBUG, "Expanding '{}' with {} expansion entries", token.value, token.resolved_expansions.size());
|
|
|
|
size_t current_offset = 0;
|
|
auto value_bytes = token.value.bytes_as_string_view();
|
|
for (auto& expansion : token.resolved_expansions) {
|
|
TRY(expansion.visit(
|
|
[&](ResolvedParameterExpansion const& x) -> ErrorOr<void> {
|
|
dbgln_if(SHELL_POSIX_PARSER_DEBUG, " Expanding '{}' ({}+{})", x.to_byte_string(), x.range.start, x.range.length);
|
|
if (x.range.start >= value_bytes.length()) {
|
|
dbgln("Parameter expansion range {}-{} is out of bounds for '{}'", x.range.start, x.range.length, value_bytes);
|
|
return {};
|
|
}
|
|
|
|
if (x.range.start != current_offset) {
|
|
TRY(append_string(value_bytes.substring_view(current_offset, x.range.start - current_offset)));
|
|
current_offset = x.range.start;
|
|
}
|
|
current_offset += x.range.length;
|
|
return append_parameter_expansion(x);
|
|
},
|
|
[&](ResolvedArithmeticExpansion const& x) -> ErrorOr<void> {
|
|
if (x.range.start >= value_bytes.length()) {
|
|
dbgln("Parameter expansion range {}-{} is out of bounds for '{}'", x.range.start, x.range.length, value_bytes);
|
|
return {};
|
|
}
|
|
|
|
if (x.range.start != current_offset) {
|
|
TRY(append_string(value_bytes.substring_view(current_offset, x.range.start - current_offset)));
|
|
current_offset = x.range.start;
|
|
}
|
|
current_offset += x.range.length;
|
|
return append_arithmetic_expansion(x);
|
|
},
|
|
[&](ResolvedCommandExpansion const& x) -> ErrorOr<void> {
|
|
if (x.range.start >= value_bytes.length()) {
|
|
dbgln("Parameter expansion range {}-{} is out of bounds for '{}'", x.range.start, x.range.length, value_bytes);
|
|
return {};
|
|
}
|
|
|
|
if (x.range.start != current_offset) {
|
|
TRY(append_string(value_bytes.substring_view(current_offset, x.range.start - current_offset)));
|
|
current_offset = x.range.start;
|
|
}
|
|
current_offset += x.range.length;
|
|
return append_command_expansion(x);
|
|
}));
|
|
}
|
|
|
|
if (current_offset > value_bytes.length()) {
|
|
dbgln("Parameter expansion range {}- is out of bounds for '{}'", current_offset, value_bytes);
|
|
return word;
|
|
}
|
|
|
|
if (current_offset != value_bytes.length())
|
|
TRY(append_string(value_bytes.substring_view(current_offset)));
|
|
|
|
return word;
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_bash_like_list()
|
|
{
|
|
if (peek().type != Token::Type::OpenParen)
|
|
return nullptr;
|
|
|
|
auto start_position = peek().position.value_or(empty_position());
|
|
consume();
|
|
|
|
auto list = parse_word_list(AllowNewlines::Yes);
|
|
|
|
if (peek().type != Token::Type::CloseParen) {
|
|
return make_ref_counted<AST::SyntaxError>(
|
|
peek().position.value_or(empty_position()),
|
|
TRY(String::formatted("Expected ')', not {}", peek().type_name())));
|
|
}
|
|
|
|
consume();
|
|
|
|
if (list)
|
|
list->position() = start_position.with_end(peek().position.value_or(empty_position()));
|
|
else
|
|
list = make_ref_counted<AST::ListConcatenate>(start_position.with_end(peek().position.value_or(empty_position())), Vector<NonnullRefPtr<AST::Node>> {});
|
|
|
|
return list;
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_do_group()
|
|
{
|
|
if (peek().type != Token::Type::Do) {
|
|
return make_ref_counted<AST::SyntaxError>(
|
|
peek().position.value_or(empty_position()),
|
|
TRY(String::formatted("Expected 'do', not {}", peek().type_name())));
|
|
}
|
|
|
|
consume();
|
|
|
|
auto list = TRY(parse_compound_list());
|
|
|
|
RefPtr<AST::SyntaxError> error;
|
|
if (peek().type != Token::Type::Done) {
|
|
error = make_ref_counted<AST::SyntaxError>(
|
|
peek().position.value_or(empty_position()),
|
|
TRY(String::formatted("Expected 'done', not {}", peek().type_name())));
|
|
} else {
|
|
consume();
|
|
}
|
|
|
|
if (error) {
|
|
if (list)
|
|
list->set_is_syntax_error(*error);
|
|
else
|
|
list = error;
|
|
}
|
|
|
|
return make_ref_counted<AST::Execute>(list->position(), *list);
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_simple_command()
|
|
{
|
|
auto start_position = peek().position.value_or(empty_position());
|
|
|
|
Vector<String> definitions;
|
|
HashMap<String, NonnullRefPtr<AST::Node>> list_assignments;
|
|
Vector<NonnullRefPtr<AST::Node>> nodes;
|
|
|
|
for (;;) {
|
|
if (auto io_redirect = TRY(parse_io_redirect()))
|
|
nodes.append(*io_redirect);
|
|
else
|
|
break;
|
|
}
|
|
|
|
while (is_one_of(peek().type, Token::Type::ListAssignmentWord, Token::Type::AssignmentWord)) {
|
|
if (peek().type == Token::Type::ListAssignmentWord) {
|
|
auto token = consume();
|
|
auto value = TRY(parse_bash_like_list());
|
|
if (!value)
|
|
return make_ref_counted<AST::SyntaxError>(
|
|
token.position.value_or(empty_position()),
|
|
TRY(String::formatted("Expected a list literal after '{}', not {}", token.value, peek().type_name())));
|
|
|
|
list_assignments.set(token.value, value.release_nonnull());
|
|
continue;
|
|
}
|
|
|
|
definitions.append(peek().value);
|
|
|
|
if (nodes.is_empty()) {
|
|
// run_with_env -e*(assignments) -- (command)
|
|
nodes.append(make_ref_counted<AST::BarewordLiteral>(
|
|
empty_position(),
|
|
"run_with_env"_string));
|
|
}
|
|
|
|
auto position = peek().position.value_or(empty_position());
|
|
nodes.append(reexpand(position, make_ref_counted<AST::StringLiteral>(position, TRY(String::formatted("-e{}", consume().value)), AST::StringLiteral::EnclosureType::DoubleQuotes)));
|
|
}
|
|
|
|
if (!definitions.is_empty()) {
|
|
nodes.append(make_ref_counted<AST::BarewordLiteral>(
|
|
empty_position(),
|
|
"--"_string));
|
|
}
|
|
|
|
// WORD or io_redirect: IO_NUMBER or io_file
|
|
if (!is_one_of(peek().type,
|
|
Token::Type::Word, Token::Type::IoNumber,
|
|
Token::Type::Less, Token::Type::LessAnd, Token::Type::Great, Token::Type::GreatAnd,
|
|
Token::Type::DoubleGreat, Token::Type::LessGreat, Token::Type::Clobber)) {
|
|
if (!definitions.is_empty() || !list_assignments.is_empty()) {
|
|
Vector<AST::VariableDeclarations::Variable> variables;
|
|
for (auto& definition : definitions) {
|
|
auto equal_offset = definition.find_byte_offset('=');
|
|
auto split_offset = equal_offset.value_or(definition.bytes().size());
|
|
auto name = make_ref_counted<AST::BarewordLiteral>(
|
|
empty_position(),
|
|
TRY(definition.substring_from_byte_offset_with_shared_superstring(0, split_offset)));
|
|
|
|
auto position = peek().position.value_or(empty_position());
|
|
auto expanded_value = reexpand(position, make_ref_counted<AST::StringLiteral>(position, TRY(definition.substring_from_byte_offset_with_shared_superstring(split_offset + 1)), AST::StringLiteral::EnclosureType::DoubleQuotes));
|
|
|
|
variables.append({ move(name), move(expanded_value) });
|
|
}
|
|
for (auto& [key, value] : list_assignments) {
|
|
auto equal_offset = key.find_byte_offset('=');
|
|
auto split_offset = equal_offset.value_or(key.bytes().size());
|
|
auto name = make_ref_counted<AST::BarewordLiteral>(
|
|
empty_position(),
|
|
TRY(key.substring_from_byte_offset_with_shared_superstring(0, split_offset)));
|
|
|
|
variables.append({ move(name), move(value) });
|
|
}
|
|
|
|
return make_ref_counted<AST::VariableDeclarations>(empty_position(), move(variables));
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
if (!list_assignments.is_empty()) {
|
|
return make_ref_counted<AST::SyntaxError>(
|
|
peek().position.value_or(empty_position()),
|
|
"List assignments are not allowed as a command prefix"_string);
|
|
}
|
|
|
|
// auto first = true;
|
|
for (;;) {
|
|
if (peek().type == Token::Type::Word) {
|
|
auto new_word = TRY(parse_word());
|
|
if (!new_word)
|
|
break;
|
|
|
|
nodes.append(new_word.release_nonnull());
|
|
} else if (auto io_redirect = TRY(parse_io_redirect())) {
|
|
nodes.append(io_redirect.release_nonnull());
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
auto node = make_ref_counted<AST::ListConcatenate>(
|
|
start_position.with_end(peek().position.value_or(empty_position())),
|
|
move(nodes));
|
|
|
|
return node;
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_io_redirect()
|
|
{
|
|
auto start_position = peek().position.value_or(empty_position());
|
|
auto start_index = m_token_index;
|
|
|
|
// io_redirect: IO_NUMBER? io_file | IO_NUMBER? io_here
|
|
Optional<int> io_number;
|
|
|
|
if (peek().type == Token::Type::IoNumber)
|
|
io_number = consume().value.to_number<int>();
|
|
|
|
if (auto io_file = TRY(parse_io_file(start_position, io_number)))
|
|
return io_file;
|
|
|
|
if (auto io_here = TRY(parse_io_here(start_position, io_number)))
|
|
return io_here;
|
|
|
|
m_token_index = start_index;
|
|
return nullptr;
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_io_here(AST::Position start_position, Optional<int> fd)
|
|
{
|
|
// io_here: IO_NUMBER? (DLESS | DLESSDASH) WORD
|
|
auto io_operator = peek().type;
|
|
if (!is_one_of(io_operator, Token::Type::DoubleLess, Token::Type::DoubleLessDash))
|
|
return nullptr;
|
|
|
|
auto io_operator_token = consume();
|
|
|
|
auto redirection_fd = fd.value_or(0);
|
|
|
|
auto end_keyword = consume();
|
|
if (!is_one_of(end_keyword.type, Token::Type::Word, Token::Type::Token))
|
|
return make_ref_counted<AST::SyntaxError>(io_operator_token.position.value_or(start_position), "Expected a heredoc keyword"_string, true);
|
|
|
|
auto [end_keyword_text, allow_interpolation] = Lexer::process_heredoc_key(end_keyword);
|
|
RefPtr<AST::SyntaxError> error;
|
|
|
|
auto position = start_position.with_end(peek().position.value_or(empty_position()));
|
|
auto result = make_ref_counted<AST::Heredoc>(
|
|
position,
|
|
end_keyword_text,
|
|
allow_interpolation,
|
|
io_operator == Token::Type::DoubleLessDash,
|
|
Optional<int> { redirection_fd });
|
|
|
|
m_unprocessed_heredoc_entries.set(end_keyword_text, result);
|
|
|
|
if (error)
|
|
result->set_is_syntax_error(*error);
|
|
|
|
return result;
|
|
}
|
|
|
|
ErrorOr<RefPtr<AST::Node>> Parser::parse_io_file(AST::Position start_position, Optional<int> fd)
|
|
{
|
|
auto start_index = m_token_index;
|
|
|
|
// io_file = (LESS | LESSAND | GREAT | GREATAND | DGREAT | LESSGREAT | CLOBBER) WORD
|
|
auto io_operator = peek().type;
|
|
if (!is_one_of(io_operator,
|
|
Token::Type::Less, Token::Type::LessAnd, Token::Type::Great, Token::Type::GreatAnd,
|
|
Token::Type::DoubleGreat, Token::Type::LessGreat, Token::Type::Clobber))
|
|
return nullptr;
|
|
|
|
auto io_operator_token = consume();
|
|
|
|
RefPtr<AST::Node> word;
|
|
if (peek().type == Token::Type::IoNumber) {
|
|
auto token = consume();
|
|
word = make_ref_counted<AST::BarewordLiteral>(
|
|
token.position.value_or(empty_position()),
|
|
token.value);
|
|
} else {
|
|
word = TRY(parse_word());
|
|
}
|
|
|
|
if (!word) {
|
|
m_token_index = start_index;
|
|
return nullptr;
|
|
}
|
|
|
|
auto position = start_position.with_end(peek().position.value_or(empty_position()));
|
|
switch (io_operator) {
|
|
case Token::Type::Less:
|
|
return make_ref_counted<AST::ReadRedirection>(
|
|
position,
|
|
fd.value_or(0),
|
|
word.release_nonnull());
|
|
case Token::Type::Clobber:
|
|
// FIXME: Add support for clobber (and 'noclobber')
|
|
case Token::Type::Great:
|
|
return make_ref_counted<AST::WriteRedirection>(
|
|
position,
|
|
fd.value_or(1),
|
|
word.release_nonnull());
|
|
case Token::Type::DoubleGreat:
|
|
return make_ref_counted<AST::WriteAppendRedirection>(
|
|
position,
|
|
fd.value_or(1),
|
|
word.release_nonnull());
|
|
case Token::Type::LessGreat:
|
|
return make_ref_counted<AST::ReadWriteRedirection>(
|
|
position,
|
|
fd.value_or(0),
|
|
word.release_nonnull());
|
|
case Token::Type::LessAnd:
|
|
case Token::Type::GreatAnd: {
|
|
auto is_less = io_operator == Token::Type::LessAnd;
|
|
auto source_fd = fd.value_or(is_less ? 0 : 1);
|
|
if (word->is_bareword()) {
|
|
auto text = static_ptr_cast<AST::BarewordLiteral>(word)->text();
|
|
if (!is_less && text == "-"sv) {
|
|
return make_ref_counted<AST::CloseFdRedirection>(
|
|
position,
|
|
source_fd);
|
|
}
|
|
|
|
auto maybe_target_fd = text.to_number<int>();
|
|
if (maybe_target_fd.has_value()) {
|
|
auto target_fd = maybe_target_fd.release_value();
|
|
if (is_less)
|
|
swap(source_fd, target_fd);
|
|
|
|
return make_ref_counted<AST::Fd2FdRedirection>(
|
|
position,
|
|
source_fd,
|
|
target_fd);
|
|
}
|
|
}
|
|
if (is_less) {
|
|
return make_ref_counted<AST::ReadRedirection>(
|
|
position,
|
|
source_fd,
|
|
word.release_nonnull());
|
|
}
|
|
|
|
return make_ref_counted<AST::WriteRedirection>(
|
|
position,
|
|
source_fd,
|
|
word.release_nonnull());
|
|
}
|
|
default:
|
|
VERIFY_NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
}
|