diff options
author | Ali Mohammad Pur <ali.mpfard@gmail.com> | 2022-03-06 11:58:49 +0330 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2022-03-06 13:20:41 +0100 |
commit | 0ea775f2575148f9bac3c4b22589033804f8406f (patch) | |
tree | 595799cec025809413447abdc65310ca610bad42 /Userland/Shell | |
parent | 118590325a0fe289e3e23b975b60c60d0a10c04e (diff) | |
download | serenity-0ea775f2575148f9bac3c4b22589033804f8406f.zip |
Shell: Allow completing StringLiterals as paths
This auto-escapes the token as well :^)
Diffstat (limited to 'Userland/Shell')
-rw-r--r-- | Userland/Shell/AST.cpp | 40 | ||||
-rw-r--r-- | Userland/Shell/AST.h | 10 | ||||
-rw-r--r-- | Userland/Shell/ImmediateFunctions.cpp | 8 | ||||
-rw-r--r-- | Userland/Shell/Parser.cpp | 16 | ||||
-rw-r--r-- | Userland/Shell/Shell.cpp | 65 | ||||
-rw-r--r-- | Userland/Shell/Shell.h | 44 |
6 files changed, 135 insertions, 48 deletions
diff --git a/Userland/Shell/AST.cpp b/Userland/Shell/AST.cpp index aad68b4467..ef79901c67 100644 --- a/Userland/Shell/AST.cpp +++ b/Userland/Shell/AST.cpp @@ -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(); + + 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 > node->text().length()) + 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) { } diff --git a/Userland/Shell/AST.h b/Userland/Shell/AST.h index 7661b68b56..e9c25ead06 100644 --- a/Userland/Shell/AST.h +++ b/Userland/Shell/AST.h @@ -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 { diff --git a/Userland/Shell/ImmediateFunctions.cpp b/Userland/Shell/ImmediateFunctions.cpp index 27b391dd1a..d95deb972c 100644 --- a/Userland/Shell/ImmediateFunctions.cpp +++ b/Userland/Shell/ImmediateFunctions.cpp @@ -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)); } } } diff --git a/Userland/Shell/Parser.cpp b/Userland/Shell/Parser.cpp index e70fe6ea6d..68d189ce04 100644 --- a/Userland/Shell/Parser.cpp +++ b/Userland/Shell/Parser.cpp @@ -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) { diff --git a/Userland/Shell/Shell.cpp b/Userland/Shell/Shell.cpp index 30295eb6b2..d66cf03b74 100644 --- a/Userland/Shell/Shell.cpp +++ b/Userland/Shell/Shell.cpp @@ -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) diff --git a/Userland/Shell/Shell.h b/Userland/Shell/Shell.h index 9a920cd42b..3e43a03894 100644 --- a/Userland/Shell/Shell.h +++ b/Userland/Shell/Shell.h @@ -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; |