summaryrefslogtreecommitdiff
path: root/Userland/Shell
diff options
context:
space:
mode:
authorAli Mohammad Pur <ali.mpfard@gmail.com>2022-03-06 13:04:31 +0330
committerAndreas Kling <kling@serenityos.org>2022-03-06 13:20:41 +0100
commita76730823ab3b7e125ea501e095a3ffd25a7dd18 (patch)
tree133fe8212d15532bd2888f4cdcc3c68b42662362 /Userland/Shell
parentc4d937747751d59ca3a5a1c04f608a0ccb2ee9b0 (diff)
downloadserenity-a76730823ab3b7e125ea501e095a3ffd25a7dd18.zip
Shell: Be more smart with pasted stuff
Shell can now use LibLine's `on_paste` hook to more intelligently escape pasted data, with the following heuristics: - If the current command is invalid, just pile the pasted string on top - If the cursor is *after* a command node, escape the pasted data, whichever way yields a smaller encoding - If the cursor is at the start of or in the middle of a command name, paste the data as-is, assuming that the user wants to paste code - If the cursor is otherwise in some argument, escape the pasted data according to which kind of string the cursor is in the middle of (double-quoted, single-quoted or a simple bareword)
Diffstat (limited to 'Userland/Shell')
-rw-r--r--Userland/Shell/Shell.cpp126
-rw-r--r--Userland/Shell/Shell.h1
-rw-r--r--Userland/Shell/main.cpp73
3 files changed, 141 insertions, 59 deletions
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_
}
}
-String Shell::escape_token(StringView token, EscapeMode escape_mode)
+static String do_escape(Shell::EscapeMode escape_mode, 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, 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('\\');
+ 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);
- if (escape_mode == EscapeMode::SingleQuotedString)
- builder.append("'");
+ 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 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("'");
+ case '\t':
+ builder.append(R"(\t)");
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));
- else
- builder.appendff(R"(\u{:0>8x})", static_cast<u32>(c));
-
- if (escape_mode != EscapeMode::DoubleQuotedString)
- builder.append("\"");
- if (escape_mode == EscapeMode::SingleQuotedString)
- builder.append("'");
+ 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<u8>::max())
+ builder.appendff(R"(\x{:0>2x})", static_cast<u8>(c));
+ else
+ builder.appendff(R"(\u{:0>8x})", static_cast<u32>(c));
+
+ if (escape_mode != Shell::EscapeMode::DoubleQuotedString)
+ builder.append("\"");
+ if (escape_mode == Shell::EscapeMode::SingleQuotedString)
+ builder.append("'");
+ break;
}
+ }
- return builder.build();
- };
+ 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)
+{
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<int> 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<char> 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<Shell::AST::StringLiteral const*>(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;