Shell: Allow completing StringLiterals as paths

This auto-escapes the token as well :^)
This commit is contained in:
Ali Mohammad Pur 2022-03-06 11:58:49 +03:30 committed by Andreas Kling
parent 118590325a
commit 0ea775f257
Notes: sideshowbarker 2024-07-17 17:52:59 +09:00
6 changed files with 135 additions and 48 deletions

View file

@ -335,17 +335,40 @@ Vector<Line::CompletionSuggestion> Node::complete_for_editor(Shell& shell, size_
{
auto matching_node = hit_test_result.matching_node;
if (matching_node) {
if (matching_node->is_bareword()) {
auto* node = static_cast<BarewordLiteral*>(matching_node.ptr());
auto corrected_offset = find_offset_into_node(node->text(), offset - matching_node->position().start_offset);
auto kind = matching_node->kind();
StringLiteral::EnclosureType enclosure_type = StringLiteral::EnclosureType::None;
if (kind == Kind::StringLiteral)
enclosure_type = static_cast<StringLiteral*>(matching_node.ptr())->enclosure_type();
if (corrected_offset > node->text().length())
auto set_results_trivia = [enclosure_type](Vector<Line::CompletionSuggestion>&& suggestions) {
if (enclosure_type != StringLiteral::EnclosureType::None) {
for (auto& entry : suggestions)
entry.trailing_trivia = { static_cast<u32>(enclosure_type == StringLiteral::EnclosureType::SingleQuotes ? '\'' : '"') };
}
return suggestions;
};
if (kind == Kind::BarewordLiteral || kind == Kind::StringLiteral) {
Shell::EscapeMode escape_mode;
StringView text;
size_t corrected_offset;
if (kind == Kind::BarewordLiteral) {
auto* node = static_cast<BarewordLiteral*>(matching_node.ptr());
text = node->text();
escape_mode = Shell::EscapeMode::Bareword;
corrected_offset = find_offset_into_node(text, offset - matching_node->position().start_offset, escape_mode);
} else {
auto* node = static_cast<StringLiteral*>(matching_node.ptr());
text = node->text();
escape_mode = enclosure_type == StringLiteral::EnclosureType::SingleQuotes ? Shell::EscapeMode::SingleQuotedString : Shell::EscapeMode::DoubleQuotedString;
corrected_offset = find_offset_into_node(text, offset - matching_node->position().start_offset + 1, escape_mode);
}
if (corrected_offset > text.length())
return {};
auto& text = node->text();
// If the literal isn't an option, treat it as a path.
if (!(text.starts_with("-") || text == "--" || text == "-"))
return shell.complete_path("", text, corrected_offset, Shell::ExecutableOnly::No);
return set_results_trivia(shell.complete_path("", text, corrected_offset, Shell::ExecutableOnly::No, escape_mode));
// If the literal is an option, we have to know the program name
// should we have no way to get that, bail early.
@ -363,7 +386,7 @@ Vector<Line::CompletionSuggestion> Node::complete_for_editor(Shell& shell, size_
else
program_name = static_cast<StringLiteral*>(program_name_node.ptr())->text();
return shell.complete_option(program_name, text, corrected_offset);
return set_results_trivia(shell.complete_option(program_name, text, corrected_offset));
}
return {};
}
@ -3096,9 +3119,10 @@ void StringLiteral::highlight_in_editor(Line::Editor& editor, Shell&, HighlightM
editor.stylize({ m_position.start_offset, m_position.end_offset }, move(style));
}
StringLiteral::StringLiteral(Position position, String text)
StringLiteral::StringLiteral(Position position, String text, EnclosureType enclosure_type)
: Node(move(position))
, m_text(move(text))
, m_enclosure_type(enclosure_type)
{
}

View file

@ -1344,11 +1344,18 @@ private:
class StringLiteral final : public Node {
public:
StringLiteral(Position, String);
enum class EnclosureType {
None,
SingleQuotes,
DoubleQuotes,
};
StringLiteral(Position, String, EnclosureType);
virtual ~StringLiteral();
virtual void visit(NodeVisitor& visitor) override { visitor.visit(this); }
const String& text() const { return m_text; }
EnclosureType enclosure_type() const { return m_enclosure_type; }
private:
NODE(StringLiteral);
@ -1358,6 +1365,7 @@ private:
virtual RefPtr<Node> leftmost_trivial_literal() const override { return this; };
String m_text;
EnclosureType m_enclosure_type;
};
class StringPartCompose final : public Node {

View file

@ -228,7 +228,7 @@ RefPtr<AST::Node> Shell::immediate_regex_replace(AST::ImmediateExpression& invok
Regex<PosixExtendedParser> re { pattern->resolve_as_list(this).first() };
auto result = re.replace(value->resolve_as_list(this)[0], replacement->resolve_as_list(this)[0], PosixFlags::Global | PosixFlags::Multiline | PosixFlags::Unicode);
return AST::make_ref_counted<AST::StringLiteral>(invoking_node.position(), move(result));
return AST::make_ref_counted<AST::StringLiteral>(invoking_node.position(), move(result), AST::StringLiteral::EnclosureType::None);
}
RefPtr<AST::Node> Shell::immediate_remove_suffix(AST::ImmediateExpression& invoking_node, const NonnullRefPtrVector<AST::Node>& arguments)
@ -256,7 +256,7 @@ RefPtr<AST::Node> Shell::immediate_remove_suffix(AST::ImmediateExpression& invok
if (value_str.ends_with(suffix_str))
removed = removed.substring_view(0, value_str.length() - suffix_str.length());
nodes.append(AST::make_ref_counted<AST::StringLiteral>(invoking_node.position(), removed));
nodes.append(AST::make_ref_counted<AST::StringLiteral>(invoking_node.position(), removed, AST::StringLiteral::EnclosureType::None));
}
return AST::make_ref_counted<AST::ListConcatenate>(invoking_node.position(), move(nodes));
@ -287,7 +287,7 @@ RefPtr<AST::Node> Shell::immediate_remove_prefix(AST::ImmediateExpression& invok
if (value_str.starts_with(prefix_str))
removed = removed.substring_view(prefix_str.length());
nodes.append(AST::make_ref_counted<AST::StringLiteral>(invoking_node.position(), removed));
nodes.append(AST::make_ref_counted<AST::StringLiteral>(invoking_node.position(), removed, AST::StringLiteral::EnclosureType::None));
}
return AST::make_ref_counted<AST::ListConcatenate>(invoking_node.position(), move(nodes));
@ -375,7 +375,7 @@ RefPtr<AST::Node> Shell::immediate_concat_lists(AST::ImmediateExpression& invoki
} else {
auto values = list_of_values->resolve_as_list(this);
for (auto& entry : values)
result.append(AST::make_ref_counted<AST::StringLiteral>(argument.position(), entry));
result.append(AST::make_ref_counted<AST::StringLiteral>(argument.position(), entry, AST::StringLiteral::EnclosureType::None));
}
}
}

View file

@ -333,7 +333,7 @@ RefPtr<AST::Node> Parser::parse_variable_decls()
if (!expression) {
if (is_whitespace(peek())) {
auto string_start = push_start();
expression = create<AST::StringLiteral>("");
expression = create<AST::StringLiteral>("", AST::StringLiteral::EnclosureType::None);
} else {
restore_to(pos_before_name.offset, pos_before_name.line);
return nullptr;
@ -1276,7 +1276,7 @@ RefPtr<AST::Node> Parser::parse_string()
bool is_error = false;
if (!expect('\''))
is_error = true;
auto result = create<AST::StringLiteral>(move(text)); // String Literal
auto result = create<AST::StringLiteral>(move(text), AST::StringLiteral::EnclosureType::SingleQuotes); // String Literal
if (is_error)
result->set_is_syntax_error(*create<AST::SyntaxError>("Expected a terminating single quote", true));
return result;
@ -1358,7 +1358,7 @@ RefPtr<AST::Node> Parser::parse_string_inner(StringEndCondition condition)
continue;
}
if (peek() == '$') {
auto string_literal = create<AST::StringLiteral>(builder.to_string()); // String Literal
auto string_literal = create<AST::StringLiteral>(builder.to_string(), AST::StringLiteral::EnclosureType::DoubleQuotes); // String Literal
auto read_concat = [&](auto&& node) {
auto inner = create<AST::StringPartCompose>(
move(string_literal),
@ -1384,7 +1384,7 @@ RefPtr<AST::Node> Parser::parse_string_inner(StringEndCondition condition)
builder.append(consume());
}
return create<AST::StringLiteral>(builder.to_string()); // String Literal
return create<AST::StringLiteral>(builder.to_string(), AST::StringLiteral::EnclosureType::DoubleQuotes); // String Literal
}
RefPtr<AST::Node> Parser::parse_variable()
@ -1923,7 +1923,7 @@ RefPtr<AST::Node> Parser::parse_brace_expansion_spec()
if (next_is(",")) {
// Note that we don't consume the ',' here.
subexpressions.append(create<AST::StringLiteral>(""));
subexpressions.append(create<AST::StringLiteral>("", AST::StringLiteral::EnclosureType::None));
} else {
auto start_expr = parse_expression();
if (start_expr) {
@ -1948,7 +1948,7 @@ RefPtr<AST::Node> Parser::parse_brace_expansion_spec()
if (expr) {
subexpressions.append(expr.release_nonnull());
} else {
subexpressions.append(create<AST::StringLiteral>(""));
subexpressions.append(create<AST::StringLiteral>("", AST::StringLiteral::EnclosureType::None));
}
}
@ -2061,7 +2061,7 @@ bool Parser::parse_heredoc_entries()
if (!last_line_offset.has_value())
last_line_offset = current_position();
// Now just wrap it in a StringLiteral and set it as the node's contents
auto node = create<AST::StringLiteral>(m_input.substring_view(rule_start->offset, last_line_offset->offset - rule_start->offset));
auto node = create<AST::StringLiteral>(m_input.substring_view(rule_start->offset, last_line_offset->offset - rule_start->offset), AST::StringLiteral::EnclosureType::None);
if (!found_key)
node->set_is_syntax_error(*create<AST::SyntaxError>(String::formatted("Expected to find the heredoc key '{}', but found Eof", record.end), true));
record.node->set_contents(move(node));
@ -2110,7 +2110,7 @@ bool Parser::parse_heredoc_entries()
}
if (!expr && found_key) {
expr = create<AST::StringLiteral>("");
expr = create<AST::StringLiteral>("", AST::StringLiteral::EnclosureType::None);
} else if (!expr) {
expr = create<AST::SyntaxError>(String::formatted("Expected to find a valid string inside a heredoc (with end key '{}')", record.end), true);
} else if (!found_key) {

View file

@ -1146,12 +1146,19 @@ String Shell::escape_token_for_double_quotes(StringView token)
return builder.build();
}
Shell::SpecialCharacterEscapeMode Shell::special_character_escape_mode(u32 code_point)
Shell::SpecialCharacterEscapeMode Shell::special_character_escape_mode(u32 code_point, EscapeMode mode)
{
switch (code_point) {
case '\'':
if (mode == EscapeMode::DoubleQuotedString)
return SpecialCharacterEscapeMode::Untouched;
return SpecialCharacterEscapeMode::Escaped;
case '"':
case '$':
case '\\':
if (mode == EscapeMode::SingleQuotedString)
return SpecialCharacterEscapeMode::Untouched;
return SpecialCharacterEscapeMode::Escaped;
case '|':
case '>':
case '<':
@ -1161,8 +1168,9 @@ Shell::SpecialCharacterEscapeMode Shell::special_character_escape_mode(u32 code_
case '}':
case '&':
case ';':
case '\\':
case ' ':
if (mode == EscapeMode::SingleQuotedString || mode == EscapeMode::DoubleQuotedString)
return SpecialCharacterEscapeMode::Untouched;
return SpecialCharacterEscapeMode::Escaped;
case '\n':
case '\t':
@ -1176,13 +1184,13 @@ Shell::SpecialCharacterEscapeMode Shell::special_character_escape_mode(u32 code_
}
}
String Shell::escape_token(StringView token)
String Shell::escape_token(StringView token, EscapeMode escape_mode)
{
auto do_escape = [](auto& token) {
auto do_escape = [escape_mode](auto& token) {
StringBuilder builder;
for (auto c : token) {
static_assert(sizeof(c) == sizeof(u32) || sizeof(c) == sizeof(u8));
switch (special_character_escape_mode(c)) {
switch (special_character_escape_mode(c, escape_mode)) {
case SpecialCharacterEscapeMode::Untouched:
if constexpr (sizeof(c) == sizeof(u8))
builder.append(c);
@ -1190,29 +1198,51 @@ String Shell::escape_token(StringView token)
builder.append(Utf32View { &c, 1 });
break;
case SpecialCharacterEscapeMode::Escaped:
if (escape_mode == EscapeMode::SingleQuotedString)
builder.append("'");
builder.append('\\');
builder.append(c);
if (escape_mode == EscapeMode::SingleQuotedString)
builder.append("'");
break;
case SpecialCharacterEscapeMode::QuotedAsEscape:
if (escape_mode == EscapeMode::SingleQuotedString)
builder.append("'");
if (escape_mode != EscapeMode::DoubleQuotedString)
builder.append("\"");
switch (c) {
case '\n':
builder.append(R"("\n")");
builder.append(R"(\n)");
break;
case '\t':
builder.append(R"("\t")");
builder.append(R"(\t)");
break;
case '\r':
builder.append(R"("\r")");
builder.append(R"(\r)");
break;
default:
VERIFY_NOT_REACHED();
}
if (escape_mode != EscapeMode::DoubleQuotedString)
builder.append("\"");
if (escape_mode == EscapeMode::SingleQuotedString)
builder.append("'");
break;
case SpecialCharacterEscapeMode::QuotedAsHex:
if (escape_mode == EscapeMode::SingleQuotedString)
builder.append("'");
if (escape_mode != EscapeMode::DoubleQuotedString)
builder.append("\"");
if (c <= NumericLimits<u8>::max())
builder.appendff(R"("\x{:0>2x}")", static_cast<u8>(c));
builder.appendff(R"(\x{:0>2x})", static_cast<u8>(c));
else
builder.appendff(R"("\u{:0>8x}")", static_cast<u32>(c));
builder.appendff(R"(\u{:0>8x})", static_cast<u32>(c));
if (escape_mode != EscapeMode::DoubleQuotedString)
builder.append("\"");
if (escape_mode == EscapeMode::SingleQuotedString)
builder.append("'");
break;
}
}
@ -1372,8 +1402,7 @@ Vector<Line::CompletionSuggestion> Shell::complete()
return ast->complete_for_editor(*this, line.length());
}
Vector<Line::CompletionSuggestion> Shell::complete_path(StringView base,
StringView part, size_t offset, ExecutableOnly executable_only)
Vector<Line::CompletionSuggestion> Shell::complete_path(StringView base, StringView part, size_t offset, ExecutableOnly executable_only, EscapeMode escape_mode)
{
auto token = offset ? part.substring_view(0, offset) : "";
String path;
@ -1415,7 +1444,7 @@ Vector<Line::CompletionSuggestion> Shell::complete_path(StringView base,
// e. in `cd /foo/bar', 'bar' is the invariant
// since we are not suggesting anything starting with
// `/foo/', but rather just `bar...'
auto token_length = escape_token(token).length();
auto token_length = escape_token(token, escape_mode).length();
size_t static_offset = last_slash + 1;
auto invariant_offset = token_length;
if (m_editor)
@ -1435,11 +1464,11 @@ Vector<Line::CompletionSuggestion> Shell::complete_path(StringView base,
int stat_error = stat(file_path.characters(), &program_status);
if (!stat_error && (executable_only == ExecutableOnly::No || access(file_path.characters(), X_OK) == 0)) {
if (S_ISDIR(program_status.st_mode)) {
suggestions.append({ escape_token(file), "/" });
suggestions.append({ escape_token(file, escape_mode), "/" });
} else {
if (!allow_direct_children && !file.contains("/"))
continue;
suggestions.append({ escape_token(file), " " });
suggestions.append({ escape_token(file, escape_mode), " " });
}
suggestions.last().input_offset = token_length;
suggestions.last().invariant_offset = invariant_offset;
@ -1451,7 +1480,7 @@ Vector<Line::CompletionSuggestion> Shell::complete_path(StringView base,
return suggestions;
}
Vector<Line::CompletionSuggestion> Shell::complete_program_name(StringView name, size_t offset)
Vector<Line::CompletionSuggestion> Shell::complete_program_name(StringView name, size_t offset, EscapeMode escape_mode)
{
auto match = binary_search(
cached_path.span(),
@ -1465,10 +1494,10 @@ Vector<Line::CompletionSuggestion> Shell::complete_program_name(StringView name,
});
if (!match)
return complete_path("", name, offset, ExecutableOnly::Yes);
return complete_path("", name, offset, ExecutableOnly::Yes, escape_mode);
String completion = *match;
auto token_length = escape_token(name).length();
auto token_length = escape_token(name, escape_mode).length();
auto invariant_offset = token_length;
size_t static_offset = 0;
if (m_editor)

View file

@ -156,9 +156,14 @@ public:
[[nodiscard]] Frame push_frame(String name);
void pop_frame();
enum class EscapeMode {
Bareword,
SingleQuotedString,
DoubleQuotedString,
};
static String escape_token_for_double_quotes(StringView token);
static String escape_token_for_single_quotes(StringView token);
static String escape_token(StringView token);
static String escape_token(StringView token, EscapeMode = EscapeMode::Bareword);
static String unescape_token(StringView token);
enum class SpecialCharacterEscapeMode {
Untouched,
@ -166,7 +171,7 @@ public:
QuotedAsEscape,
QuotedAsHex,
};
static SpecialCharacterEscapeMode special_character_escape_mode(u32 c);
static SpecialCharacterEscapeMode special_character_escape_mode(u32 c, EscapeMode);
static bool is_glob(StringView);
static Vector<StringView> split_path(StringView);
@ -178,8 +183,8 @@ public:
void highlight(Line::Editor&) const;
Vector<Line::CompletionSuggestion> complete();
Vector<Line::CompletionSuggestion> complete_path(StringView base, StringView, size_t offset, ExecutableOnly executable_only);
Vector<Line::CompletionSuggestion> complete_program_name(StringView, size_t offset);
Vector<Line::CompletionSuggestion> complete_path(StringView base, StringView, size_t offset, ExecutableOnly executable_only, EscapeMode = EscapeMode::Bareword);
Vector<Line::CompletionSuggestion> complete_program_name(StringView, size_t offset, EscapeMode = EscapeMode::Bareword);
Vector<Line::CompletionSuggestion> complete_variable(StringView, size_t offset);
Vector<Line::CompletionSuggestion> complete_user(StringView, size_t offset);
Vector<Line::CompletionSuggestion> complete_option(StringView, StringView, size_t offset);
@ -378,7 +383,7 @@ private:
return c == '_' || (c <= 'Z' && c >= 'A') || (c <= 'z' && c >= 'a') || (c <= '9' && c >= '0');
}
inline size_t find_offset_into_node(StringView unescaped_text, size_t escaped_offset)
inline size_t find_offset_into_node(StringView unescaped_text, size_t escaped_offset, Shell::EscapeMode escape_mode)
{
size_t unescaped_offset = 0;
size_t offset = 0;
@ -387,20 +392,41 @@ inline size_t find_offset_into_node(StringView unescaped_text, size_t escaped_of
if (offset == escaped_offset)
return unescaped_offset;
switch (Shell::special_character_escape_mode(c)) {
switch (Shell::special_character_escape_mode(c, escape_mode)) {
case Shell::SpecialCharacterEscapeMode::Untouched:
break;
case Shell::SpecialCharacterEscapeMode::Escaped:
++offset; // X -> \X
break;
case Shell::SpecialCharacterEscapeMode::QuotedAsEscape:
offset += 3; // X -> "\Y"
switch (escape_mode) {
case Shell::EscapeMode::Bareword:
offset += 3; // X -> "\Y"
break;
case Shell::EscapeMode::SingleQuotedString:
offset += 5; // X -> '"\Y"'
break;
case Shell::EscapeMode::DoubleQuotedString:
offset += 1; // X -> \Y
break;
}
break;
case Shell::SpecialCharacterEscapeMode::QuotedAsHex:
switch (escape_mode) {
case Shell::EscapeMode::Bareword:
offset += 2; // X -> "\..."
break;
case Shell::EscapeMode::SingleQuotedString:
offset += 4; // X -> '"\..."'
break;
case Shell::EscapeMode::DoubleQuotedString:
// X -> \...
break;
}
if (c > NumericLimits<u8>::max())
offset += 11; // X -> "\uhhhhhhhh"
offset += 8; // X -> "\uhhhhhhhh"
else
offset += 5; // X -> "\xhh"
offset += 3; // X -> "\xhh"
break;
}
++offset;