Files
serenity/Userland/Libraries/LibShell/Formatter.cpp
Liav A. d46be35f3f Userland: Move Shell code to be in a library directory
This change ensures that users can use LibShell easily understand this
library now, because we have an actual library directory.

In addition to that, we move the test scripts to Tests/LibShell, to
match the usual pattern of putting test-related files in the Tests/
directory.
2024-10-04 10:56:27 +02:00

916 lines
25 KiB
C++

/*
* Copyright (c) 2020, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "Formatter.h"
#include "AST.h"
#include "Parser.h"
#include "PosixParser.h"
#include <AK/Hex.h>
#include <AK/ScopedValueRollback.h>
#include <AK/TemporaryChange.h>
namespace Shell {
ByteString Formatter::format()
{
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;
if (!node)
return ByteString();
if (node->is_syntax_error())
return m_source;
if (m_cursor >= 0) {
auto hit_test = node->hit_test_position(m_cursor);
if (hit_test.matching_node)
m_hit_node = hit_test.matching_node.ptr();
else
m_hit_node = nullptr;
}
m_parent_node = nullptr;
node->visit(*this);
VERIFY(m_builders.size() == 1);
auto string = current_builder().string_view();
if (!string.ends_with(' '))
current_builder().append(m_trivia);
return current_builder().to_byte_string();
}
void Formatter::with_added_indent(int indent, Function<void()> callback)
{
TemporaryChange indent_change { m_current_indent, m_current_indent + indent };
callback();
}
void Formatter::in_new_block(Function<void()> callback)
{
current_builder().append('{');
with_added_indent(1, [&] {
insert_separator();
callback();
});
insert_separator();
current_builder().append('}');
}
ByteString Formatter::in_new_builder(Function<void()> callback, StringBuilder new_builder)
{
m_builders.append(move(new_builder));
callback();
return m_builders.take_last().to_byte_string();
}
void Formatter::test_and_update_output_cursor(const AST::Node* node)
{
if (!node)
return;
if (node != m_hit_node)
return;
m_output_cursor = current_builder().length() + m_cursor - node->position().start_offset;
}
void Formatter::visited(const AST::Node* node)
{
m_last_visited_node = node;
}
void Formatter::will_visit(const AST::Node* node)
{
if (!m_last_visited_node)
return;
if (!node)
return;
auto direct_sequence_child = !m_parent_node || m_parent_node->kind() == AST::Node::Kind::Sequence;
if (direct_sequence_child && node->kind() != AST::Node::Kind::Sequence && node->kind() != AST::Node::Kind::Execute) {
// Collapse more than one empty line to a single one.
if (node->position().start_line.line_number - m_last_visited_node->position().end_line.line_number > 1)
insert_separator();
}
}
void Formatter::insert_separator(bool escaped)
{
if (escaped)
current_builder().append('\\');
current_builder().append('\n');
if (!escaped && !m_heredocs_to_append_after_sequence.is_empty()) {
for (auto& entry : m_heredocs_to_append_after_sequence) {
current_builder().append(entry);
}
m_heredocs_to_append_after_sequence.clear();
}
insert_indent();
}
void Formatter::insert_indent()
{
for (size_t i = 0; i < m_current_indent; ++i)
current_builder().append(" "sv);
}
void Formatter::visit(const AST::PathRedirectionNode* node)
{
will_visit(node);
test_and_update_output_cursor(node);
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
NodeVisitor::visit(node);
visited(node);
}
void Formatter::visit(const AST::And* node)
{
will_visit(node);
test_and_update_output_cursor(node);
auto should_indent = m_parent_node && m_parent_node->kind() != AST::Node::Kind::And;
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
with_added_indent(should_indent ? 1 : 0, [&] {
node->left()->visit(*this);
current_builder().append(' ');
insert_separator(true);
current_builder().append("&& "sv);
node->right()->visit(*this);
});
visited(node);
}
void Formatter::visit(const AST::ListConcatenate* node)
{
will_visit(node);
test_and_update_output_cursor(node);
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
auto first = true;
for (auto& subnode : node->list()) {
if (!first)
current_builder().append(' ');
first = false;
subnode->visit(*this);
}
visited(node);
}
void Formatter::visit(const AST::Background* node)
{
will_visit(node);
test_and_update_output_cursor(node);
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
NodeVisitor::visit(node);
current_builder().append(" &"sv);
visited(node);
}
void Formatter::visit(const AST::BarewordLiteral* node)
{
will_visit(node);
test_and_update_output_cursor(node);
current_builder().append(node->text());
visited(node);
}
void Formatter::visit(const AST::BraceExpansion* node)
{
will_visit(node);
test_and_update_output_cursor(node);
if (!m_parent_node || m_parent_node->kind() != AST::Node::Kind::Slice)
current_builder().append('{');
{
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
bool first = true;
for (auto& entry : node->entries()) {
if (!first)
current_builder().append(',');
first = false;
entry->visit(*this);
}
}
if (!m_parent_node || m_parent_node->kind() != AST::Node::Kind::Slice)
current_builder().append('}');
visited(node);
}
void Formatter::visit(const AST::CastToCommand* node)
{
will_visit(node);
test_and_update_output_cursor(node);
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
NodeVisitor::visit(node);
visited(node);
}
void Formatter::visit(const AST::CastToList* node)
{
will_visit(node);
test_and_update_output_cursor(node);
current_builder().append('(');
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
NodeVisitor::visit(node);
current_builder().append(')');
visited(node);
}
void Formatter::visit(const AST::CloseFdRedirection* node)
{
will_visit(node);
test_and_update_output_cursor(node);
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
current_builder().appendff("{}>&-", node->fd());
visited(node);
}
void Formatter::visit(const AST::CommandLiteral*)
{
VERIFY_NOT_REACHED();
}
void Formatter::visit(const AST::Comment* node)
{
will_visit(node);
test_and_update_output_cursor(node);
current_builder().append("#"sv);
current_builder().append(node->text());
visited(node);
}
void Formatter::visit(const AST::ContinuationControl* node)
{
will_visit(node);
test_and_update_output_cursor(node);
if (node->continuation_kind() == AST::ContinuationControl::Break)
current_builder().append("break"sv);
else if (node->continuation_kind() == AST::ContinuationControl::Continue)
current_builder().append("continue"sv);
else
VERIFY_NOT_REACHED();
visited(node);
}
void Formatter::visit(const AST::DynamicEvaluate* node)
{
will_visit(node);
test_and_update_output_cursor(node);
current_builder().append('$');
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
NodeVisitor::visit(node);
visited(node);
}
void Formatter::visit(const AST::DoubleQuotedString* node)
{
will_visit(node);
test_and_update_output_cursor(node);
auto not_in_heredoc = m_parent_node->kind() != AST::Node::Kind::Heredoc;
if (not_in_heredoc)
current_builder().append("\""sv);
TemporaryChange quotes { m_options.in_double_quotes, true };
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
NodeVisitor::visit(node);
if (not_in_heredoc)
current_builder().append("\""sv);
visited(node);
}
void Formatter::visit(const AST::Fd2FdRedirection* node)
{
will_visit(node);
test_and_update_output_cursor(node);
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
current_builder().appendff("{}>&{}", node->source_fd(), node->dest_fd());
if (m_hit_node == node)
++m_output_cursor;
visited(node);
}
void Formatter::visit(const AST::FunctionDeclaration* node)
{
will_visit(node);
test_and_update_output_cursor(node);
current_builder().append(node->name().name);
current_builder().append('(');
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
auto first = true;
for (auto& arg : node->arguments()) {
if (!first)
current_builder().append(' ');
first = false;
current_builder().append(arg.name);
}
current_builder().append(") "sv);
in_new_block([&] {
if (node->block())
node->block()->visit(*this);
});
visited(node);
}
void Formatter::visit(const AST::ForLoop* node)
{
will_visit(node);
test_and_update_output_cursor(node);
auto is_loop = node->iterated_expression().is_null();
current_builder().append(is_loop ? "loop"sv : "for "sv);
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
if (!is_loop) {
if (node->index_variable().has_value()) {
current_builder().append("index "sv);
current_builder().append(node->index_variable()->name);
current_builder().append(" "sv);
}
if (node->variable().has_value() && node->variable()->name != "it") {
current_builder().append(node->variable()->name);
current_builder().append(" in "sv);
}
node->iterated_expression()->visit(*this);
}
current_builder().append(' ');
in_new_block([&] {
if (node->block())
node->block()->visit(*this);
});
visited(node);
}
void Formatter::visit(const AST::Glob* node)
{
will_visit(node);
test_and_update_output_cursor(node);
current_builder().append(node->text());
visited(node);
}
void Formatter::visit(const AST::Heredoc* node)
{
will_visit(node);
test_and_update_output_cursor(node);
current_builder().append("<<"sv);
if (node->deindent())
current_builder().append('~');
else
current_builder().append('-');
if (node->allow_interpolation())
current_builder().appendff("{}", node->end());
else
current_builder().appendff("'{}'", node->end());
auto content = in_new_builder([&] {
if (!node->contents())
return;
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
TemporaryChange heredoc { m_options.in_heredoc, true };
auto& contents = *node->contents();
contents.visit(*this);
current_builder().appendff("\n{}\n", node->end());
});
m_heredocs_to_append_after_sequence.append(move(content));
visited(node);
}
void Formatter::visit(const AST::HistoryEvent* node)
{
will_visit(node);
test_and_update_output_cursor(node);
current_builder().append('!');
switch (node->selector().event.kind) {
case AST::HistorySelector::EventKind::ContainingStringLookup:
current_builder().append('?');
current_builder().append(node->selector().event.text);
break;
case AST::HistorySelector::EventKind::StartingStringLookup:
current_builder().append(node->selector().event.text);
break;
case AST::HistorySelector::EventKind::IndexFromStart:
current_builder().append(node->selector().event.text);
break;
case AST::HistorySelector::EventKind::IndexFromEnd:
if (node->selector().event.index == 0)
current_builder().append('!');
else
current_builder().append(node->selector().event.text);
break;
}
auto& range = node->selector().word_selector_range;
if (!range.end.has_value()
|| range.end.value().kind != AST::HistorySelector::WordSelectorKind::Last
|| range.start.kind != AST::HistorySelector::WordSelectorKind::Index || range.start.selector != 0) {
auto append_word = [this](auto& selector) {
switch (selector.kind) {
case AST::HistorySelector::WordSelectorKind::Index:
if (selector.selector == 0)
current_builder().append('^');
else
current_builder().appendff("{}", selector.selector);
break;
case AST::HistorySelector::WordSelectorKind::Last:
current_builder().append('$');
break;
}
};
current_builder().append(':');
append_word(range.start);
if (range.end.has_value()) {
current_builder().append('-');
append_word(range.end.value());
}
}
visited(node);
}
void Formatter::visit(const AST::Execute* node)
{
will_visit(node);
test_and_update_output_cursor(node);
auto& builder = current_builder();
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
ScopedValueRollback options_rollback { m_options };
if (node->does_capture_stdout())
builder.append("$("sv);
NodeVisitor::visit(node);
if (node->does_capture_stdout())
builder.append(")"sv);
visited(node);
}
void Formatter::visit(const AST::IfCond* node)
{
will_visit(node);
test_and_update_output_cursor(node);
current_builder().append("if "sv);
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
node->condition()->visit(*this);
current_builder().append(' ');
in_new_block([&] {
if (node->true_branch())
node->true_branch()->visit(*this);
});
if (node->false_branch()) {
current_builder().append(" else "sv);
if (node->false_branch()->kind() != AST::Node::Kind::IfCond) {
in_new_block([&] {
node->false_branch()->visit(*this);
});
} else {
node->false_branch()->visit(*this);
}
} else if (node->else_position().has_value()) {
current_builder().append(" else "sv);
}
visited(node);
}
void Formatter::visit(const AST::ImmediateExpression* node)
{
will_visit(node);
test_and_update_output_cursor(node);
current_builder().append("${"sv);
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
current_builder().append(node->function_name());
for (auto& node : node->arguments()) {
current_builder().append(' ');
node->visit(*this);
}
if (node->has_closing_brace())
current_builder().append('}');
visited(node);
}
void Formatter::visit(const AST::Join* node)
{
will_visit(node);
test_and_update_output_cursor(node);
auto should_parenthesise = m_options.explicit_parentheses;
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
TemporaryChange parens { m_options.explicit_parentheses, false };
if (should_parenthesise)
current_builder().append('(');
node->left()->visit(*this);
current_builder().append(' ');
node->right()->visit(*this);
if (should_parenthesise)
current_builder().append(')');
visited(node);
}
void Formatter::visit(const AST::MatchExpr* node)
{
will_visit(node);
test_and_update_output_cursor(node);
current_builder().append("match "sv);
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
node->matched_expr()->visit(*this);
if (!node->expr_name().is_empty()) {
current_builder().append(" as "sv);
current_builder().append(node->expr_name());
}
current_builder().append(' ');
in_new_block([&] {
auto first_entry = true;
for (auto& entry : node->entries()) {
if (!first_entry)
insert_separator();
first_entry = false;
auto first = true;
entry.options.visit(
[&](Vector<NonnullRefPtr<AST::Node>> const& patterns) {
for (auto& option : patterns) {
if (!first)
current_builder().append(" | "sv);
first = false;
option->visit(*this);
}
},
[&](Vector<Regex<ECMA262>> const& patterns) {
for (auto& option : patterns) {
if (!first)
current_builder().append(" | "sv);
first = false;
auto node = make_ref_counted<AST::BarewordLiteral>(AST::Position {}, String::from_byte_string(option.pattern_value).release_value_but_fixme_should_propagate_errors());
node->visit(*this);
}
});
current_builder().append(' ');
if (entry.match_names.has_value() && !entry.match_names.value().is_empty()) {
current_builder().append("as ("sv);
auto first = true;
for (auto& name : entry.match_names.value()) {
if (!first)
current_builder().append(' ');
first = false;
current_builder().append(name);
}
current_builder().append(") "sv);
}
in_new_block([&] {
if (entry.body)
entry.body->visit(*this);
});
}
});
visited(node);
}
void Formatter::visit(const AST::Or* node)
{
will_visit(node);
test_and_update_output_cursor(node);
auto should_indent = m_parent_node && m_parent_node->kind() != AST::Node::Kind::Or;
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
with_added_indent(should_indent ? 1 : 0, [&] {
node->left()->visit(*this);
current_builder().append(" "sv);
insert_separator(true);
current_builder().append("|| "sv);
node->right()->visit(*this);
});
visited(node);
}
void Formatter::visit(const AST::Pipe* node)
{
will_visit(node);
test_and_update_output_cursor(node);
auto should_indent = m_parent_node && m_parent_node->kind() != AST::Node::Kind::Pipe;
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
node->left()->visit(*this);
current_builder().append(" "sv);
with_added_indent(should_indent ? 1 : 0, [&] {
insert_separator(true);
current_builder().append("| "sv);
node->right()->visit(*this);
});
visited(node);
}
void Formatter::visit(const AST::Range* node)
{
will_visit(node);
test_and_update_output_cursor(node);
if (!m_parent_node || m_parent_node->kind() != AST::Node::Kind::Slice)
current_builder().append('{');
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
node->start()->visit(*this);
current_builder().append(".."sv);
node->end()->visit(*this);
if (!m_parent_node || m_parent_node->kind() != AST::Node::Kind::Slice)
current_builder().append('}');
visited(node);
}
void Formatter::visit(const AST::ReadRedirection* node)
{
will_visit(node);
test_and_update_output_cursor(node);
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
if (node->fd() != 0)
current_builder().appendff(" {}<", node->fd());
else
current_builder().append(" <"sv);
NodeVisitor::visit(node);
visited(node);
}
void Formatter::visit(const AST::ReadWriteRedirection* node)
{
will_visit(node);
test_and_update_output_cursor(node);
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
if (node->fd() != 0)
current_builder().appendff(" {}<>", node->fd());
else
current_builder().append(" <>"sv);
NodeVisitor::visit(node);
visited(node);
}
void Formatter::visit(const AST::Sequence* node)
{
will_visit(node);
test_and_update_output_cursor(node);
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
bool first = true;
for (auto& entry : node->entries()) {
if (first)
first = false;
else
insert_separator();
entry->visit(*this);
}
visited(node);
}
void Formatter::visit(const AST::Subshell* node)
{
will_visit(node);
test_and_update_output_cursor(node);
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
in_new_block([&] {
NodeVisitor::visit(node);
});
visited(node);
}
void Formatter::visit(const AST::Slice* node)
{
will_visit(node);
test_and_update_output_cursor(node);
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
current_builder().append('[');
node->selector()->visit(*this);
current_builder().append(']');
visited(node);
}
void Formatter::visit(const AST::SimpleVariable* node)
{
will_visit(node);
test_and_update_output_cursor(node);
current_builder().append('$');
current_builder().append(node->name());
if (const AST::Node* slice = node->slice())
slice->visit(*this);
visited(node);
}
void Formatter::visit(const AST::SpecialVariable* node)
{
will_visit(node);
test_and_update_output_cursor(node);
current_builder().append('$');
current_builder().append(node->name());
if (const AST::Node* slice = node->slice())
slice->visit(*this);
visited(node);
}
void Formatter::visit(const AST::Juxtaposition* node)
{
will_visit(node);
test_and_update_output_cursor(node);
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
NodeVisitor::visit(node);
visited(node);
}
void Formatter::visit(const AST::StringLiteral* node)
{
will_visit(node);
test_and_update_output_cursor(node);
if (!m_options.in_double_quotes && !m_options.in_heredoc)
current_builder().append("'"sv);
if (m_options.in_double_quotes && !m_options.in_heredoc) {
for (auto ch : node->text().bytes_as_string_view()) {
switch (ch) {
case '"':
case '\\':
case '$':
current_builder().append('\\');
break;
case '\n':
current_builder().append("\\n"sv);
continue;
case '\r':
current_builder().append("\\r"sv);
continue;
case '\t':
current_builder().append("\\t"sv);
continue;
case '\v':
current_builder().append("\\v"sv);
continue;
case '\f':
current_builder().append("\\f"sv);
continue;
case '\a':
current_builder().append("\\a"sv);
continue;
case '\e':
current_builder().append("\\e"sv);
continue;
default:
break;
}
current_builder().append(ch);
}
} else {
current_builder().append(node->text());
}
if (!m_options.in_double_quotes && !m_options.in_heredoc)
current_builder().append("'"sv);
visited(node);
}
void Formatter::visit(const AST::StringPartCompose* node)
{
will_visit(node);
test_and_update_output_cursor(node);
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
NodeVisitor::visit(node);
visited(node);
}
void Formatter::visit(const AST::SyntaxError* node)
{
will_visit(node);
test_and_update_output_cursor(node);
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
NodeVisitor::visit(node);
visited(node);
}
void Formatter::visit(const AST::Tilde* node)
{
will_visit(node);
test_and_update_output_cursor(node);
current_builder().append(node->text());
visited(node);
}
void Formatter::visit(const AST::VariableDeclarations* node)
{
will_visit(node);
test_and_update_output_cursor(node);
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
auto first = true;
for (auto& entry : node->variables()) {
if (!first)
current_builder().append(' ');
first = false;
entry.name->visit(*this);
current_builder().append('=');
if (entry.value->is_command())
current_builder().append('(');
entry.value->visit(*this);
if (entry.value->is_command())
current_builder().append(')');
}
visited(node);
}
void Formatter::visit(const AST::WriteAppendRedirection* node)
{
will_visit(node);
test_and_update_output_cursor(node);
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
if (node->fd() != 1)
current_builder().appendff(" {}>>", node->fd());
else
current_builder().append(" >>"sv);
NodeVisitor::visit(node);
visited(node);
}
void Formatter::visit(const AST::WriteRedirection* node)
{
will_visit(node);
test_and_update_output_cursor(node);
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
if (node->fd() != 1)
current_builder().appendff(" {}>", node->fd());
else
current_builder().append(" >"sv);
NodeVisitor::visit(node);
visited(node);
}
}