diff --git a/Userland/Shell/AST.cpp b/Userland/Shell/AST.cpp index 126b136273..7bf4780e09 100644 --- a/Userland/Shell/AST.cpp +++ b/Userland/Shell/AST.cpp @@ -620,8 +620,22 @@ RefPtr BarewordLiteral::run(RefPtr) void BarewordLiteral::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata) { if (metadata.is_first_in_list) { - if (shell.is_runnable(m_text)) { - editor.stylize({ m_position.start_offset, m_position.end_offset }, { Line::Style::Bold }); + auto runnable = shell.runnable_path_for(m_text); + if (runnable.has_value()) { + Line::Style bold = { Line::Style::Bold }; + Line::Style style = bold; + +#ifdef __serenity__ + if (runnable->kind == Shell::RunnablePath::Kind::Executable || runnable->kind == Shell::RunnablePath::Kind::Alias) { + auto name = shell.help_path_for({}, *runnable); + if (name.has_value()) { + auto url = URL::create_with_help_scheme(name.release_value(), shell.hostname); + style = bold.unified_with(Line::Style::Hyperlink(url.to_string())); + } + } +#endif + + editor.stylize({ m_position.start_offset, m_position.end_offset }, style); } else { editor.stylize({ m_position.start_offset, m_position.end_offset }, { Line::Style::Foreground(Line::Style::XtermColor::Red) }); } diff --git a/Userland/Shell/Builtin.cpp b/Userland/Shell/Builtin.cpp index 5ee707a30a..538a4bb6f1 100644 --- a/Userland/Shell/Builtin.cpp +++ b/Userland/Shell/Builtin.cpp @@ -67,7 +67,7 @@ int Shell::builtin_alias(int argc, char const** argv) } } else { m_aliases.set(parts[0], parts[1]); - add_entry_to_cache(parts[0]); + add_entry_to_cache({ RunnablePath::Kind::Alias, parts[0] }); } } diff --git a/Userland/Shell/Shell.cpp b/Userland/Shell/Shell.cpp index 532db8ed9c..e169847dc9 100644 --- a/Userland/Shell/Shell.cpp +++ b/Userland/Shell/Shell.cpp @@ -421,7 +421,7 @@ void Shell::unset_local_variable(StringView name, bool only_in_current_frame) void Shell::define_function(String name, Vector argnames, RefPtr body) { - add_entry_to_cache(name); + add_entry_to_cache({ RunnablePath::Kind::Function, name }); m_functions.set(name, { name, move(argnames), move(body) }); } @@ -517,14 +517,47 @@ String Shell::resolve_alias(StringView name) const return m_aliases.get(name).value_or({}); } -bool Shell::is_runnable(StringView name) +Optional Shell::runnable_path_for(StringView name) { auto parts = name.split_view('/'); auto path = name.to_string(); - if (parts.size() > 1 && access(path.characters(), X_OK) == 0) - return true; + if (parts.size() > 1) { + auto file = Core::File::open(path.characters(), Core::OpenMode::ReadOnly); + if (!file.is_error() && !file.value()->is_directory() && access(path.characters(), X_OK) == 0) + return RunnablePath { RunnablePath::Kind::Executable, name }; + } - return binary_search(cached_path.span(), path, nullptr); + auto* found = binary_search(cached_path.span(), path, nullptr, RunnablePathComparator {}); + if (!found) + return {}; + + return *found; +} + +Optional Shell::help_path_for(Vector visited, Shell::RunnablePath const& runnable_path) +{ + switch (runnable_path.kind) { + case RunnablePath::Kind::Executable: { + LexicalPath lexical_path(runnable_path.path); + return lexical_path.basename(); + } + + case RunnablePath::Kind::Alias: { + if (visited.contains_slow(runnable_path)) + return {}; // Break out of an alias loop + + auto resolved = resolve_alias(runnable_path.path); + auto* runnable = binary_search(cached_path.span(), resolved, nullptr, RunnablePathComparator {}); + if (!runnable) + return {}; + + visited.append(runnable_path); + return help_path_for(visited, *runnable); + } + + default: + return {}; + } } int Shell::run_command(StringView cmd, Optional source_position_override) @@ -1336,14 +1369,14 @@ void Shell::cache_path() // Add shell builtins to the cache. for (auto const& builtin_name : builtin_names) - cached_path.append(escape_token(builtin_name)); + cached_path.append({ RunnablePath::Kind::Builtin, escape_token(builtin_name) }); // Add functions to the cache. for (auto& function : m_functions) { auto name = escape_token(function.key); if (cached_path.contains_slow(name)) continue; - cached_path.append(name); + cached_path.append({ RunnablePath::Kind::Function, name }); } // Add aliases to the cache. @@ -1351,7 +1384,7 @@ void Shell::cache_path() auto name = escape_token(alias.key); if (cached_path.contains_slow(name)) continue; - cached_path.append(name); + cached_path.append({ RunnablePath::Kind::Alias, name }); } String path = getenv("PATH"); @@ -1366,7 +1399,7 @@ void Shell::cache_path() if (cached_path.contains_slow(escaped_name)) continue; if (access(program_path.characters(), X_OK) == 0) - cached_path.append(escaped_name); + cached_path.append({ RunnablePath::Kind::Executable, escaped_name }); } } } @@ -1374,15 +1407,15 @@ void Shell::cache_path() quick_sort(cached_path); } -void Shell::add_entry_to_cache(String const& entry) +void Shell::add_entry_to_cache(RunnablePath const& entry) { size_t index = 0; - auto match = binary_search(cached_path.span(), entry, &index); + auto match = binary_search(cached_path.span(), entry, &index, RunnablePathComparator {}); if (match) return; - while (index < cached_path.size() && strcmp(cached_path[index].characters(), entry.characters()) < 0) { + while (index < cached_path.size() && strcmp(cached_path[index].path.characters(), entry.path.characters()) < 0) { index++; } cached_path.insert(index, entry); @@ -1391,7 +1424,7 @@ void Shell::add_entry_to_cache(String const& entry) void Shell::remove_entry_from_cache(StringView entry) { size_t index { 0 }; - auto match = binary_search(cached_path.span(), entry, &index); + auto match = binary_search(cached_path.span(), entry, &index, RunnablePathComparator {}); if (match) cached_path.remove(index); @@ -1518,14 +1551,14 @@ Vector Shell::complete_program_name(StringView name, [](auto& name, auto& program) { return strncmp( name.characters_without_null_termination(), - program.characters(), + program.path.characters(), name.length()); }); if (!match) return complete_path("", name, offset, ExecutableOnly::Yes, nullptr, nullptr, escape_mode); - String completion = *match; + String completion = match->path; auto token_length = escape_token(name, escape_mode).length(); auto invariant_offset = token_length; size_t static_offset = 0; @@ -1539,11 +1572,11 @@ Vector Shell::complete_program_name(StringView name, Vector suggestions; int index = match - cached_path.data(); - for (int i = index - 1; i >= 0 && cached_path[i].starts_with(name); --i) - suggestions.append({ cached_path[i], " " }); - for (size_t i = index + 1; i < cached_path.size() && cached_path[i].starts_with(name); ++i) - suggestions.append({ cached_path[i], " " }); - suggestions.append({ cached_path[index], " " }); + for (int i = index - 1; i >= 0 && cached_path[i].path.starts_with(name); --i) + suggestions.append({ cached_path[i].path, " " }); + for (size_t i = index + 1; i < cached_path.size() && cached_path[i].path.starts_with(name); ++i) + suggestions.append({ cached_path[i].path, " " }); + suggestions.append({ cached_path[index].path, " " }); for (auto& entry : suggestions) { entry.input_offset = token_length; @@ -1670,7 +1703,7 @@ ErrorOr> Shell::complete_via_program_itself(s completion_command = expand_aliases({ completion_command }).last(); auto completion_utility_name = String::formatted("_complete_{}", completion_command.argv[0]); - if (binary_search(cached_path, completion_utility_name)) + if (binary_search(cached_path.span(), completion_utility_name, nullptr, RunnablePathComparator {}) != nullptr) completion_command.argv[0] = completion_utility_name; else if (!options.invoke_program_for_autocomplete) return Error::from_string_literal("Refusing to use the program itself as completion source"); diff --git a/Userland/Shell/Shell.h b/Userland/Shell/Shell.h index e26ed5ae9e..53d1c4d9c9 100644 --- a/Userland/Shell/Shell.h +++ b/Userland/Shell/Shell.h @@ -93,8 +93,52 @@ public: Optional position; }; + struct RunnablePath { + enum class Kind { + Builtin, + Function, + Alias, + Executable, + }; + + Kind kind; + String path; + + bool operator<(RunnablePath const& other) const + { + return path < other.path; + } + + bool operator==(RunnablePath const&) const = default; + }; + + struct RunnablePathComparator { + int operator()(RunnablePath const& lhs, RunnablePath const& rhs) + { + if (lhs.path > rhs.path) + return 1; + + if (lhs.path < rhs.path) + return -1; + + return 0; + } + + int operator()(StringView lhs, RunnablePath const& rhs) + { + if (lhs > rhs.path) + return 1; + + if (lhs < rhs.path) + return -1; + + return 0; + } + }; + int run_command(StringView, Optional = {}); - bool is_runnable(StringView); + Optional runnable_path_for(StringView); + Optional help_path_for(Vector visited, RunnablePath const& runnable_path); ErrorOr> run_command(const AST::Command&); NonnullRefPtrVector run_commands(Vector&); bool run_file(String const&, bool explicitly_invoked = true); @@ -269,7 +313,7 @@ public: Vector directory_stack; CircularQueue cd_history; // FIXME: have a configurable cd history length HashMap> jobs; - Vector cached_path; + Vector cached_path; String current_script; @@ -346,7 +390,7 @@ private: void bring_cursor_to_beginning_of_a_line() const; Optional resolve_job_spec(StringView); - void add_entry_to_cache(String const&); + void add_entry_to_cache(RunnablePath const&); void remove_entry_from_cache(StringView); void stop_all_jobs(); Job const* m_current_job { nullptr }; @@ -489,3 +533,27 @@ inline size_t find_offset_into_node(StringView unescaped_text, size_t escaped_of } } + +namespace AK { + +template<> +struct Traits : public GenericTraits { + static constexpr bool is_trivial() { return false; } + + static bool equals(Shell::Shell::RunnablePath const& self, Shell::Shell::RunnablePath const& other) + { + return self == other; + } + + static bool equals(Shell::Shell::RunnablePath const& self, StringView other) + { + return self.path == other; + } + + static bool equals(Shell::Shell::RunnablePath const& self, String const& other) + { + return self.path == other; + } +}; + +}