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)
This commit is contained in:
Fabian Homborg 2020-09-12 19:26:04 +02:00
parent 8cf389baf2
commit 568f9031aa
5 changed files with 83 additions and 19 deletions

View File

@ -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.

View File

@ -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<int> 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<int> 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");

View File

@ -403,7 +403,7 @@ maybe_t<wcstring> 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;

View File

@ -76,7 +76,7 @@ maybe_t<wcstring> 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

View File

@ -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)