diff --git a/Userland/Shell/Shell.cpp b/Userland/Shell/Shell.cpp index d66cf03b74..6155a0f794 100644 --- a/Userland/Shell/Shell.cpp +++ b/Userland/Shell/Shell.cpp @@ -1168,6 +1168,8 @@ Shell::SpecialCharacterEscapeMode Shell::special_character_escape_mode(u32 code_ case '}': case '&': case ';': + case '?': + case '*': case ' ': if (mode == EscapeMode::SingleQuotedString || mode == EscapeMode::DoubleQuotedString) return SpecialCharacterEscapeMode::Untouched; @@ -1184,76 +1186,82 @@ Shell::SpecialCharacterEscapeMode Shell::special_character_escape_mode(u32 code_ } } +static String do_escape(Shell::EscapeMode escape_mode, auto& token) +{ + StringBuilder builder; + for (auto c : token) { + static_assert(sizeof(c) == sizeof(u32) || sizeof(c) == sizeof(u8)); + switch (Shell::special_character_escape_mode(c, escape_mode)) { + case Shell::SpecialCharacterEscapeMode::Untouched: + if constexpr (sizeof(c) == sizeof(u8)) + builder.append(c); + else + builder.append(Utf32View { &c, 1 }); + break; + case Shell::SpecialCharacterEscapeMode::Escaped: + if (escape_mode == Shell::EscapeMode::SingleQuotedString) + builder.append("'"); + builder.append('\\'); + builder.append(c); + if (escape_mode == Shell::EscapeMode::SingleQuotedString) + builder.append("'"); + break; + case Shell::SpecialCharacterEscapeMode::QuotedAsEscape: + if (escape_mode == Shell::EscapeMode::SingleQuotedString) + builder.append("'"); + if (escape_mode != Shell::EscapeMode::DoubleQuotedString) + builder.append("\""); + switch (c) { + case '\n': + builder.append(R"(\n)"); + break; + case '\t': + builder.append(R"(\t)"); + break; + case '\r': + builder.append(R"(\r)"); + break; + default: + VERIFY_NOT_REACHED(); + } + if (escape_mode != Shell::EscapeMode::DoubleQuotedString) + builder.append("\""); + if (escape_mode == Shell::EscapeMode::SingleQuotedString) + builder.append("'"); + break; + case Shell::SpecialCharacterEscapeMode::QuotedAsHex: + if (escape_mode == Shell::EscapeMode::SingleQuotedString) + builder.append("'"); + if (escape_mode != Shell::EscapeMode::DoubleQuotedString) + builder.append("\""); + + if (c <= NumericLimits::max()) + builder.appendff(R"(\x{:0>2x})", static_cast(c)); + else + builder.appendff(R"(\u{:0>8x})", static_cast(c)); + + if (escape_mode != Shell::EscapeMode::DoubleQuotedString) + builder.append("\""); + if (escape_mode == Shell::EscapeMode::SingleQuotedString) + builder.append("'"); + break; + } + } + + return builder.build(); +} + +String Shell::escape_token(Utf32View token, EscapeMode escape_mode) +{ + return do_escape(escape_mode, token); +} + String Shell::escape_token(StringView token, EscapeMode escape_mode) { - 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, escape_mode)) { - case SpecialCharacterEscapeMode::Untouched: - if constexpr (sizeof(c) == sizeof(u8)) - builder.append(c); - else - 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)"); - break; - case '\t': - builder.append(R"(\t)"); - break; - case '\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::max()) - builder.appendff(R"(\x{:0>2x})", static_cast(c)); - else - builder.appendff(R"(\u{:0>8x})", static_cast(c)); - - if (escape_mode != EscapeMode::DoubleQuotedString) - builder.append("\""); - if (escape_mode == EscapeMode::SingleQuotedString) - builder.append("'"); - break; - } - } - - return builder.build(); - }; - Utf8View view { token }; if (view.validate()) - return do_escape(view); - return do_escape(token); + return do_escape(escape_mode, view); + return do_escape(escape_mode, token); } String Shell::unescape_token(StringView token) diff --git a/Userland/Shell/Shell.h b/Userland/Shell/Shell.h index 3e43a03894..79a984c779 100644 --- a/Userland/Shell/Shell.h +++ b/Userland/Shell/Shell.h @@ -164,6 +164,7 @@ public: static String escape_token_for_double_quotes(StringView token); static String escape_token_for_single_quotes(StringView token); static String escape_token(StringView token, EscapeMode = EscapeMode::Bareword); + static String escape_token(Utf32View token, EscapeMode = EscapeMode::Bareword); static String unescape_token(StringView token); enum class SpecialCharacterEscapeMode { Untouched, diff --git a/Userland/Shell/main.cpp b/Userland/Shell/main.cpp index bcca36f59d..8ce039b4b7 100644 --- a/Userland/Shell/main.cpp +++ b/Userland/Shell/main.cpp @@ -82,6 +82,79 @@ ErrorOr serenity_main(Main::Arguments arguments) editor->on_tab_complete = [&](const Line::Editor&) { return shell->complete(); }; + editor->on_paste = [&](Utf32View data, Line::Editor& editor) { + auto line = editor.line(editor.cursor()); + Shell::Parser parser(line, false); + auto ast = parser.parse(); + if (!ast) { + editor.insert(data); + return; + } + + auto hit_test_result = ast->hit_test_position(editor.cursor()); + // If the argument isn't meant to be an entire command, escape it. + // This allows copy-pasting entire commands where commands are expected, and otherwise escapes everything. + auto should_escape = false; + if (!hit_test_result.matching_node && hit_test_result.closest_command_node) { + // There's *some* command, but our cursor is immediate after it + should_escape = editor.cursor() >= hit_test_result.closest_command_node->position().end_offset; + hit_test_result.matching_node = hit_test_result.closest_command_node; + } else if (hit_test_result.matching_node && hit_test_result.closest_command_node) { + // There's a command, and we're at the end of or in the middle of some node. + auto leftmost_literal = hit_test_result.closest_command_node->leftmost_trivial_literal(); + if (leftmost_literal) + should_escape = !hit_test_result.matching_node->position().contains(leftmost_literal->position().start_offset); + } + + if (should_escape) { + String escaped_string; + Optional trivia {}; + bool starting_trivia_already_provided = false; + auto escape_mode = Shell::Shell::EscapeMode::Bareword; + if (hit_test_result.matching_node->kind() == Shell::AST::Node::Kind::StringLiteral) { + // If we're pasting in a string literal, make sure to only consider that specific escape mode + auto* node = static_cast(hit_test_result.matching_node.ptr()); + switch (node->enclosure_type()) { + case Shell::AST::StringLiteral::EnclosureType::None: + break; + case Shell::AST::StringLiteral::EnclosureType::SingleQuotes: + escape_mode = Shell::Shell::EscapeMode::SingleQuotedString; + trivia = '\''; + starting_trivia_already_provided = true; + break; + case Shell::AST::StringLiteral::EnclosureType::DoubleQuotes: + escape_mode = Shell::Shell::EscapeMode::DoubleQuotedString; + trivia = '"'; + starting_trivia_already_provided = true; + break; + } + } + + if (starting_trivia_already_provided) { + escaped_string = shell->escape_token(data, escape_mode); + } else { + escaped_string = shell->escape_token(data, Shell::Shell::EscapeMode::Bareword); + if (auto string = shell->escape_token(data, Shell::Shell::EscapeMode::SingleQuotedString); string.length() + 2 < escaped_string.length()) { + escaped_string = move(string); + trivia = '\''; + } + if (auto string = shell->escape_token(data, Shell::Shell::EscapeMode::DoubleQuotedString); string.length() + 2 < escaped_string.length()) { + escaped_string = move(string); + trivia = '"'; + } + } + + if (trivia.has_value() && !starting_trivia_already_provided) + editor.insert(*trivia); + + editor.insert(escaped_string); + + if (trivia.has_value()) + editor.insert(*trivia); + } else { + editor.insert(data); + } + }; }; const char* command_to_run = nullptr;