summaryrefslogtreecommitdiff
path: root/Userland/Shell
diff options
context:
space:
mode:
authorAli Mohammad Pur <ali.mpfard@gmail.com>2022-03-06 11:58:49 +0330
committerAndreas Kling <kling@serenityos.org>2022-03-06 13:20:41 +0100
commit0ea775f2575148f9bac3c4b22589033804f8406f (patch)
tree595799cec025809413447abdc65310ca610bad42 /Userland/Shell
parent118590325a0fe289e3e23b975b60c60d0a10c04e (diff)
downloadserenity-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.cpp40
-rw-r--r--Userland/Shell/AST.h10
-rw-r--r--Userland/Shell/ImmediateFunctions.cpp8
-rw-r--r--Userland/Shell/Parser.cpp16
-rw-r--r--Userland/Shell/Shell.cpp65
-rw-r--r--Userland/Shell/Shell.h44
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;