From 0e26f2657ec30f51fc17c83e12a0dbb556d9e0d3 Mon Sep 17 00:00:00 2001 From: Vetrox <39677514+Vetrox@users.noreply.github.com> Date: Thu, 22 Dec 2022 21:51:27 +0100 Subject: [PATCH] Shell: Add `where` builtin The builtin is based on the behaviour of the z-shell. Namely it tries to resolve every argument one by one. When resolving (in the order below) the following results can occur: 1. The argument is a shell built-in command. Then print it. 2. The argument is an alias. In this case we print the mapped value. 3. The argument was found in the `PATH` environment variable. In this case we print the resolved absolute path and try to find more occurences in the `PATH` environment variable. 4. None of the above. If no earlier argument got resolved, we print the error `{argument} not found`. If at least one argument got resolved we exit with exit code 0, otherwise 1. By not using Core::File to resolve the executable in the environment but rather using a modified version of the code we print every matching executable of the given name. This behaviour matches up with the z-shell. The builtin has the following flags to modify the behaviour according to the users needs: - `-p --path-only`: This skips the built-in and alias checks (step 1 & 2) - `-s --follow-symlink`: This follows the symlinks of an executable to its symlink-free location. - `-w --type`: This displays the type of the found object without any additional descriptive information. --- Userland/Shell/Builtin.cpp | 100 +++++++++++++++++++++++++++++++++++++ Userland/Shell/Shell.h | 1 + 2 files changed, 101 insertions(+) diff --git a/Userland/Shell/Builtin.cpp b/Userland/Shell/Builtin.cpp index 989b3031c9..d49a2f4283 100644 --- a/Userland/Shell/Builtin.cpp +++ b/Userland/Shell/Builtin.cpp @@ -39,6 +39,106 @@ int Shell::builtin_dump(int argc, char const** argv) return 0; } +enum FollowSymlinks { + Yes, + No +}; + +static Vector find_matching_executables_in_path(StringView filename, FollowSymlinks follow_symlinks = FollowSymlinks::No) +{ + // Edge cases in which there are guaranteed no solutions + if (filename.is_empty() || filename.contains('/')) + return {}; + + char const* path_str = getenv("PATH"); + auto path = DEFAULT_PATH_SV; + if (path_str != nullptr) // maybe && *path_str + path = { path_str, strlen(path_str) }; + + Vector executables; + auto directories = path.split_view(':'); + for (auto directory : directories) { + auto file = DeprecatedString::formatted("{}/{}", directory, filename); + + if (follow_symlinks == FollowSymlinks::Yes) { + auto path_or_error = Core::File::read_link(file); + if (!path_or_error.is_error()) + file = path_or_error.release_value(); + } + if (access(file.characters(), X_OK) == 0) + executables.append(move(file)); + } + + return executables; +} + +int Shell::builtin_where(int argc, char const** argv) +{ + Vector arguments; + bool do_only_path_search { false }; + bool do_follow_symlinks { false }; + bool do_print_only_type { false }; + + Core::ArgsParser parser; + parser.add_positional_argument(arguments, "List of shell builtins, aliases or executables", "arguments"); + parser.add_option(do_only_path_search, "Search only for executables in the PATH environment variable", "path-only", 'p'); + parser.add_option(do_follow_symlinks, "Follow symlinks and print the symlink free path", "follow-symlink", 's'); + parser.add_option(do_print_only_type, "Print the argument type instead of a human readable description", "type", 'w'); + + if (!parser.parse(argc, const_cast(argv), Core::ArgsParser::FailureBehavior::PrintUsage)) + return 1; + + auto const lookup_alias = [do_only_path_search, &m_aliases = this->m_aliases](StringView alias) -> Optional { + if (do_only_path_search) + return {}; + return m_aliases.get(alias); + }; + + auto const lookup_builtin = [do_only_path_search](StringView builtin) -> Optional { + if (do_only_path_search) + return {}; + for (auto const& _builtin : builtin_names) { + if (_builtin == builtin) { + return builtin; + } + } + return {}; + }; + + bool at_least_one_succeded { false }; + for (auto const& argument : arguments) { + auto const alias = lookup_alias(argument); + if (alias.has_value()) { + if (do_print_only_type) + outln("{}: alias", argument); + else + outln("{}: aliased to {}", argument, alias.value()); + at_least_one_succeded = true; + } + + auto const builtin = lookup_builtin(argument); + if (builtin.has_value()) { + if (do_print_only_type) + outln("{}: builtin", builtin.value()); + else + outln("{}: shell built-in command", builtin.value()); + at_least_one_succeded = true; + } + + auto const executables = find_matching_executables_in_path(argument, do_follow_symlinks ? FollowSymlinks::Yes : FollowSymlinks::No); + for (auto const& path : executables) { + if (do_print_only_type) + outln("{}: command", argument); + else + outln(path); + at_least_one_succeded = true; + } + if (!at_least_one_succeded) + warnln("{} not found", argument); + } + return at_least_one_succeded ? 0 : 1; +} + int Shell::builtin_alias(int argc, char const** argv) { Vector arguments; diff --git a/Userland/Shell/Shell.h b/Userland/Shell/Shell.h index 35c309302d..ef5b95e25c 100644 --- a/Userland/Shell/Shell.h +++ b/Userland/Shell/Shell.h @@ -25,6 +25,7 @@ #define ENUMERATE_SHELL_BUILTINS() \ __ENUMERATE_SHELL_BUILTIN(alias) \ + __ENUMERATE_SHELL_BUILTIN(where) \ __ENUMERATE_SHELL_BUILTIN(cd) \ __ENUMERATE_SHELL_BUILTIN(cdh) \ __ENUMERATE_SHELL_BUILTIN(pwd) \