From 568f9031aaeb688c3b157b0fb05090da9962a0fa Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Sat, 12 Sep 2020 19:26:04 +0200 Subject: [PATCH] builtin realpath: Add --no-symlinks option Taken from GNU realpath, this one makes realpath not resolve symlinks. It still makes paths absolute and handles duplicate and trailing slashes. (useful in fish_add_path) --- doc_src/cmds/realpath.rst | 8 +++- src/builtin_realpath.cpp | 75 +++++++++++++++++++++++++++++++------- src/wutil.cpp | 4 +- src/wutil.h | 2 +- tests/checks/realpath.fish | 13 +++++++ 5 files changed, 83 insertions(+), 19 deletions(-) diff --git a/doc_src/cmds/realpath.rst b/doc_src/cmds/realpath.rst index ed1c87670..b78aeb821 100644 --- a/doc_src/cmds/realpath.rst +++ b/doc_src/cmds/realpath.rst @@ -15,6 +15,10 @@ Description ``realpath`` resolves a path to its absolute path. -fish provides a ``realpath`` builtin as a fallback for systems where there is no ``realpath`` command. fish's implementation always resolves its first argument, and does not support any options. +fish provides a ``realpath`` builtin as a fallback for systems where there is no ``realpath`` command, your OS might provide a version with more features. -If the operation fails, an error will be reported. +If a ``realpath`` command exists, it will be preferred, so if you want to use the builtin you should use ``builtin realpath`` explicitly. + +The following options are available: + +- ``-s`` or ``--no-symlink``: Don't resolve symlinks, only make paths absolute, squash multiple slashes and remove trailing slashes. diff --git a/src/builtin_realpath.cpp b/src/builtin_realpath.cpp index 1264545c8..d648327a4 100644 --- a/src/builtin_realpath.cpp +++ b/src/builtin_realpath.cpp @@ -12,18 +12,61 @@ #include "common.h" #include "fallback.h" // IWYU pragma: keep #include "io.h" +#include "wgetopt.h" #include "wutil.h" // IWYU pragma: keep +struct realpath_cmd_opts_t { + bool print_help = false; + bool no_symlinks = false; +}; + +static const wchar_t *const short_options = L"+:hs"; +static const struct woption long_options[] = {{L"no-symlinks", no_argument, nullptr, 's'}, + {L"help", no_argument, nullptr, 'h'}, + {nullptr, 0, nullptr, 0}}; + +static int parse_cmd_opts(realpath_cmd_opts_t &opts, int *optind, //!OCLINT(high ncss method) + int argc, wchar_t **argv, parser_t &parser, io_streams_t &streams) { + const wchar_t *cmd = argv[0]; + int opt; + wgetopter_t w; + while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) { + switch (opt) { + case 's': { + opts.no_symlinks = true; + break; + } + case 'h': { + opts.print_help = true; + break; + } + case ':': { + builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]); + return STATUS_INVALID_ARGS; + } + case '?': { + builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]); + return STATUS_INVALID_ARGS; + } + default: { + DIE("unexpected retval from wgetopt_long"); + } + } + } + + *optind = w.woptind; + return STATUS_CMD_OK; +} /// An implementation of the external realpath command. Doesn't support any options. /// In general scripts shouldn't invoke this directly. They should just use `realpath` which /// will fallback to this builtin if an external command cannot be found. maybe_t builtin_realpath(parser_t &parser, io_streams_t &streams, wchar_t **argv) { const wchar_t *cmd = argv[0]; - help_only_cmd_opts_t opts; + realpath_cmd_opts_t opts; int argc = builtin_count_args(argv); int optind; - int retval = parse_help_only_cmd_opts(opts, &optind, argc, argv, parser, streams); + int retval = parse_cmd_opts(opts, &optind, argc, argv, parser, streams); if (retval != STATUS_CMD_OK) return retval; if (opts.print_help) { @@ -37,20 +80,24 @@ maybe_t builtin_realpath(parser_t &parser, io_streams_t &streams, wchar_t * return STATUS_INVALID_ARGS; } - if (auto real_path = wrealpath(argv[optind])) { - streams.out.append(*real_path); - } else { - if (errno) { - // realpath() just couldn't do it. Report the error and make it clear - // this is an error from our builtin, not the system's realpath. - streams.err.append_format(L"builtin %ls: %ls: %s\n", cmd, argv[optind], - std::strerror(errno)); + if (!opts.no_symlinks) { + if (auto real_path = wrealpath(argv[optind])) { + streams.out.append(*real_path); } else { - // Who knows. Probably a bug in our wrealpath() implementation. - streams.err.append_format(_(L"builtin %ls: Invalid path: %ls\n"), cmd, argv[optind]); - } + if (errno) { + // realpath() just couldn't do it. Report the error and make it clear + // this is an error from our builtin, not the system's realpath. + streams.err.append_format(L"builtin %ls: %ls: %s\n", cmd, argv[optind], + std::strerror(errno)); + } else { + // Who knows. Probably a bug in our wrealpath() implementation. + streams.err.append_format(_(L"builtin %ls: Invalid path: %ls\n"), cmd, argv[optind]); + } - return STATUS_CMD_ERROR; + return STATUS_CMD_ERROR; + } + } else { + streams.out.append(normalize_path(argv[optind], /* allow leading double slashes */ false)); } streams.out.append(L"\n"); diff --git a/src/wutil.cpp b/src/wutil.cpp index 5fb63abcc..af2a6f443 100644 --- a/src/wutil.cpp +++ b/src/wutil.cpp @@ -403,7 +403,7 @@ maybe_t wrealpath(const wcstring &pathname) { return str2wcstring(real_path); } -wcstring normalize_path(const wcstring &path) { +wcstring normalize_path(const wcstring &path, bool allow_leading_double_slashes) { // Count the leading slashes. const wchar_t sep = L'/'; size_t leading_slashes = 0; @@ -430,7 +430,7 @@ wcstring normalize_path(const wcstring &path) { wcstring result = join_strings(new_comps, sep); // Prepend one or two leading slashes. // Two slashes are preserved. Three+ slashes are collapsed to one. (!) - result.insert(0, leading_slashes > 2 ? 1 : leading_slashes, sep); + result.insert(0, allow_leading_double_slashes && leading_slashes > 2 ? 1 : leading_slashes, sep); // Ensure ./ normalizes to . and not empty. if (result.empty()) result.push_back(L'.'); return result; diff --git a/src/wutil.h b/src/wutil.h index 5fbf5bfcd..7bb0f995d 100644 --- a/src/wutil.h +++ b/src/wutil.h @@ -76,7 +76,7 @@ maybe_t wrealpath(const wcstring &pathname); /// 1. Collapse multiple /s into a single /, except maybe at the beginning. /// 2. .. goes up a level. /// 3. Remove /./ in the middle. -wcstring normalize_path(const wcstring &path); +wcstring normalize_path(const wcstring &path, bool allow_leading_double_slashes = true); /// Given an input path \p path and a working directory \p wd, do a "normalizing join" in a way /// appropriate for cd. That is, return effectively wd + path while resolving leading ../s from diff --git a/tests/checks/realpath.fish b/tests/checks/realpath.fish index 8a6d6646a..60560865e 100644 --- a/tests/checks/realpath.fish +++ b/tests/checks/realpath.fish @@ -62,6 +62,19 @@ else echo "fish-symlink not handled correctly: $real_path != $expected_real_path" >&2 end +# With "-s" the symlink is not resolved. +set -l real_path (builtin realpath -s $XDG_DATA_HOME/fish-symlink) +set -l expected_real_path "$XDG_DATA_HOME/fish-symlink" +if test "$real_path" = "$expected_real_path" + echo "fish-symlink handled correctly" + # CHECK: fish-symlink handled correctly +else + echo "fish-symlink not handled correctly: $real_path != $expected_real_path" >&2 +end + +test (builtin realpath -s /usr/bin/../) = "/usr" +or echo builtin realpath -s does not resolve .. or resolves symlink wrong + # A nonexistent file relative to a valid symlink to a directory gets converted. # This depends on the symlink created by the previous test. set -l real_path (builtin realpath $XDG_DATA_HOME/fish-symlink/nonexistent-file-relative-to-a-symlink)