From f6fb347d98dd9aa339cce4481bd192e6a2c71d27 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Sat, 28 Aug 2021 14:45:24 +0200 Subject: [PATCH 01/63] Add "path" builtin This adds a "path" builtin that can handle paths. Implemented so far: - "path filter PATHS", filters paths according to existence and optionally type and permissions - "path base" and "path dir", run basename and dirname, respectively - "path extension PATHS", prints the extension, if any - "path strip-extension", prints the path without the extension - "path normalize PATHS", normalizes paths - removing "/./" components - and such. - "path real", does realpath - i.e. normalizing *and* link resolution. Some of these - base, dir, {strip-,}extension and normalize operate on the paths only as strings, so they handle nonexistent paths. filter and real ignore any nonexistent paths. All output is split explicitly, so paths with newlines in them are handled correctly. Alternatively, all subcommands have a "--null-input"/"-z" and "--null-output"/"-Z" option to handle null-terminated input and create null-terminated output. So find . -print0 | path base -z prints the basename of all files in the current directory, recursively. With "-Z" it also prints it null-separated. (if stdout is going to a command substitution, we probably want to skip this) All subcommands also have a "-q"/"--quiet" flag that tells them to skip output. They return true "when something happened". For match/filter that's when a file passed, for "base"/"dir"/"extension"/"strip-extension" that's when something about the path *changed*. Filtering --------- `filter` supports all the file*types* `test` has - "dir", "file", "link", "block"..., as well as the permissions - "read", "write", "exec" and things like "suid". It is missing the tty check and the check for the file being non-empty. The former is best done via `isatty`, the latter I don't think I've ever seen used. There currently is no way to only get "real" files, i.e. ignore links pointing to files. Examples -------- > path real /bin///sh /usr/bin/bash > path extension foo.mp4 mp4 > path extension ~/.config (nothing, because ".config" isn't an extension.) --- CMakeLists.txt | 2 +- doc_src/cmds/path.rst | 268 +++++++++++++++++ src/builtin.cpp | 2 + src/builtin_path.cpp | 644 +++++++++++++++++++++++++++++++++++++++++ src/builtin_path.h | 11 + tests/checks/path.fish | 108 +++++++ 6 files changed, 1034 insertions(+), 1 deletion(-) create mode 100644 doc_src/cmds/path.rst create mode 100644 src/builtin_path.cpp create mode 100644 src/builtin_path.h create mode 100644 tests/checks/path.fish diff --git a/CMakeLists.txt b/CMakeLists.txt index 51805628f..04d4f39a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -92,7 +92,7 @@ set(FISH_BUILTIN_SRCS src/builtins/disown.cpp src/builtins/echo.cpp src/builtins/emit.cpp src/builtins/eval.cpp src/builtins/exit.cpp src/builtins/fg.cpp src/builtins/function.cpp src/builtins/functions.cpp src/builtins/history.cpp - src/builtins/jobs.cpp src/builtins/math.cpp src/builtins/printf.cpp + src/builtins/jobs.cpp src/builtins/math.cpp src/builtins/printf.cpp src/builtin_path.cpp src/builtins/pwd.cpp src/builtins/random.cpp src/builtins/read.cpp src/builtins/realpath.cpp src/builtins/return.cpp src/builtins/set.cpp src/builtins/set_color.cpp src/builtins/source.cpp src/builtins/status.cpp diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst new file mode 100644 index 000000000..f10117e44 --- /dev/null +++ b/doc_src/cmds/path.rst @@ -0,0 +1,268 @@ +.. _cmd-path: + +path - manipulate and check paths +================================= + +Synopsis +-------- + +:: + + path base [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] + path dir [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] + path extension [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] + path filter [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH...] + path normalize [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] + path real [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] + path strip-extension [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] + +Description +----------- + +``path`` performs operations on paths. + +PATH arguments are taken from the command line unless standard input is connected to a pipe or a file, in which case they are read from standard input, one PATH per line. It is an error to supply PATH arguments on the command line and on standard input. + +Arguments beginning with ``-`` are normally interpreted as switches; ``--`` causes the following arguments not to be treated as switches even if they begin with ``-``. Switches and required arguments are recognized only on the command line. + +All subcommands accept a ``-q`` or ``--quiet`` switch, which suppresses the usual output but exits with the documented status. In this case these commands will quit early, without reading all of the available input. + +All subcommands also accept a ``-z`` or ``--null-in`` switch, which makes them accept arguments from stdin separated with NULL-bytes. Since paths on uni can't contain NULL, that makes it possible to handle all possible paths and read input from e.g. ``find -print0``. If arguments are given on the commandline this has no effect. + +All subcommands also accept a ``-Z`` or ``--null-out`` switch, which makes them print output separated with NULL instead of newlines. This is for further processing, e.g. passing to another ``path`` with ``--null-in``, or ``xargs -0``. This is not recommended when the output goes to the terminal or a command substitution. + +Some subcommands operate on the paths as strings and so work on nonexistent paths, while others need to access the paths themselves and so filter out nonexistent paths. + +The following subcommands are available. + +.. _cmd-path-base: + +"base" subcommand +-------------------- + +:: + + path base [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] + +``path base`` returns the basename for the given path. This is the part after the last "/", discounting trailing slashes. In other words, it is the part that is not the dirname (discounting superfluous slashes). + +It returns 0 if there was a basename, i.e. if the path wasn't empty or just slashes. + +Examples +^^^^^^^^ + +:: + + >_ path base ./foo.mp4 + foo.mp4 + + >_ path base ../banana + banana + + >_ path base /usr/bin/ + bin + +"dir" subcommand +-------------------- + +:: + + path dir [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] + +``path dir`` returns the dirname for the given path. This is the part before the last "/", discounting trailing slashes. In other words, it is the part that is not the basename (discounting superfluous slashes). + +It returns 0 if there was a dirname, i.e. if the path wasn't empty or just slashes. + +Examples +^^^^^^^^ + +:: + + >_ path dir ./foo.mp4 + . + + >_ path base ../banana + banana + + >_ path base /usr/bin/ + bin + +"extension" subcommand +----------------------- + +:: + + path extension [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] + +``path extension`` returns the extension for the given path. This is the part after (and excluding) the last ".", unless that "." followed a "/" or the basename is "." or "..", in which case there is no extension and nothing is printed. + +If the filename ends in a ".", the extension is empty, so an empty line will be printed. + +It returns 0 if there was an extension. + +Examples +^^^^^^^^ + +:: + + >_ path extension ./foo.mp4 + mp4 + + >_ path extension ../banana + # nothing, status 1 + + >_ path extension ~/.config + # nothing, status 1 + + >_ path extension ~/.config.d + d + + >_ path extension ~/.config. + # one empty line, status 0 + +"filter" subcommand +-------------------- + +:: + + path filter [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH...] + +``path filter`` returns all of the given paths that match the checks it was given. In all cases, the paths need to exist, nonexistent paths are always filtered. + +The available filters are: + +- ``-t`` or ``--type`` with the options: "dir", "file", "link", "block", "char", "fifo", "socket" and "link", in which case the path needs to be a directory, file, link, block device, character device, named pipe, socket or symbolic link, respectively. + +- ``-p`` or ``--perm`` with the options: "read", "write", and "exec", as well as "suid", "sgid", "sticky", "user" (referring to the path owner) and "group" (referring to the path's group), in which case the path needs to have all of the given permissions for the current user. + +Note that the path needs to be *any* of the given types, but have *all* of the given permissions. The filter options can either be given as multiple options, or comma-separated - ``path filter -t dir,file`` or ``path filter --type dir --type file`` are equivalent. + +And if your operating system doesn't support a "sticky" bit, checking for it will always be false, so no path will pass. + +It returns 0 if at least one path passed the filter. + +Examples +^^^^^^^^ + +:: + + >_ path filter /usr/bin /usr/argagagji + # The (hopefully) nonexistent argagagji is filtered implicitly: + /usr/bin + + >_ path filter --type file /usr/bin /usr/bin/fish + # Only fish is a file + /usr/bin/fish + + >_ path filter --type file,dir --perm exec,write /usr/bin/fish /home/me + # fish is a file, which passes, and executable, which passes, + # but probably not writable, which fails. + # + # $HOME is a directory and both writable and executable, typically. + # So it passes. + /home/me + +"normalize" subcommand +----------------------- + +:: + + path normalize [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] + +``path normalize`` returns the normalized versions of all paths. That means it squashes duplicate "/" (except for two leading "//"), collapses "../" with earlier components and removes "." components. + +It is the same as ``realpath --no-symlinks``, as it creates the "real", canonical version of the path but doesn't resolve any symlinks. As such it can operate on nonexistent paths. + +It returns 0 if any normalization was done, i.e. any given path wasn't in canonical form. + +Examples +^^^^^^^^ + +:: + + >_ path normalize /usr/bin//../../etc/fish + # The "//" is squashed and the ".." components neutralize the components before + /etc/fish + + >_ path normalize /bin//bash + # The "//" is squashed, but /bin isn't resolved even if your system links it to /usr/bin. + /bin/bash + +"real" subcommand +-------------------- + +:: + + path real [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] + +``path normalize`` returns the normalized, physical versions of all paths. That means it resolves symlinks and does what ``path normalize`` does: it squashes duplicate "/" (except for two leading "//"), collapses "../" with earlier components and removes "." components. + +It is the same as ``realpath``, as it creates the "real", canonical version of the path. As such it can't operate on nonexistent paths. + +It returns 0 if any normalization or resolution was done, i.e. any given path wasn't in canonical form. + +Examples +^^^^^^^^ + +:: + >_ path real /bin//sh + # The "//" is squashed, and /bin is resolved if your system links it to /usr/bin. + # sh here is bash (on an Archlinux system) + /usr/bin/bash + +"strip-extension" subcommand +---------------------------- + +:: + path strip-extension [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] + +``path strip-extension`` returns the given paths without the extension. This is the part after (and excluding) the last ".", unless that "." followed a "/" or the basename is "." or "..", in which case there is no extension and the full path is printed. + +This is, of course, the inverse of ``path extension``. + +It returns 0 if there was an extension. + +Examples +^^^^^^^^ + +:: + + >_ path strip.extension ./foo.mp4 + ./foo + + >_ path strip-extension ../banana + ../banana + # but status 1, because there was no extension. + + >_ path strip-extension ~/.config + /home/alfa/.config + # status 1 + + >_ path extension ~/.config.d + /home/alfa/.config + # status 0 + + >_ path extension ~/.config. + /home/alfa/.config + # status 0 + +Combining ``path`` +------------------- + +``path`` is meant to be easy to combine with itself, other tools and fish. + +This is why + +- ``path``'s output is automatically split by fish if it goes into a command substitution, so just doing ``(path ...)`` handles all paths, even those containing newlines, correctly +- ``path`` has ``--null-in`` to handle null-delimited input, and ``--null-out`` to pass on null-delimited output + +Some examples of combining ``path``:: + + # Expand all paths in the current directory, leave only executable files, and print their real path + path expand '*' -Z | path filter -zZ --perm=exec --type=file | path real -z + + # The same thing, but using find (note -maxdepth needs to come first or find will scream) + # (this also depends on your particular version of find) + find . -maxdepth 1 -type f -executable -print0 | path real -z + + set -l paths (path filter -p exec $PATH/fish -Z | path real) diff --git a/src/builtin.cpp b/src/builtin.cpp index 2b35d3de0..007aa12f8 100644 --- a/src/builtin.cpp +++ b/src/builtin.cpp @@ -65,6 +65,7 @@ #include "builtins/type.h" #include "builtins/ulimit.h" #include "builtins/wait.h" +#include "builtin_path.h" #include "common.h" #include "complete.h" #include "exec.h" @@ -393,6 +394,7 @@ static constexpr builtin_data_t builtin_datas[] = { {L"math", &builtin_math, N_(L"Evaluate math expressions")}, {L"not", &builtin_generic, N_(L"Negate exit status of job")}, {L"or", &builtin_generic, N_(L"Execute command if previous command failed")}, + {L"path", &builtin_path, N_(L"Handle paths")}, {L"printf", &builtin_printf, N_(L"Prints formatted text")}, {L"pwd", &builtin_pwd, N_(L"Print the working directory")}, {L"random", &builtin_random, N_(L"Generate random number")}, diff --git a/src/builtin_path.cpp b/src/builtin_path.cpp new file mode 100644 index 000000000..50e1f570b --- /dev/null +++ b/src/builtin_path.cpp @@ -0,0 +1,644 @@ +// Implementation of the string builtin. +#include "config.h" // IWYU pragma: keep + +#include +#include +#include + +#include +#include +#include +#include + +#include "builtin.h" +#include "common.h" +#include "fallback.h" // IWYU pragma: keep +#include "io.h" +#include "wcstringutil.h" +#include "wgetopt.h" +#include "wutil.h" // IWYU pragma: keep + +// How many bytes we read() at once. +// Bash uses 128 here, so we do too (see READ_CHUNK_SIZE). +// This should be about the size of a line. +#define PATH_CHUNK_SIZE 128 + +static void path_error(io_streams_t &streams, const wchar_t *fmt, ...) { + streams.err.append(L"path "); + va_list va; + va_start(va, fmt); + streams.err.append_formatv(fmt, va); + va_end(va); +} + +static void path_unknown_option(parser_t &parser, io_streams_t &streams, const wchar_t *subcmd, + const wchar_t *opt) { + path_error(streams, BUILTIN_ERR_UNKNOWN, subcmd, opt); + builtin_print_error_trailer(parser, streams.err, L"path"); +} + +// We read from stdin if we are the second or later process in a pipeline. +static bool path_args_from_stdin(const io_streams_t &streams) { + return streams.stdin_is_directly_redirected; +} + +static const wchar_t *path_get_arg_argv(int *argidx, const wchar_t *const *argv) { + return argv && argv[*argidx] ? argv[(*argidx)++] : nullptr; +} + +// A helper type for extracting arguments from either argv or stdin. +namespace { +class arg_iterator_t { + // The list of arguments passed to the string builtin. + const wchar_t *const *argv_; + // If using argv, index of the next argument to return. + int argidx_; + // If not using argv, a string to store bytes that have been read but not yet returned. + std::string buffer_; + // The char to split on when reading from stdin. + const char split_; + // Backing storage for the next() string. + wcstring storage_; + const io_streams_t &streams_; + + /// Reads the next argument from stdin, returning true if an argument was produced and false if + /// not. On true, the string is stored in storage_. + bool get_arg_stdin() { + assert(path_args_from_stdin(streams_) && "should not be reading from stdin"); + assert(streams_.stdin_fd >= 0 && "should have a valid fd"); + // Read in chunks from fd until buffer has a line (or the end if split_ is unset). + size_t pos; + while ((pos = buffer_.find(split_)) == std::string::npos) { + char buf[PATH_CHUNK_SIZE]; + long n = read_blocked(streams_.stdin_fd, buf, PATH_CHUNK_SIZE); + if (n == 0) { + // If we still have buffer contents, flush them, + // in case there was no trailing sep. + if (buffer_.empty()) return false; + storage_ = str2wcstring(buffer_); + buffer_.clear(); + return true; + } + if (n == -1) { + // Some error happened. We can't do anything about it, + // so ignore it. + // (read_blocked already retries for EAGAIN and EINTR) + storage_ = str2wcstring(buffer_); + buffer_.clear(); + return false; + } + buffer_.append(buf, n); + } + + // Split the buffer on the sep and return the first part. + storage_ = str2wcstring(buffer_, pos); + buffer_.erase(0, pos + 1); + return true; + } + + public: + arg_iterator_t(const wchar_t *const *argv, int argidx, const io_streams_t &streams, + char split = '\n') + : argv_(argv), argidx_(argidx), split_(split), streams_(streams) {} + + const wcstring *nextstr() { + if (path_args_from_stdin(streams_)) { + return get_arg_stdin() ? &storage_ : nullptr; + } + if (auto arg = path_get_arg_argv(&argidx_, argv_)) { + storage_ = arg; + return &storage_; + } else { + return nullptr; + } + } +}; +} // namespace + +enum { + TYPE_BLOCK = 1 << 0, /// A block device + TYPE_DIR = 1 << 1, /// A directory + TYPE_FILE = 1 << 2, /// A regular file + TYPE_LINK = 1 << 3, /// A link + TYPE_CHAR = 1 << 4, /// A character device + TYPE_FIFO = 1 << 5, /// A fifo + TYPE_SOCK = 1 << 6, /// A socket +}; +typedef uint32_t path_type_flags_t; + +enum { + PERM_READ = 1 << 0, + PERM_WRITE = 1 << 1, + PERM_EXEC = 1 << 2, + PERM_SUID = 1 << 3, + PERM_SGID = 1 << 4, + PERM_STICKY = 1 << 5, + PERM_USER = 1 << 6, + PERM_GROUP = 1 << 7, +}; +typedef uint32_t path_perm_flags_t; + +// This is used by the string subcommands to communicate with the option parser which flags are +// valid and get the result of parsing the command for flags. +struct options_t { //!OCLINT(too many fields) + bool perm_valid = false; + bool type_valid = false; + + bool null_in = false; + bool null_out = false; + + bool quiet = false; + + bool have_type = false; + path_type_flags_t type = 0; + + bool have_perm = false; + // Whether we need to check a special permission like suid. + bool have_special_perm = false; + path_perm_flags_t perm = 0; +}; + +static void path_out(io_streams_t &streams, const options_t &opts, const wcstring &str) { + if (!opts.quiet) { + if (!opts.null_out) { + streams.out.append_with_separation(str, + separation_type_t::explicitly); + } else { + streams.out.append(str); + streams.out.push_back(L'\0'); + } + } +} + +static int handle_flag_q(const wchar_t **argv, parser_t &parser, io_streams_t &streams, + const wgetopter_t &w, options_t *opts) { + UNUSED(argv); + UNUSED(parser); + UNUSED(streams); + UNUSED(w); + opts->quiet = true; + return STATUS_CMD_OK; +} + +static int handle_flag_z(const wchar_t **argv, parser_t &parser, io_streams_t &streams, + const wgetopter_t &w, options_t *opts) { + UNUSED(argv); + UNUSED(parser); + UNUSED(streams); + UNUSED(w); + opts->null_in = true; + return STATUS_CMD_OK; +} + +static int handle_flag_Z(const wchar_t **argv, parser_t &parser, io_streams_t &streams, + const wgetopter_t &w, options_t *opts) { + UNUSED(argv); + UNUSED(parser); + UNUSED(streams); + UNUSED(w); + opts->null_out = true; + return STATUS_CMD_OK; +} + +static int handle_flag_t(const wchar_t **argv, parser_t &parser, io_streams_t &streams, + const wgetopter_t &w, options_t *opts) { + if (opts->type_valid) { + if (!opts->have_type) opts->type = 0; + opts->have_type = true; + wcstring_list_t types = split_string_tok(w.woptarg, L","); + for (auto t : types) { + if (t == L"file") { + opts->type |= TYPE_FILE; + } else if (t == L"dir") { + opts->type |= TYPE_DIR; + } else if (t == L"block") { + opts->type |= TYPE_BLOCK; + } else if (t == L"char") { + opts->type |= TYPE_CHAR; + } else if (t == L"fifo") { + opts->type |= TYPE_FIFO; + } else if (t == L"socket") { + opts->type |= TYPE_SOCK; + } else if (t == L"link") { + opts->type |= TYPE_LINK; + } else { + path_error(streams, _(L"%ls: Invalid type '%ls'"), L"path", t.c_str()); + return STATUS_INVALID_ARGS; + } + } + return STATUS_CMD_OK; + } + path_unknown_option(parser, streams, argv[0], argv[w.woptind - 1]); + return STATUS_INVALID_ARGS; +} + + +static int handle_flag_p(const wchar_t **argv, parser_t &parser, io_streams_t &streams, + const wgetopter_t &w, options_t *opts) { + if (opts->perm_valid) { + if (!opts->have_perm) opts->perm = 0; + opts->have_perm = true; + wcstring_list_t perms = split_string_tok(w.woptarg, L","); + for (auto p : perms) { + if (p == L"read") { + opts->perm |= PERM_READ; + } else if (p == L"write") { + opts->perm |= PERM_WRITE; + } else if (p == L"exec") { + opts->perm |= PERM_EXEC; + } else if (p == L"suid") { + opts->perm |= PERM_SUID; + opts->have_special_perm = true; + } else if (p == L"sgid") { + opts->perm |= PERM_SGID; + opts->have_special_perm = true; + } else if (p == L"sticky") { + opts->perm |= PERM_STICKY; + opts->have_special_perm = true; + } else if (p == L"user") { + opts->perm |= PERM_USER; + opts->have_special_perm = true; + } else if (p == L"group") { + opts->perm |= PERM_GROUP; + opts->have_special_perm = true; + } else { + path_error(streams, _(L"%ls: Invalid permission '%ls'"), L"path", p.c_str()); + return STATUS_INVALID_ARGS; + } + } + return STATUS_CMD_OK; + } + path_unknown_option(parser, streams, argv[0], argv[w.woptind - 1]); + return STATUS_INVALID_ARGS; +} + +/// This constructs the wgetopt() short options string based on which arguments are valid for the +/// subcommand. We have to do this because many short flags have multiple meanings and may or may +/// not require an argument depending on the meaning. +static wcstring construct_short_opts(options_t *opts) { //!OCLINT(high npath complexity) + // All commands accept -z, -Z and -q + wcstring short_opts(L":zZq"); + if (opts->perm_valid) short_opts.append(L"p:"); + if (opts->type_valid) short_opts.append(L"t:"); + return short_opts; +} + +// Note that several long flags share the same short flag. That is okay. The caller is expected +// to indicate that a max of one of the long flags sharing a short flag is valid. +// Remember: adjust share/completions/string.fish when `string` options change +static const struct woption long_options[] = { + {L"quiet", no_argument, nullptr, 'q'}, + {L"null-input", no_argument, nullptr, 'z'}, + {L"null-output", no_argument, nullptr, 'Z'}, + {L"perm", required_argument, nullptr, 'p'}, + {L"type", required_argument, nullptr, 't'}, + {nullptr, 0, nullptr, 0}}; + +static const std::unordered_map flag_to_function = { + {'q', handle_flag_q}, + {'z', handle_flag_z}, {'Z', handle_flag_Z}, + {'t', handle_flag_t}, {'p', handle_flag_p}, +}; + +/// Parse the arguments for flags recognized by a specific string subcommand. +static int parse_opts(options_t *opts, int *optind, int argc, const wchar_t **argv, + parser_t &parser, io_streams_t &streams) { + const wchar_t *cmd = argv[0]; + wcstring short_opts = construct_short_opts(opts); + const wchar_t *short_options = short_opts.c_str(); + int opt; + wgetopter_t w; + while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) { + auto fn = flag_to_function.find(opt); + if (fn != flag_to_function.end()) { + int retval = fn->second(argv, parser, streams, w, opts); + if (retval != STATUS_CMD_OK) return retval; + } else if (opt == ':') { + streams.err.append(L"path "); // clone of string_error + builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], + false /* print_hints */); + return STATUS_INVALID_ARGS; + } else if (opt == '?') { + path_unknown_option(parser, streams, cmd, argv[w.woptind - 1]); + return STATUS_INVALID_ARGS; + } else { + DIE("unexpected retval from wgetopt_long"); + } + } + + *optind = w.woptind; + + // At this point we should not have optional args and be reading args from stdin. + if (path_args_from_stdin(streams) && argc > *optind) { + path_error(streams, BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd); + return STATUS_INVALID_ARGS; + } + + return STATUS_CMD_OK; +} + +static int path_transform(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv, + wcstring (*func)(wcstring)) { + options_t opts; + int optind; + int retval = parse_opts(&opts, &optind, argc, argv, parser, streams); + if (retval != STATUS_CMD_OK) return retval; + + int n_transformed = 0; + arg_iterator_t aiter(argv, optind, streams, opts.null_in ? '\0' : '\n'); + while (const wcstring *arg = aiter.nextstr()) { + wcstring transformed(*arg); + // Empty paths make no sense, but e.g. wbasename returns true for them. + if (arg->empty()) continue; + transformed = func(*arg); + if (transformed != *arg) { + n_transformed++; + // Return okay if path wasn't already in this form + // TODO: Is that correct? + if (opts.quiet) return STATUS_CMD_OK; + } + path_out(streams, opts, transformed); + } + + return n_transformed > 0 ? STATUS_CMD_OK : STATUS_CMD_ERROR; +} + + +static int path_base(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { + return path_transform(parser, streams, argc, argv, wbasename); +} + +static int path_dir(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { + return path_transform(parser, streams, argc, argv, wdirname); +} + +// Not a constref because this must have the same type as wdirname. +// cppcheck-suppress passedByValue +static wcstring normalize_helper(wcstring path) { + return normalize_path(path, false); +} + +static bool filter_path(options_t opts, const wcstring &path) { + // TODO: Add moar stuff: + // fifos, sockets, size greater than zero, setuid, ... + // Nothing to check, file existence is checked elsewhere. + if (!opts.have_type && !opts.have_perm) return true; + + if (opts.have_type) { + bool type_ok = false; + struct stat buf; + if (opts.type & TYPE_LINK) { + auto lret = !lwstat(path, &buf); + type_ok = lret && S_ISLNK(buf.st_mode); + } + + auto ret = !wstat(path, &buf); + if (!ret) { + // Does not exist + return false; + } + if (!type_ok && opts.type & TYPE_FILE && S_ISREG(buf.st_mode)) { + type_ok = true; + } + if (!type_ok && opts.type & TYPE_DIR && S_ISDIR(buf.st_mode)) { + type_ok = true; + } + if (!type_ok && opts.type & TYPE_BLOCK && S_ISBLK(buf.st_mode)) { + type_ok = true; + } + if (!type_ok && opts.type & TYPE_CHAR && S_ISCHR(buf.st_mode)) { + type_ok = true; + } + if (!type_ok && opts.type & TYPE_FIFO && S_ISFIFO(buf.st_mode)) { + type_ok = true; + } + if (!type_ok && opts.type & TYPE_SOCK && S_ISSOCK(buf.st_mode)) { + type_ok = true; + } + if (!type_ok) return false; + } + if (opts.have_perm) { + int amode = 0; + if (opts.perm & PERM_READ) amode |= R_OK; + if (opts.perm & PERM_WRITE) amode |= W_OK; + if (opts.perm & PERM_EXEC) amode |= X_OK; + // access returns 0 on success, + // -1 on failure. Yes, C can't even keep its bools straight. + if (waccess(path, amode)) return false; + + // Permissions that require special handling + if (opts.have_special_perm) { + struct stat buf; + auto ret = !wstat(path, &buf); + if (!ret) { + // Does not exist, WTF? + return false; + } + + if (opts.perm & PERM_SUID && !(S_ISUID & buf.st_mode)) return false; + if (opts.perm & PERM_SGID && !(S_ISGID & buf.st_mode)) return false; + if (opts.perm & PERM_USER && !(geteuid() == buf.st_uid)) return false; + if (opts.perm & PERM_GROUP && !(getegid() == buf.st_gid)) return false; + if (opts.perm & PERM_STICKY && !(S_ISVTX & buf.st_mode)) return false; + } + } + + // No filters failed. + return true; +} + +static int path_normalize(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { + return path_transform(parser, streams, argc, argv, normalize_helper); +} + +static maybe_t find_extension (const wcstring &path) { + // The extension belongs to the basename, + // if there is a "." before the last component it doesn't matter. + // e.g. ~/.config/fish/conf.d/foo + // does not have an extension! The ".d" here is not a file extension for "foo". + // And "~/.config" doesn't have an extension either - the ".config" is the filename. + wcstring filename = wbasename(path); + + // "." and ".." aren't really *files* and therefore don't have an extension. + if (filename == L"." || filename == L"..") return none(); + + // If we don't have a "." or the "." is the first in the filename, + // we do not have an extension + size_t pos = filename.find_last_of(L"."); + if (pos == wcstring::npos || pos == 0) { + return none(); + } + + // Convert pos back to what it would be in the original path. + return pos + path.size() - filename.size(); +} + +static int path_extension(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { + options_t opts; + int optind; + int retval = parse_opts(&opts, &optind, argc, argv, parser, streams); + if (retval != STATUS_CMD_OK) return retval; + + int n_transformed = 0; + arg_iterator_t aiter(argv, optind, streams, opts.null_in ? '\0' : '\n'); + while (const wcstring *arg = aiter.nextstr()) { + auto pos = find_extension(*arg); + + if (!pos) continue; + + // This ends up being empty if the filename ends with ".". + // That's arguably correct. + // + // So we print an empty string but return true, + // because there *is* an extension, it just happens to be empty. + wcstring ext = arg->substr(*pos + 1); + if (opts.quiet && !ext.empty()) { + return STATUS_CMD_OK; + } + path_out(streams, opts, ext); + n_transformed++; + } + + return n_transformed > 0 ? STATUS_CMD_OK : STATUS_CMD_ERROR; +} + +static int path_strip_extension(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { + options_t opts; + int optind; + int retval = parse_opts(&opts, &optind, argc, argv, parser, streams); + if (retval != STATUS_CMD_OK) return retval; + + int n_transformed = 0; + arg_iterator_t aiter(argv, optind, streams, opts.null_in ? '\0' : '\n'); + while (const wcstring *arg = aiter.nextstr()) { + auto pos = find_extension(*arg); + + if (!pos) { + path_out(streams, opts, *arg); + continue; + } + + // This ends up being empty if the filename ends with ".". + // That's arguably correct. + // + // So we print an empty string but return true, + // because there *is* an extension, it just happens to be empty. + wcstring ext = arg->substr(0, *pos); + if (opts.quiet && !ext.empty()) { + // Return 0 if we *had* an extension + return STATUS_CMD_OK; + } + path_out(streams, opts, ext); + n_transformed++; + } + + return n_transformed > 0 ? STATUS_CMD_OK : STATUS_CMD_ERROR; +} + +static int path_real(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { + options_t opts; + int optind; + int retval = parse_opts(&opts, &optind, argc, argv, parser, streams); + if (retval != STATUS_CMD_OK) return retval; + + int n_transformed = 0; + arg_iterator_t aiter(argv, optind, streams, opts.null_in ? '\0' : '\n'); + while (const wcstring *arg = aiter.nextstr()) { + auto real = wrealpath(*arg); + + if (!real) { + continue; + } + + // Return 0 if we found a realpath. + if (opts.quiet) { + return STATUS_CMD_OK; + } + path_out(streams, opts, *real); + n_transformed++; + } + + return n_transformed > 0 ? STATUS_CMD_OK : STATUS_CMD_ERROR; +} + + +// `path filter` +// All strings are taken to be filenames, and if they match the type/perms/etc (and exist!) +// they are passed along. +static int path_filter(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { + options_t opts; + opts.type_valid = true; + opts.perm_valid = true; + int optind; + int retval = parse_opts(&opts, &optind, argc, argv, parser, streams); + if (retval != STATUS_CMD_OK) return retval; + + int n_transformed = 0; + arg_iterator_t aiter(argv, optind, streams, opts.null_in ? '\0' : '\n'); + while (const wcstring *arg = aiter.nextstr()) { + if (filter_path(opts, *arg)) { + // If we don't have filters, check if it exists. + // (for match this is done by the glob already) + if (!opts.have_type && !opts.have_perm) { + if (waccess(*arg, F_OK)) continue; + } + + path_out(streams, opts, *arg); + n_transformed++; + if (opts.quiet) return STATUS_CMD_OK; + } + } + + return n_transformed > 0 ? STATUS_CMD_OK : STATUS_CMD_ERROR; +} + +// Keep sorted alphabetically +static constexpr const struct path_subcommand { + const wchar_t *name; + int (*handler)(parser_t &, io_streams_t &, int argc, //!OCLINT(unused param) + const wchar_t **argv); //!OCLINT(unused param) +} path_subcommands[] = { + // TODO: Which operations do we want? + // TODO: "base" or "basename"? + {L"base", &path_base}, + {L"dir", &path_dir}, + {L"extension", &path_extension}, + {L"filter", &path_filter}, + {L"normalize", &path_normalize}, + {L"real", &path_real}, + {L"strip-extension", &path_strip_extension}, +}; +ASSERT_SORTED_BY_NAME(path_subcommands); + +/// The string builtin, for manipulating strings. +maybe_t builtin_path(parser_t &parser, io_streams_t &streams, const wchar_t **argv) { + const wchar_t *cmd = argv[0]; + int argc = builtin_count_args(argv); + if (argc <= 1) { + streams.err.append_format(BUILTIN_ERR_MISSING_SUBCMD, cmd); + builtin_print_error_trailer(parser, streams.err, L"path"); + return STATUS_INVALID_ARGS; + } + + if (std::wcscmp(argv[1], L"-h") == 0 || std::wcscmp(argv[1], L"--help") == 0) { + builtin_print_help(parser, streams, L"path"); + return STATUS_CMD_OK; + } + + const wchar_t *subcmd_name = argv[1]; + const auto *subcmd = get_by_sorted_name(subcmd_name, path_subcommands); + if (!subcmd) { + streams.err.append_format(BUILTIN_ERR_INVALID_SUBCMD, cmd, subcmd_name); + builtin_print_error_trailer(parser, streams.err, L"path"); + return STATUS_INVALID_ARGS; + } + + if (argc >= 3 && (std::wcscmp(argv[2], L"-h") == 0 || std::wcscmp(argv[2], L"--help") == 0)) { + wcstring path_dash_subcommand = wcstring(argv[0]) + L"-" + subcmd_name; + builtin_print_help(parser, streams, path_dash_subcommand.c_str()); + return STATUS_CMD_OK; + } + argc--; + argv++; + return subcmd->handler(parser, streams, argc, argv); +} diff --git a/src/builtin_path.h b/src/builtin_path.h new file mode 100644 index 000000000..00fe84a6e --- /dev/null +++ b/src/builtin_path.h @@ -0,0 +1,11 @@ +// Prototypes for functions for executing builtin_string functions. +#ifndef FISH_BUILTIN_PATH_H +#define FISH_BUILTIN_PATH_H + +#include +#include + +class parser_t; + +maybe_t builtin_path(parser_t &parser, io_streams_t &streams, const wchar_t **argv); +#endif diff --git a/tests/checks/path.fish b/tests/checks/path.fish new file mode 100644 index 000000000..856777c40 --- /dev/null +++ b/tests/checks/path.fish @@ -0,0 +1,108 @@ +#RUN: %fish %s +# The "path" builtin for dealing with paths + +# Extension - for figuring out the file extension of a given path. +path extension / +or echo None +# CHECK: None + +# No extension +path extension /. +or echo Filename is just a dot, no extension +# CHECK: Filename is just a dot, no extension + +# No extension - ".foo" is the filename +path extension /.foo +or echo None again +# CHECK: None again + +path extension /foo +or echo None once more +# CHECK: None once more +path extension /foo.txt +and echo Success +# CHECK: txt +# CHECK: Success +path extension /foo.txt/bar +or echo Not even here +# CHECK: Not even here +path extension . .. +or echo No extension +# CHECK: No extension +path extension ./foo.mp4 +# CHECK: mp4 +path extension ../banana +# nothing, status 1 +echo $status +# CHECK: 1 +path extension ~/.config +# nothing, status 1 +echo $status +# CHECK: 1 +path extension ~/.config.d +# CHECK: d +path extension ~/.config. +echo $status +# one empty line, status 0 +# CHECK: +# CHECK: 0 + +path strip-extension ./foo.mp4 +# CHECK: ./foo +path strip-extension ../banana +# CHECK: ../banana +# but status 1, because there was no extension. +echo $status +# CHECK: 1 +path strip-extension ~/.config +# CHECK: {{.*}}/.config +echo $status +# CHECK: 1 + +path base ./foo.mp4 +# CHECK: foo.mp4 +path base ../banana +# CHECK: banana +path base /usr/bin/ +# CHECK: bin +path dir ./foo.mp4 +# CHECK: . +path base ../banana +# CHECK: banana +path base /usr/bin/ +# CHECK: bin + +cd $TMPDIR +mkdir -p bin +touch bin/{bash,bssh,chsh,dash,fish,slsh,ssh,zsh} +ln -s $TMPDIR/bin/bash bin/sh + +chmod +x bin/* +# We need files from here on +path filter bin argagagji +# The (hopefully) nonexistent argagagji is filtered implicitly: +# CHECK: bin +path filter --type file bin bin/fish +# Only fish is a file +# CHECK: bin/fish +chmod 500 bin/fish +path filter --type file,dir --perm exec,write bin/fish . +# fish is a file, which passes, and executable, which passes, +# but not writable, which fails. +# +# . is a directory and both writable and executable, typically. +# So it passes. +# CHECK: . + +path normalize /usr/bin//../../etc/fish +# The "//" is squashed and the ".." components neutralize the components before +# CHECK: /etc/fish +path normalize /bin//bash +# The "//" is squashed, but /bin isn't resolved even if your system links it to /usr/bin. +# CHECK: /bin/bash + +# We need to remove the rest of the path because we have no idea what its value looks like. +path real bin//sh | string match -r -- 'bin/bash$' +# The "//" is squashed, and the symlink is resolved. +# sh here is bash +# CHECK: bin/bash From 3a9c52cefafc007afb3b485c9962838440ee7e1d Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Fri, 3 Sep 2021 16:28:44 +0200 Subject: [PATCH 02/63] Add --invert to filter/match Like `grep -v`/`string match -v`. --- doc_src/cmds/path.rst | 4 +++- src/builtin_path.cpp | 25 ++++++++++++++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index f10117e44..fff3a4f73 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -11,7 +11,7 @@ Synopsis path base [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] path dir [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] path extension [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] - path filter [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH...] + path filter [(-z | --null-in)] [(-Z | --null-out)] [(-v | --invert)] [(-q | --quiet)] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH...] path normalize [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] path real [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] path strip-extension [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] @@ -139,6 +139,8 @@ Note that the path needs to be *any* of the given types, but have *all* of the g And if your operating system doesn't support a "sticky" bit, checking for it will always be false, so no path will pass. +With ``--invert``, the meaning of the filtering is inverted - any path that wouldn't pass (including by not existing) passes, and any path that would pass fails. + It returns 0 if at least one path passed the filter. Examples diff --git a/src/builtin_path.cpp b/src/builtin_path.cpp index 50e1f570b..93f86dd7a 100644 --- a/src/builtin_path.cpp +++ b/src/builtin_path.cpp @@ -143,10 +143,10 @@ typedef uint32_t path_perm_flags_t; struct options_t { //!OCLINT(too many fields) bool perm_valid = false; bool type_valid = false; + bool invert_valid = false; bool null_in = false; bool null_out = false; - bool quiet = false; bool have_type = false; @@ -156,6 +156,9 @@ struct options_t { //!OCLINT(too many fields) // Whether we need to check a special permission like suid. bool have_special_perm = false; path_perm_flags_t perm = 0; + + bool invert = false; + }; static void path_out(io_streams_t &streams, const options_t &opts, const wcstring &str) { @@ -272,6 +275,16 @@ static int handle_flag_p(const wchar_t **argv, parser_t &parser, io_streams_t &s return STATUS_INVALID_ARGS; } +static int handle_flag_v(const wchar_t **argv, parser_t &parser, io_streams_t &streams, + const wgetopter_t &w, options_t *opts) { + if (opts->invert_valid) { + opts->invert = true; + return STATUS_CMD_OK; + } + path_unknown_option(parser, streams, argv[0], argv[w.woptind - 1]); + return STATUS_INVALID_ARGS; +} + /// This constructs the wgetopt() short options string based on which arguments are valid for the /// subcommand. We have to do this because many short flags have multiple meanings and may or may /// not require an argument depending on the meaning. @@ -280,6 +293,7 @@ static wcstring construct_short_opts(options_t *opts) { //!OCLINT(high npath co wcstring short_opts(L":zZq"); if (opts->perm_valid) short_opts.append(L"p:"); if (opts->type_valid) short_opts.append(L"t:"); + if (opts->invert_valid) short_opts.append(L"v"); return short_opts; } @@ -292,10 +306,11 @@ static const struct woption long_options[] = { {L"null-output", no_argument, nullptr, 'Z'}, {L"perm", required_argument, nullptr, 'p'}, {L"type", required_argument, nullptr, 't'}, + {L"invert", required_argument, nullptr, 't'}, {nullptr, 0, nullptr, 0}}; static const std::unordered_map flag_to_function = { - {'q', handle_flag_q}, + {'q', handle_flag_q}, {'v', handle_flag_v}, {'z', handle_flag_z}, {'Z', handle_flag_Z}, {'t', handle_flag_t}, {'p', handle_flag_p}, }; @@ -562,13 +577,13 @@ static int path_real(parser_t &parser, io_streams_t &streams, int argc, const wc } -// `path filter` // All strings are taken to be filenames, and if they match the type/perms/etc (and exist!) // they are passed along. static int path_filter(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { options_t opts; opts.type_valid = true; opts.perm_valid = true; + opts.invert_valid = true; int optind; int retval = parse_opts(&opts, &optind, argc, argv, parser, streams); if (retval != STATUS_CMD_OK) return retval; @@ -576,11 +591,11 @@ static int path_filter(parser_t &parser, io_streams_t &streams, int argc, const int n_transformed = 0; arg_iterator_t aiter(argv, optind, streams, opts.null_in ? '\0' : '\n'); while (const wcstring *arg = aiter.nextstr()) { - if (filter_path(opts, *arg)) { + if ((!opts.invert || (!opts.have_perm && !opts.have_type)) && filter_path(opts, *arg)) { // If we don't have filters, check if it exists. // (for match this is done by the glob already) if (!opts.have_type && !opts.have_perm) { - if (waccess(*arg, F_OK)) continue; + if (!(!waccess(*arg, F_OK) ^ opts.invert)) continue; } path_out(streams, opts, *arg); From af1050d83f25aaa94871a871e9d366dff724ee87 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Wed, 8 Sep 2021 20:01:04 +0200 Subject: [PATCH 03/63] Update the rest of the docs for path --- doc_src/fish_for_bash_users.rst | 4 ++-- doc_src/language.rst | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/doc_src/fish_for_bash_users.rst b/doc_src/fish_for_bash_users.rst index e9f014796..68f743c37 100644 --- a/doc_src/fish_for_bash_users.rst +++ b/doc_src/fish_for_bash_users.rst @@ -83,7 +83,7 @@ See :ref:`Shell variables ` for more. Wildcards (globs) ----------------- -Fish only supports the ``*`` and ``**`` glob (and the deprecated ``?`` glob). If a glob doesn't match it fails the command (like with bash's ``failglob``) unless the command is ``for``, ``set`` or ``count`` or the glob is used with an environment override (``VAR=* command``), in which case it expands to nothing (like with bash's ``nullglob`` option). +Fish only supports the ``*`` and ``**`` glob (and the deprecated ``?`` glob) as syntax. If a glob doesn't match it fails the command (like with bash's ``failglob``) unless the command is ``for``, ``set`` or ``count`` or the glob is used with an environment override (``VAR=* command``), in which case it expands to nothing (like with bash's ``nullglob`` option). Globbing doesn't happen on expanded variables, so:: @@ -94,7 +94,7 @@ will not match any files. There are no options to control globbing so it always behaves like that. -See :ref:`Wildcards ` for more. +See :ref:`Wildcards ` for more. For more involved globbing, the :ref:`path ` builtin has the ``path expand`` and ``path match`` subcommands that feature the familiar globs from bash, plus ``**``. Quoting ------- diff --git a/doc_src/language.rst b/doc_src/language.rst index c868cd533..429eac113 100644 --- a/doc_src/language.rst +++ b/doc_src/language.rst @@ -492,6 +492,20 @@ Unlike bash (by default), fish will not pass on the literal glob character if no apt install "ncurses-*" +For more capable wildcards, see the :ref:`path ` builtin, that features the ``path expand`` and ``path match`` subcommands that have full-featured globs, including ``[a-z]`` character ranges (and sets), ``[[:alnum:]]`` character classes and ``?`` for single-character matches. An example:: + + # I want all photos I took in October to December 2019, but not the ".raw" versions + > path expand 'IMG_20191[012]*' | path match -v '*.raw' + IMG_20191002_154337675_HDR.jpg + IMG_20191002_193313306.png + IMG_20191102_195530400_HDR.gif + IMG_20191104_122747460_HDR.jpg + IMG_20191105_195601152 (1).jpg + IMG_20191201_195601152.jpg + + # Okay, now delete them + > rm (path expand 'IMG_20191[012]*' | path match -v '*.raw') + .. _expand-variable: Variable expansion From 7b6c2cb8dd489976afbc056fa39fb1170df477fd Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Tue, 14 Sep 2021 17:55:11 +0200 Subject: [PATCH 04/63] Apply suggestions from code review Co-authored-by: Johannes Altmanninger --- doc_src/cmds/path.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index fff3a4f73..aea671a7e 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -21,13 +21,13 @@ Description ``path`` performs operations on paths. -PATH arguments are taken from the command line unless standard input is connected to a pipe or a file, in which case they are read from standard input, one PATH per line. It is an error to supply PATH arguments on the command line and on standard input. +PATH arguments are taken from the command line unless standard input is connected to a pipe or a file, in which case they are read from standard input, one PATH per line. It is an error to supply PATH arguments on both the command line and on standard input. -Arguments beginning with ``-`` are normally interpreted as switches; ``--`` causes the following arguments not to be treated as switches even if they begin with ``-``. Switches and required arguments are recognized only on the command line. +Arguments starting with ``-`` are normally interpreted as switches; ``--`` causes the following arguments not to be treated as switches even if they begin with ``-``. Switches and required arguments are recognized only on the command line. All subcommands accept a ``-q`` or ``--quiet`` switch, which suppresses the usual output but exits with the documented status. In this case these commands will quit early, without reading all of the available input. -All subcommands also accept a ``-z`` or ``--null-in`` switch, which makes them accept arguments from stdin separated with NULL-bytes. Since paths on uni can't contain NULL, that makes it possible to handle all possible paths and read input from e.g. ``find -print0``. If arguments are given on the commandline this has no effect. +All subcommands also accept a ``-z`` or ``--null-in`` switch, which makes them accept arguments from stdin separated with NULL-bytes. Since Unix paths can't contain NULL, that makes it possible to handle all possible paths and read input from e.g. ``find -print0``. If arguments are given on the commandline this has no effect. All subcommands also accept a ``-Z`` or ``--null-out`` switch, which makes them print output separated with NULL instead of newlines. This is for further processing, e.g. passing to another ``path`` with ``--null-in``, or ``xargs -0``. This is not recommended when the output goes to the terminal or a command substitution. @@ -44,7 +44,7 @@ The following subcommands are available. path base [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] -``path base`` returns the basename for the given path. This is the part after the last "/", discounting trailing slashes. In other words, it is the part that is not the dirname (discounting superfluous slashes). +``path base`` returns the last path component of the given path, by removing the directory prefix and removing trailing slashes. In other words, it is the part that is not the dirname. It returns 0 if there was a basename, i.e. if the path wasn't empty or just slashes. @@ -94,7 +94,7 @@ Examples path extension [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] -``path extension`` returns the extension for the given path. This is the part after (and excluding) the last ".", unless that "." followed a "/" or the basename is "." or "..", in which case there is no extension and nothing is printed. +``path extension`` returns the extension of the given path. This is the part after (and excluding) the last ".", unless that "." followed a "/" or the basename is "." or "..", in which case there is no extension and nothing is printed. If the filename ends in a ".", the extension is empty, so an empty line will be printed. @@ -127,7 +127,7 @@ Examples path filter [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH...] -``path filter`` returns all of the given paths that match the checks it was given. In all cases, the paths need to exist, nonexistent paths are always filtered. +``path filter`` returns all of the given paths that match the given checks. In all cases, the paths need to exist, nonexistent paths are always filtered. The available filters are: From 0ff25d581cfc043b486d8be336a43299f6e547b8 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Wed, 15 Sep 2021 18:23:21 +0200 Subject: [PATCH 05/63] Infer splitting on NULL if one appears in the first PATH_MAX bytes This is theoretically sound, because a path can only be PATH_MAX - 1 bytes long, so at least the PATH_MAXest byte needs to be a NULL. The one case this could break is when something has a NULL-output mode but doesn't bother printing the NULL for only one path, and that path contains a newline. So we leave --null-in there, to force it on. --- doc_src/cmds/path.rst | 4 ++-- src/builtin_path.cpp | 40 ++++++++++++++++++++++++++-------------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index aea671a7e..d0e3cc6a4 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -27,10 +27,10 @@ Arguments starting with ``-`` are normally interpreted as switches; ``--`` cause All subcommands accept a ``-q`` or ``--quiet`` switch, which suppresses the usual output but exits with the documented status. In this case these commands will quit early, without reading all of the available input. -All subcommands also accept a ``-z`` or ``--null-in`` switch, which makes them accept arguments from stdin separated with NULL-bytes. Since Unix paths can't contain NULL, that makes it possible to handle all possible paths and read input from e.g. ``find -print0``. If arguments are given on the commandline this has no effect. - All subcommands also accept a ``-Z`` or ``--null-out`` switch, which makes them print output separated with NULL instead of newlines. This is for further processing, e.g. passing to another ``path`` with ``--null-in``, or ``xargs -0``. This is not recommended when the output goes to the terminal or a command substitution. +All subcommands also accept a ``-z`` or ``--null-in`` switch, which makes them accept arguments from stdin separated with NULL-bytes. Since Unix paths can't contain NULL, that makes it possible to handle all possible paths and read input from e.g. ``find -print0``. If arguments are given on the commandline this has no effect. This should mostly be unnecessary since ``path`` automatically starts splitting on NULL if one appears in the first PATH_MAX bytes, PATH_MAX being the operating system's maximum length for a path plus a NULL byte. + Some subcommands operate on the paths as strings and so work on nonexistent paths, while others need to access the paths themselves and so filter out nonexistent paths. The following subcommands are available. diff --git a/src/builtin_path.cpp b/src/builtin_path.cpp index 93f86dd7a..ec2d20985 100644 --- a/src/builtin_path.cpp +++ b/src/builtin_path.cpp @@ -19,9 +19,9 @@ #include "wutil.h" // IWYU pragma: keep // How many bytes we read() at once. -// Bash uses 128 here, so we do too (see READ_CHUNK_SIZE). -// This should be about the size of a line. -#define PATH_CHUNK_SIZE 128 +// We use PATH_MAX here so we always get at least one path, +// and so we can automatically detect NULL-separated input. +#define PATH_CHUNK_SIZE PATH_MAX static void path_error(io_streams_t &streams, const wchar_t *fmt, ...) { streams.err.append(L"path "); @@ -55,8 +55,13 @@ class arg_iterator_t { int argidx_; // If not using argv, a string to store bytes that have been read but not yet returned. std::string buffer_; - // The char to split on when reading from stdin. - const char split_; + // Whether we have found a char to split on yet, when reading from stdin. + // If explicitly passed, we will always split on NULL, + // if not we will split on NULL if the first PATH_MAX chunk includes one, + // or '\n' otherwise. + bool have_split_; + // The char we have decided to split on when reading from stdin. + char split_{'\0'}; // Backing storage for the next() string. wcstring storage_; const io_streams_t &streams_; @@ -68,7 +73,7 @@ class arg_iterator_t { assert(streams_.stdin_fd >= 0 && "should have a valid fd"); // Read in chunks from fd until buffer has a line (or the end if split_ is unset). size_t pos; - while ((pos = buffer_.find(split_)) == std::string::npos) { + while (!have_split_ || (pos = buffer_.find(split_)) == std::string::npos) { char buf[PATH_CHUNK_SIZE]; long n = read_blocked(streams_.stdin_fd, buf, PATH_CHUNK_SIZE); if (n == 0) { @@ -88,6 +93,14 @@ class arg_iterator_t { return false; } buffer_.append(buf, n); + if (!have_split_) { + if (buffer_.find('\0') != std::string::npos) { + split_ = '\0'; + } else { + split_ = '\n'; + } + have_split_ = true; + } } // Split the buffer on the sep and return the first part. @@ -97,9 +110,8 @@ class arg_iterator_t { } public: - arg_iterator_t(const wchar_t *const *argv, int argidx, const io_streams_t &streams, - char split = '\n') - : argv_(argv), argidx_(argidx), split_(split), streams_(streams) {} + arg_iterator_t(const wchar_t *const *argv, int argidx, const io_streams_t &streams, bool split_null) + : argv_(argv), argidx_(argidx), have_split_(split_null), streams_(streams) {} const wcstring *nextstr() { if (path_args_from_stdin(streams_)) { @@ -360,7 +372,7 @@ static int path_transform(parser_t &parser, io_streams_t &streams, int argc, con if (retval != STATUS_CMD_OK) return retval; int n_transformed = 0; - arg_iterator_t aiter(argv, optind, streams, opts.null_in ? '\0' : '\n'); + arg_iterator_t aiter(argv, optind, streams, opts.null_in); while (const wcstring *arg = aiter.nextstr()) { wcstring transformed(*arg); // Empty paths make no sense, but e.g. wbasename returns true for them. @@ -495,7 +507,7 @@ static int path_extension(parser_t &parser, io_streams_t &streams, int argc, con if (retval != STATUS_CMD_OK) return retval; int n_transformed = 0; - arg_iterator_t aiter(argv, optind, streams, opts.null_in ? '\0' : '\n'); + arg_iterator_t aiter(argv, optind, streams, opts.null_in); while (const wcstring *arg = aiter.nextstr()) { auto pos = find_extension(*arg); @@ -524,7 +536,7 @@ static int path_strip_extension(parser_t &parser, io_streams_t &streams, int arg if (retval != STATUS_CMD_OK) return retval; int n_transformed = 0; - arg_iterator_t aiter(argv, optind, streams, opts.null_in ? '\0' : '\n'); + arg_iterator_t aiter(argv, optind, streams, opts.null_in); while (const wcstring *arg = aiter.nextstr()) { auto pos = find_extension(*arg); @@ -557,7 +569,7 @@ static int path_real(parser_t &parser, io_streams_t &streams, int argc, const wc if (retval != STATUS_CMD_OK) return retval; int n_transformed = 0; - arg_iterator_t aiter(argv, optind, streams, opts.null_in ? '\0' : '\n'); + arg_iterator_t aiter(argv, optind, streams, opts.null_in); while (const wcstring *arg = aiter.nextstr()) { auto real = wrealpath(*arg); @@ -589,7 +601,7 @@ static int path_filter(parser_t &parser, io_streams_t &streams, int argc, const if (retval != STATUS_CMD_OK) return retval; int n_transformed = 0; - arg_iterator_t aiter(argv, optind, streams, opts.null_in ? '\0' : '\n'); + arg_iterator_t aiter(argv, optind, streams, opts.null_in); while (const wcstring *arg = aiter.nextstr()) { if ((!opts.invert || (!opts.have_perm && !opts.have_type)) && filter_path(opts, *arg)) { // If we don't have filters, check if it exists. From 39d4a7d13a2db011b151763c617735977fc17749 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Wed, 15 Sep 2021 18:25:23 +0200 Subject: [PATCH 06/63] Actually name the switches "--null-in" and out These were officially called "--null-input", but I just used "--null-in" everywhere, which worked because getopt allows unambiguous abbreviations. But since *I* couldn't keep it straight and the "put" is just superfluous, let's remove it. --- src/builtin_path.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/builtin_path.cpp b/src/builtin_path.cpp index ec2d20985..526c8ee18 100644 --- a/src/builtin_path.cpp +++ b/src/builtin_path.cpp @@ -314,8 +314,8 @@ static wcstring construct_short_opts(options_t *opts) { //!OCLINT(high npath co // Remember: adjust share/completions/string.fish when `string` options change static const struct woption long_options[] = { {L"quiet", no_argument, nullptr, 'q'}, - {L"null-input", no_argument, nullptr, 'z'}, - {L"null-output", no_argument, nullptr, 'Z'}, + {L"null-in", no_argument, nullptr, 'z'}, + {L"null-out", no_argument, nullptr, 'Z'}, {L"perm", required_argument, nullptr, 'p'}, {L"type", required_argument, nullptr, 't'}, {L"invert", required_argument, nullptr, 't'}, From 3f7e125b577498002641cbfa9e9b687d89e9b700 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Wed, 15 Sep 2021 18:32:42 +0200 Subject: [PATCH 07/63] Also give path nullglob behavior This is needed because you might feasibly give e.g. `path filter` globs to further match, and they might already present no results. It's also well-handled since path simply does nothing if given no paths. --- src/parse_execution.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parse_execution.cpp b/src/parse_execution.cpp index f0eee5ff3..2c317a6f2 100644 --- a/src/parse_execution.cpp +++ b/src/parse_execution.cpp @@ -891,7 +891,7 @@ end_execution_reason_t parse_execution_context_t::populate_plain_process( function_exists(L"cd", *parser) ? process_type_t::function : process_type_t::builtin; } else { // Not implicit cd. - const globspec_t glob_behavior = (cmd == L"set" || cmd == L"count") ? nullglob : failglob; + const globspec_t glob_behavior = (cmd == L"set" || cmd == L"count" || cmd == L"path") ? nullglob : failglob; // Form the list of arguments. The command is the first argument, followed by any arguments // from expanding the command, followed by the argument nodes themselves. E.g. if the // command is '$gco foo' and $gco is git checkout. From bcf6f8572f845c4c044dc9c499598d2e28ea19bf Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Fri, 17 Sep 2021 14:24:58 +0200 Subject: [PATCH 08/63] Another pass over the docs --- doc_src/cmds/path.rst | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index d0e3cc6a4..d264fb97e 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -27,7 +27,7 @@ Arguments starting with ``-`` are normally interpreted as switches; ``--`` cause All subcommands accept a ``-q`` or ``--quiet`` switch, which suppresses the usual output but exits with the documented status. In this case these commands will quit early, without reading all of the available input. -All subcommands also accept a ``-Z`` or ``--null-out`` switch, which makes them print output separated with NULL instead of newlines. This is for further processing, e.g. passing to another ``path`` with ``--null-in``, or ``xargs -0``. This is not recommended when the output goes to the terminal or a command substitution. +All subcommands also accept a ``-Z`` or ``--null-out`` switch, which makes them print output separated with NULL instead of newlines. This is for further processing, e.g. passing to another ``path``, or ``xargs -0``. This is not recommended when the output goes to the terminal or a command substitution. All subcommands also accept a ``-z`` or ``--null-in`` switch, which makes them accept arguments from stdin separated with NULL-bytes. Since Unix paths can't contain NULL, that makes it possible to handle all possible paths and read input from e.g. ``find -print0``. If arguments are given on the commandline this has no effect. This should mostly be unnecessary since ``path`` automatically starts splitting on NULL if one appears in the first PATH_MAX bytes, PATH_MAX being the operating system's maximum length for a path plus a NULL byte. @@ -44,7 +44,7 @@ The following subcommands are available. path base [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] -``path base`` returns the last path component of the given path, by removing the directory prefix and removing trailing slashes. In other words, it is the part that is not the dirname. +``path base`` returns the last path component of the given path, by removing the directory prefix and removing trailing slashes. In other words, it is the part that is not the dirname. For files you might call it the "filename". It returns 0 if there was a basename, i.e. if the path wasn't empty or just slashes. @@ -62,6 +62,14 @@ Examples >_ path base /usr/bin/ bin + >_ path base /usr/bin/* + # This prints all files in /usr/bin/ + # A selection: + cp + fish + grep + rm + "dir" subcommand -------------------- @@ -131,7 +139,7 @@ Examples The available filters are: -- ``-t`` or ``--type`` with the options: "dir", "file", "link", "block", "char", "fifo", "socket" and "link", in which case the path needs to be a directory, file, link, block device, character device, named pipe, socket or symbolic link, respectively. +- ``-t`` or ``--type`` with the options: "dir", "file", "link", "block", "char", "fifo" and "socket", in which case the path needs to be a directory, file, link, block device, character device, named pipe or socket, respectively. - ``-p`` or ``--perm`` with the options: "read", "write", and "exec", as well as "suid", "sgid", "sticky", "user" (referring to the path owner) and "group" (referring to the path's group), in which case the path needs to have all of the given permissions for the current user. @@ -197,7 +205,7 @@ Examples path real [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] -``path normalize`` returns the normalized, physical versions of all paths. That means it resolves symlinks and does what ``path normalize`` does: it squashes duplicate "/" (except for two leading "//"), collapses "../" with earlier components and removes "." components. +``path real`` returns the normalized, physical versions of all paths. That means it resolves symlinks and does what ``path normalize`` does: it squashes duplicate "/" (except for two leading "//"), collapses "../" with earlier components and removes "." components. It is the same as ``realpath``, as it creates the "real", canonical version of the path. As such it can't operate on nonexistent paths. @@ -229,7 +237,7 @@ Examples :: - >_ path strip.extension ./foo.mp4 + >_ path strip-extension ./foo.mp4 ./foo >_ path strip-extension ../banana @@ -240,11 +248,11 @@ Examples /home/alfa/.config # status 1 - >_ path extension ~/.config.d + >_ path strip-extension ~/.config.d /home/alfa/.config # status 0 - >_ path extension ~/.config. + >_ path strip-extension ~/.config. /home/alfa/.config # status 0 @@ -256,7 +264,7 @@ Combining ``path`` This is why - ``path``'s output is automatically split by fish if it goes into a command substitution, so just doing ``(path ...)`` handles all paths, even those containing newlines, correctly -- ``path`` has ``--null-in`` to handle null-delimited input, and ``--null-out`` to pass on null-delimited output +- ``path`` has ``--null-in`` to handle null-delimited input (typically automatically detected!), and ``--null-out`` to pass on null-delimited output Some examples of combining ``path``:: @@ -265,6 +273,8 @@ Some examples of combining ``path``:: # The same thing, but using find (note -maxdepth needs to come first or find will scream) # (this also depends on your particular version of find) + # Note the `-z` is unnecessary for any sensible version of find - if `path` sees a NULL, + # it will split on NULL automatically. find . -maxdepth 1 -type f -executable -print0 | path real -z set -l paths (path filter -p exec $PATH/fish -Z | path real) From 48ac2ea1e0705e94bf122dab2947a1f59929234d Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Fri, 17 Sep 2021 16:51:13 +0200 Subject: [PATCH 09/63] Address feedback --- doc_src/cmds/path.rst | 2 +- src/builtin_path.cpp | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index d264fb97e..fa823adb2 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -145,7 +145,7 @@ The available filters are: Note that the path needs to be *any* of the given types, but have *all* of the given permissions. The filter options can either be given as multiple options, or comma-separated - ``path filter -t dir,file`` or ``path filter --type dir --type file`` are equivalent. -And if your operating system doesn't support a "sticky" bit, checking for it will always be false, so no path will pass. +If your operating system doesn't support a "sticky" bit, that check will always be false, so no path will pass. With ``--invert``, the meaning of the filtering is inverted - any path that wouldn't pass (including by not existing) passes, and any path that would pass fails. diff --git a/src/builtin_path.cpp b/src/builtin_path.cpp index 526c8ee18..ab067a854 100644 --- a/src/builtin_path.cpp +++ b/src/builtin_path.cpp @@ -415,8 +415,7 @@ static bool filter_path(options_t opts, const wcstring &path) { bool type_ok = false; struct stat buf; if (opts.type & TYPE_LINK) { - auto lret = !lwstat(path, &buf); - type_ok = lret && S_ISLNK(buf.st_mode); + type_ok = !lwstat(path, &buf) && S_ISLNK(buf.st_mode); } auto ret = !wstat(path, &buf); @@ -513,11 +512,6 @@ static int path_extension(parser_t &parser, io_streams_t &streams, int argc, con if (!pos) continue; - // This ends up being empty if the filename ends with ".". - // That's arguably correct. - // - // So we print an empty string but return true, - // because there *is* an extension, it just happens to be empty. wcstring ext = arg->substr(*pos + 1); if (opts.quiet && !ext.empty()) { return STATUS_CMD_OK; @@ -607,7 +601,8 @@ static int path_filter(parser_t &parser, io_streams_t &streams, int argc, const // If we don't have filters, check if it exists. // (for match this is done by the glob already) if (!opts.have_type && !opts.have_perm) { - if (!(!waccess(*arg, F_OK) ^ opts.invert)) continue; + bool ok = !waccess(*arg, F_OK); + if (ok == opts.invert) continue; } path_out(streams, opts, *arg); From b23548b2a6592e7f372f90b960486c26a7df8ba8 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Mon, 20 Sep 2021 20:49:58 +0200 Subject: [PATCH 10/63] Add "-rwx" and "-fdl" shorthand These are short flags for "--perm=read" and "--type=link" and such. Not every type or permission has a shorthand - we don't want "-s" for "suid". So just the big three each get one. --- doc_src/cmds/path.rst | 7 +++++ src/builtin_path.cpp | 63 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index fa823adb2..b1f87a881 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -140,8 +140,10 @@ Examples The available filters are: - ``-t`` or ``--type`` with the options: "dir", "file", "link", "block", "char", "fifo" and "socket", in which case the path needs to be a directory, file, link, block device, character device, named pipe or socket, respectively. +- ``-d``, ``-f`` and ``-l`` are short for ``--type=dir``, ``--type=file`` and ``--type=link``, respectively. - ``-p`` or ``--perm`` with the options: "read", "write", and "exec", as well as "suid", "sgid", "sticky", "user" (referring to the path owner) and "group" (referring to the path's group), in which case the path needs to have all of the given permissions for the current user. +- ``-r``, ``-w`` and ``-x`` are short for ``--perm=read``, ``--perm=write`` and ``--perm=exec``, respectively. Note that the path needs to be *any* of the given types, but have *all* of the given permissions. The filter options can either be given as multiple options, or comma-separated - ``path filter -t dir,file`` or ``path filter --type dir --type file`` are equivalent. @@ -171,6 +173,11 @@ Examples # $HOME is a directory and both writable and executable, typically. # So it passes. /home/me + + >_ path filter -fdxw /usr/bin/fish /home/me + # This is the same as above: "-f" is "--type=file", "-d" is "--type=dir", + # "-x" is short for "--perm=exec" and "-w" short for "--perm=write"! + /home/me "normalize" subcommand ----------------------- diff --git a/src/builtin_path.cpp b/src/builtin_path.cpp index ab067a854..5080d0970 100644 --- a/src/builtin_path.cpp +++ b/src/builtin_path.cpp @@ -287,6 +287,56 @@ static int handle_flag_p(const wchar_t **argv, parser_t &parser, io_streams_t &s return STATUS_INVALID_ARGS; } +static int handle_flag_perms(const wchar_t **argv, parser_t &parser, io_streams_t &streams, + const wgetopter_t &w, options_t *opts, path_perm_flags_t perm ) { + if (opts->perm_valid) { + if (!opts->have_perm) opts->perm = 0; + opts->have_perm = true; + opts->perm |= perm; + return STATUS_CMD_OK; + } + path_unknown_option(parser, streams, argv[0], argv[w.woptind - 1]); + return STATUS_INVALID_ARGS; +} + +static int handle_flag_r(const wchar_t **argv, parser_t &parser, io_streams_t &streams, + const wgetopter_t &w, options_t *opts) { + return handle_flag_perms(argv, parser, streams, w, opts, PERM_READ); +} +static int handle_flag_w(const wchar_t **argv, parser_t &parser, io_streams_t &streams, + const wgetopter_t &w, options_t *opts) { + return handle_flag_perms(argv, parser, streams, w, opts, PERM_WRITE); +} +static int handle_flag_x(const wchar_t **argv, parser_t &parser, io_streams_t &streams, + const wgetopter_t &w, options_t *opts) { + return handle_flag_perms(argv, parser, streams, w, opts, PERM_EXEC); +} + +static int handle_flag_types(const wchar_t **argv, parser_t &parser, io_streams_t &streams, + const wgetopter_t &w, options_t *opts, path_type_flags_t type) { + if (opts->type_valid) { + if (!opts->have_type) opts->type = 0; + opts->have_type = true; + opts->type |= type; + return STATUS_CMD_OK; + } + path_unknown_option(parser, streams, argv[0], argv[w.woptind - 1]); + return STATUS_INVALID_ARGS; +} + +static int handle_flag_f(const wchar_t **argv, parser_t &parser, io_streams_t &streams, + const wgetopter_t &w, options_t *opts) { + return handle_flag_types(argv, parser, streams, w, opts, TYPE_FILE); +} +static int handle_flag_l(const wchar_t **argv, parser_t &parser, io_streams_t &streams, + const wgetopter_t &w, options_t *opts) { + return handle_flag_types(argv, parser, streams, w, opts, TYPE_LINK); +} +static int handle_flag_d(const wchar_t **argv, parser_t &parser, io_streams_t &streams, + const wgetopter_t &w, options_t *opts) { + return handle_flag_types(argv, parser, streams, w, opts, TYPE_DIR); +} + static int handle_flag_v(const wchar_t **argv, parser_t &parser, io_streams_t &streams, const wgetopter_t &w, options_t *opts) { if (opts->invert_valid) { @@ -303,8 +353,14 @@ static int handle_flag_v(const wchar_t **argv, parser_t &parser, io_streams_t &s static wcstring construct_short_opts(options_t *opts) { //!OCLINT(high npath complexity) // All commands accept -z, -Z and -q wcstring short_opts(L":zZq"); - if (opts->perm_valid) short_opts.append(L"p:"); - if (opts->type_valid) short_opts.append(L"t:"); + if (opts->perm_valid) { + short_opts.append(L"p:"); + short_opts.append(L"rwx"); + } + if (opts->type_valid) { + short_opts.append(L"t:"); + short_opts.append(L"fld"); + } if (opts->invert_valid) short_opts.append(L"v"); return short_opts; } @@ -325,6 +381,9 @@ static const std::unordered_map flag_to_function {'q', handle_flag_q}, {'v', handle_flag_v}, {'z', handle_flag_z}, {'Z', handle_flag_Z}, {'t', handle_flag_t}, {'p', handle_flag_p}, + {'r', handle_flag_r}, {'w', handle_flag_w}, + {'x', handle_flag_x}, {'f', handle_flag_f}, + {'l', handle_flag_l}, {'d', handle_flag_d}, }; /// Parse the arguments for flags recognized by a specific string subcommand. From efb3ae6d490df43ff23eb7647aa516e17b9db8ce Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Mon, 20 Sep 2021 20:56:45 +0200 Subject: [PATCH 11/63] Add `path is` shorthand for `path filter -q` This replaces `test -e` and such. --- doc_src/cmds/path.rst | 22 ++++++++++++++++++++++ src/builtin_path.cpp | 13 ++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index b1f87a881..af877d195 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -12,6 +12,7 @@ Synopsis path dir [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] path extension [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] path filter [(-z | --null-in)] [(-Z | --null-out)] [(-v | --invert)] [(-q | --quiet)] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH...] + path is [(-z | --null-in)] [(-Z | --null-out)] [(-v | --invert)] [(-q | --quiet)] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH...] path normalize [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] path real [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] path strip-extension [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] @@ -179,6 +180,27 @@ Examples # "-x" is short for "--perm=exec" and "-w" short for "--perm=write"! /home/me +"is" subcommand +-------------------- + +:: + + path is [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH...] + +``path is`` is short for ``path filter -q``. It returns true if any of the given files passes the filter. + +Examples +^^^^^^^^ + +:: + + >_ path is /usr/bin /usr/argagagji + # /usr/bin exists, so this returns a status of 0 (true). + >_ path is /usr/argagagji + # /usr/argagagji does not, so this returns a status of 1 (false). + >_ path is -fx /bin/sh + # /bin/sh is usually an executable file, so this returns true. + "normalize" subcommand ----------------------- diff --git a/src/builtin_path.cpp b/src/builtin_path.cpp index 5080d0970..6a81cfa75 100644 --- a/src/builtin_path.cpp +++ b/src/builtin_path.cpp @@ -644,7 +644,7 @@ static int path_real(parser_t &parser, io_streams_t &streams, int argc, const wc // All strings are taken to be filenames, and if they match the type/perms/etc (and exist!) // they are passed along. -static int path_filter(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { +static int path_filter(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv, bool is_is) { options_t opts; opts.type_valid = true; opts.perm_valid = true; @@ -652,6 +652,8 @@ static int path_filter(parser_t &parser, io_streams_t &streams, int argc, const int optind; int retval = parse_opts(&opts, &optind, argc, argv, parser, streams); if (retval != STATUS_CMD_OK) return retval; + // If we have been invoked as "path is", which is "path filter -q". + if (is_is) opts.quiet = true; int n_transformed = 0; arg_iterator_t aiter(argv, optind, streams, opts.null_in); @@ -673,6 +675,14 @@ static int path_filter(parser_t &parser, io_streams_t &streams, int argc, const return n_transformed > 0 ? STATUS_CMD_OK : STATUS_CMD_ERROR; } +static int path_filter(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { + return path_filter(parser, streams, argc, argv, false /* is_is */); +} + +static int path_is(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { + return path_filter(parser, streams, argc, argv, true /* is_is */); +} + // Keep sorted alphabetically static constexpr const struct path_subcommand { const wchar_t *name; @@ -685,6 +695,7 @@ static constexpr const struct path_subcommand { {L"dir", &path_dir}, {L"extension", &path_extension}, {L"filter", &path_filter}, + {L"is", &path_is}, {L"normalize", &path_normalize}, {L"real", &path_real}, {L"strip-extension", &path_strip_extension}, From 8b27a69ae449e3fa2a0c568b9614ba4697d3c22e Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Mon, 20 Sep 2021 22:05:13 +0200 Subject: [PATCH 12/63] Reword comments to be about path, not string No idea why this mentioned string so much. --- src/builtin_path.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/builtin_path.cpp b/src/builtin_path.cpp index 6a81cfa75..8918a028e 100644 --- a/src/builtin_path.cpp +++ b/src/builtin_path.cpp @@ -1,4 +1,4 @@ -// Implementation of the string builtin. +// Implementation of the path builtin. #include "config.h" // IWYU pragma: keep #include @@ -49,7 +49,7 @@ static const wchar_t *path_get_arg_argv(int *argidx, const wchar_t *const *argv) // A helper type for extracting arguments from either argv or stdin. namespace { class arg_iterator_t { - // The list of arguments passed to the string builtin. + // The list of arguments passed to this builtin. const wchar_t *const *argv_; // If using argv, index of the next argument to return. int argidx_; @@ -150,7 +150,7 @@ enum { }; typedef uint32_t path_perm_flags_t; -// This is used by the string subcommands to communicate with the option parser which flags are +// This is used by the subcommands to communicate with the option parser which flags are // valid and get the result of parsing the command for flags. struct options_t { //!OCLINT(too many fields) bool perm_valid = false; @@ -367,7 +367,7 @@ static wcstring construct_short_opts(options_t *opts) { //!OCLINT(high npath co // Note that several long flags share the same short flag. That is okay. The caller is expected // to indicate that a max of one of the long flags sharing a short flag is valid. -// Remember: adjust share/completions/string.fish when `string` options change +// Remember: adjust the completions in share/completions/ when options change static const struct woption long_options[] = { {L"quiet", no_argument, nullptr, 'q'}, {L"null-in", no_argument, nullptr, 'z'}, @@ -400,7 +400,7 @@ static int parse_opts(options_t *opts, int *optind, int argc, const wchar_t **ar int retval = fn->second(argv, parser, streams, w, opts); if (retval != STATUS_CMD_OK) return retval; } else if (opt == ':') { - streams.err.append(L"path "); // clone of string_error + streams.err.append(L"path "); builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], false /* print_hints */); return STATUS_INVALID_ARGS; @@ -599,10 +599,8 @@ static int path_strip_extension(parser_t &parser, io_streams_t &streams, int arg } // This ends up being empty if the filename ends with ".". - // That's arguably correct. - // - // So we print an empty string but return true, - // because there *is* an extension, it just happens to be empty. + // That's arguably correct, and results in an empty string, + // if we print anything. wcstring ext = arg->substr(0, *pos); if (opts.quiet && !ext.empty()) { // Return 0 if we *had* an extension @@ -702,7 +700,7 @@ static constexpr const struct path_subcommand { }; ASSERT_SORTED_BY_NAME(path_subcommands); -/// The string builtin, for manipulating strings. +/// The path builtin, for handling paths. maybe_t builtin_path(parser_t &parser, io_streams_t &streams, const wchar_t **argv) { const wchar_t *cmd = argv[0]; int argc = builtin_count_args(argv); From 359b487793f2a20dd4247fdb9283eec7efc60fa9 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Mon, 20 Sep 2021 22:23:45 +0200 Subject: [PATCH 13/63] Use wchar overload of find_last_of C++ is a silly language. --- src/builtin_path.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/builtin_path.cpp b/src/builtin_path.cpp index 8918a028e..70e39ad28 100644 --- a/src/builtin_path.cpp +++ b/src/builtin_path.cpp @@ -549,7 +549,7 @@ static maybe_t find_extension (const wcstring &path) { // If we don't have a "." or the "." is the first in the filename, // we do not have an extension - size_t pos = filename.find_last_of(L"."); + size_t pos = filename.find_last_of(L'.'); if (pos == wcstring::npos || pos == 0) { return none(); } From dca932eda442dce07b96d8f6e38d1fdfa728eba2 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Mon, 20 Sep 2021 22:57:02 +0200 Subject: [PATCH 14/63] Add completions for path --- share/completions/path.fish | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 share/completions/path.fish diff --git a/share/completions/path.fish b/share/completions/path.fish new file mode 100644 index 000000000..da549d524 --- /dev/null +++ b/share/completions/path.fish @@ -0,0 +1,29 @@ +# Completion for builtin path +# This follows a strict command-then-options approach, so we can just test the number of tokens +complete -f -c path -n "test (count (commandline -opc)) -le 2" -s h -l help -d "Display help and exit" +complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a base -d 'Give basename for given paths' +complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a dir -d 'Give dirname for given paths' +complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a extension -d 'Give extension for given paths' +complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a strip-extension -d 'Remove extension for given paths' +complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a normalize -d 'Normalize given paths (remove ./, resolve ../ against other components..)' +complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a real -d 'Normalize given paths and resolve symlinks' +complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a filter -d 'Print paths that match a filter' +complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a is -d 'Return true if any path matched a filter' +complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a match -d 'Match paths against a glob' +complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a expand -d 'Expand globs' +complete -f -c path -n "test (count (commandline -opc)) -ge 2" -s q -l quiet -d "Only return status, no output" +complete -f -c path -n "test (count (commandline -opc)) -ge 2" -s z -l null-in -d "Handle NULL-delimited input" +complete -f -c path -n "test (count (commandline -opc)) -ge 2" -s Z -l null-out -d "Print NULL-delimited output" +complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is match" -s v -l invert -d "Invert meaning of filters" +complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is match expand" -s t -l type -d "Filter by type" -x -a '(__fish_append , file link dir block char fifo socket)' +complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is match expand" -s f -d "Filter files" -x -a '(__fish_append , read write exec suid sgid sticky user group)' +complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is match expand" -s d -d "Filter directories" -x -a '(__fish_append , read write exec suid sgid sticky user group)' +complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is match expand" -s l -d "Filter symlinks" -x -a '(__fish_append , read write exec suid sgid sticky user group)' +complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is match expand" -s p -l perm -d "Filter by permission" -x -a '(__fish_append , read write exec suid sgid sticky user group)' +complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is match expand" -s r -d "Filter readable paths" -x -a '(__fish_append , read write exec suid sgid sticky user group)' +complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is match expand" -s w -d "Filter writable paths" -x -a '(__fish_append , read write exec suid sgid sticky user group)' +complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is match expand" -s x -d "Filter executale paths" -x -a '(__fish_append , read write exec suid sgid sticky user group)' +# Turn on file completions again. +# match takes a glob as first arg, expand takes only globs. +# We still want files completed then! +complete -F -c path -n "test (count (commandline -opc)) -ge 2" From d0e8eb1700e5a256b7096462be080b8e6e179fbb Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Thu, 23 Sep 2021 13:13:30 +0200 Subject: [PATCH 15/63] docs: Replace the general options recantation with "GENERAL_OPTIONS" I'm not sure if this is the actual proper syntax to describe this, but it sure is a heck of a lot more readable. --- doc_src/cmds/path.rst | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index af877d195..ec0aed5a9 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -8,14 +8,16 @@ Synopsis :: - path base [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] - path dir [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] - path extension [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] - path filter [(-z | --null-in)] [(-Z | --null-out)] [(-v | --invert)] [(-q | --quiet)] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH...] - path is [(-z | --null-in)] [(-Z | --null-out)] [(-v | --invert)] [(-q | --quiet)] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH...] - path normalize [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] - path real [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] - path strip-extension [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] + path base GENERAL_OPTIONS [PATH...] + path dir GENERAL_OPTIONS [PATH...] + path extension GENERAL_OPTIONS [PATH...] + path filter GENERAL_OPTIONS [(-v | --invert)] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH...] + path is GENERAL_OPTIONS [(-v | --invert)] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH...] + path normalize GENERAL_OPTIONS [PATH...] + path real GENERAL_OPTIONS [PATH...] + path strip-extension GENERAL_OPTIONS [PATH...] + + GENERAL_OPTIONS := [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] Description ----------- From 9f174d3a62b908adeb4cf942af748fbe4e25790c Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Thu, 23 Sep 2021 13:29:31 +0200 Subject: [PATCH 16/63] Moar on the docs --- doc_src/cmds/path.rst | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index ec0aed5a9..c6b6f7585 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -11,8 +11,10 @@ Synopsis path base GENERAL_OPTIONS [PATH...] path dir GENERAL_OPTIONS [PATH...] path extension GENERAL_OPTIONS [PATH...] - path filter GENERAL_OPTIONS [(-v | --invert)] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH...] - path is GENERAL_OPTIONS [(-v | --invert)] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH...] + path filter GENERAL_OPTIONS [(-v | --invert)] \ + [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH...] + path is GENERAL_OPTIONS [(-v | --invert)] [(-t | --type) TYPE] \ + [(-p | --perm) PERMISSION] [PATH...] path normalize GENERAL_OPTIONS [PATH...] path real GENERAL_OPTIONS [PATH...] path strip-extension GENERAL_OPTIONS [PATH...] @@ -156,6 +158,8 @@ With ``--invert``, the meaning of the filtering is inverted - any path that woul It returns 0 if at least one path passed the filter. +``path is`` is shorthand for ``path filter -q``, i.e. just checking without producing output, see :ref:`The is subcommand `. + Examples ^^^^^^^^ @@ -182,6 +186,8 @@ Examples # "-x" is short for "--perm=exec" and "-w" short for "--perm=write"! /home/me +.. _cmd-path-is: + "is" subcommand -------------------- @@ -189,7 +195,9 @@ Examples path is [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH...] -``path is`` is short for ``path filter -q``. It returns true if any of the given files passes the filter. +``path is`` is short for ``path filter -q``. It returns true if any of the given files passes the filter, but does not produce any output. + +``--quiet`` can still be passed for compatibility but is redundant. The options are the same as for ``path filter``. Examples ^^^^^^^^ @@ -197,9 +205,9 @@ Examples :: >_ path is /usr/bin /usr/argagagji - # /usr/bin exists, so this returns a status of 0 (true). + # /usr/bin exists, so this returns a status of 0 (true). It prints nothing. >_ path is /usr/argagagji - # /usr/argagagji does not, so this returns a status of 1 (false). + # /usr/argagagji does not, so this returns a status of 1 (false). It also prints nothing. >_ path is -fx /bin/sh # /bin/sh is usually an executable file, so this returns true. From fbfad686aa698939a6d76d62f8c051a217c28e5c Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Mon, 11 Oct 2021 21:02:17 +0200 Subject: [PATCH 17/63] Another pass over the docs --- doc_src/cmds/path.rst | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index c6b6f7585..d6f68c375 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -12,8 +12,10 @@ Synopsis path dir GENERAL_OPTIONS [PATH...] path extension GENERAL_OPTIONS [PATH...] path filter GENERAL_OPTIONS [(-v | --invert)] \ + [-d] [-f] [-l] [-r] [-w] [-x] \ [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH...] path is GENERAL_OPTIONS [(-v | --invert)] [(-t | --type) TYPE] \ + [-d] [-f] [-l] [-r] [-w] [-x] \ [(-p | --perm) PERMISSION] [PATH...] path normalize GENERAL_OPTIONS [PATH...] path real GENERAL_OPTIONS [PATH...] @@ -94,11 +96,11 @@ Examples >_ path dir ./foo.mp4 . - >_ path base ../banana - banana + >_ path dir ../banana + .. - >_ path base /usr/bin/ - bin + >_ path dir /usr/bin/ + /usr "extension" subcommand ----------------------- @@ -138,7 +140,9 @@ Examples :: - path filter [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH...] + path filter [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] \ + [-d] [-f] [-l] [-r] [-w] [-x] \ + [(-v | --invert)] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH...] ``path filter`` returns all of the given paths that match the given checks. In all cases, the paths need to exist, nonexistent paths are always filtered. @@ -186,6 +190,9 @@ Examples # "-x" is short for "--perm=exec" and "-w" short for "--perm=write"! /home/me + >_ path filter -fx $PATH/* + # Prints all possible commands - the first entry of each name is what fish would execute! + .. _cmd-path-is: "is" subcommand @@ -193,7 +200,9 @@ Examples :: - path is [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH...] + path is [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] \ + [-d] [-f] [-l] [-r] [-w] [-x] \ + [(-v | --invert)] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH...] ``path is`` is short for ``path filter -q``. It returns true if any of the given files passes the filter, but does not produce any output. @@ -265,7 +274,7 @@ Examples :: path strip-extension [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] -``path strip-extension`` returns the given paths without the extension. This is the part after (and excluding) the last ".", unless that "." followed a "/" or the basename is "." or "..", in which case there is no extension and the full path is printed. +``path strip-extension`` returns the given paths without the extension. The extension is the part after (and excluding) the last ".", unless that "." followed a "/" or the basename is "." or "..", in which case there is no extension and the full path is printed. This is, of course, the inverse of ``path extension``. From 268a9d8db3e3cd7b14ace8102b1be313ff6a8f2f Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Tue, 19 Oct 2021 17:47:21 +0200 Subject: [PATCH 18/63] Prevent some copies --- src/builtin_path.cpp | 7 +++---- src/builtin_path.h | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/builtin_path.cpp b/src/builtin_path.cpp index 70e39ad28..701788213 100644 --- a/src/builtin_path.cpp +++ b/src/builtin_path.cpp @@ -221,7 +221,7 @@ static int handle_flag_t(const wchar_t **argv, parser_t &parser, io_streams_t &s if (!opts->have_type) opts->type = 0; opts->have_type = true; wcstring_list_t types = split_string_tok(w.woptarg, L","); - for (auto t : types) { + for (const auto &t : types) { if (t == L"file") { opts->type |= TYPE_FILE; } else if (t == L"dir") { @@ -254,7 +254,7 @@ static int handle_flag_p(const wchar_t **argv, parser_t &parser, io_streams_t &s if (!opts->have_perm) opts->perm = 0; opts->have_perm = true; wcstring_list_t perms = split_string_tok(w.woptarg, L","); - for (auto p : perms) { + for (const auto &p : perms) { if (p == L"read") { opts->perm |= PERM_READ; } else if (p == L"write") { @@ -433,10 +433,9 @@ static int path_transform(parser_t &parser, io_streams_t &streams, int argc, con int n_transformed = 0; arg_iterator_t aiter(argv, optind, streams, opts.null_in); while (const wcstring *arg = aiter.nextstr()) { - wcstring transformed(*arg); // Empty paths make no sense, but e.g. wbasename returns true for them. if (arg->empty()) continue; - transformed = func(*arg); + wcstring transformed = func(*arg); if (transformed != *arg) { n_transformed++; // Return okay if path wasn't already in this form diff --git a/src/builtin_path.h b/src/builtin_path.h index 00fe84a6e..537234b83 100644 --- a/src/builtin_path.h +++ b/src/builtin_path.h @@ -1,4 +1,3 @@ -// Prototypes for functions for executing builtin_string functions. #ifndef FISH_BUILTIN_PATH_H #define FISH_BUILTIN_PATH_H From 00ed0bfb5d98b17d626f8b6bd9b4a38291f4aa47 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Sat, 23 Oct 2021 17:49:48 +0200 Subject: [PATCH 19/63] Rename base/dir to basename/dirname "dir" sounds like it asks "is it a directory". --- doc_src/cmds/path.rst | 34 +++++++++++++++++----------------- src/builtin_path.cpp | 9 ++++----- tests/checks/path.fish | 12 ++++++------ 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index d6f68c375..28f7856d0 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -8,8 +8,8 @@ Synopsis :: - path base GENERAL_OPTIONS [PATH...] - path dir GENERAL_OPTIONS [PATH...] + path basename GENERAL_OPTIONS [PATH...] + path dirname GENERAL_OPTIONS [PATH...] path extension GENERAL_OPTIONS [PATH...] path filter GENERAL_OPTIONS [(-v | --invert)] \ [-d] [-f] [-l] [-r] [-w] [-x] \ @@ -42,16 +42,16 @@ Some subcommands operate on the paths as strings and so work on nonexistent path The following subcommands are available. -.. _cmd-path-base: +.. _cmd-path-basename: -"base" subcommand --------------------- +"basename" subcommand +--------------------- :: - path base [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] + path basename [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] -``path base`` returns the last path component of the given path, by removing the directory prefix and removing trailing slashes. In other words, it is the part that is not the dirname. For files you might call it the "filename". +``path basename`` returns the last path component of the given path, by removing the directory prefix and removing trailing slashes. In other words, it is the part that is not the dirname. For files you might call it the "filename". It returns 0 if there was a basename, i.e. if the path wasn't empty or just slashes. @@ -60,16 +60,16 @@ Examples :: - >_ path base ./foo.mp4 + >_ path basename ./foo.mp4 foo.mp4 - >_ path base ../banana + >_ path basename ../banana banana - >_ path base /usr/bin/ + >_ path basename /usr/bin/ bin - >_ path base /usr/bin/* + >_ path basename /usr/bin/* # This prints all files in /usr/bin/ # A selection: cp @@ -77,14 +77,14 @@ Examples grep rm -"dir" subcommand +"dirname" subcommand -------------------- :: - path dir [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] + path dirname [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] -``path dir`` returns the dirname for the given path. This is the part before the last "/", discounting trailing slashes. In other words, it is the part that is not the basename (discounting superfluous slashes). +``path dirname`` returns the dirname for the given path. This is the part before the last "/", discounting trailing slashes. In other words, it is the part that is not the basename (discounting superfluous slashes). It returns 0 if there was a dirname, i.e. if the path wasn't empty or just slashes. @@ -93,13 +93,13 @@ Examples :: - >_ path dir ./foo.mp4 + >_ path dirname ./foo.mp4 . - >_ path dir ../banana + >_ path dirname ../banana .. - >_ path dir /usr/bin/ + >_ path dirname /usr/bin/ /usr "extension" subcommand diff --git a/src/builtin_path.cpp b/src/builtin_path.cpp index 701788213..c9f3c123e 100644 --- a/src/builtin_path.cpp +++ b/src/builtin_path.cpp @@ -449,11 +449,11 @@ static int path_transform(parser_t &parser, io_streams_t &streams, int argc, con } -static int path_base(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { +static int path_basename(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { return path_transform(parser, streams, argc, argv, wbasename); } -static int path_dir(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { +static int path_dirname(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { return path_transform(parser, streams, argc, argv, wdirname); } @@ -687,9 +687,8 @@ static constexpr const struct path_subcommand { const wchar_t **argv); //!OCLINT(unused param) } path_subcommands[] = { // TODO: Which operations do we want? - // TODO: "base" or "basename"? - {L"base", &path_base}, - {L"dir", &path_dir}, + {L"basename", &path_basename}, + {L"dirname", &path_dirname}, {L"extension", &path_extension}, {L"filter", &path_filter}, {L"is", &path_is}, diff --git a/tests/checks/path.fish b/tests/checks/path.fish index 856777c40..c6b3da8b4 100644 --- a/tests/checks/path.fish +++ b/tests/checks/path.fish @@ -59,17 +59,17 @@ path strip-extension ~/.config echo $status # CHECK: 1 -path base ./foo.mp4 +path basename ./foo.mp4 # CHECK: foo.mp4 -path base ../banana +path basename ../banana # CHECK: banana -path base /usr/bin/ +path basename /usr/bin/ # CHECK: bin -path dir ./foo.mp4 +path dirname ./foo.mp4 # CHECK: . -path base ../banana +path basename ../banana # CHECK: banana -path base /usr/bin/ +path basename /usr/bin/ # CHECK: bin cd $TMPDIR From ce7281905dec13097030f927561d9efe48dbe1a6 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Sat, 23 Oct 2021 17:57:30 +0200 Subject: [PATCH 20/63] Switch strip-extension to change-extension This allows replacing the extension, e.g. > path change-extension mp4 foo.wmv foo.mp4 --- doc_src/cmds/path.rst | 26 ++++++++++++------------- src/builtin_path.cpp | 44 ++++++++++++++++++++++++++---------------- tests/checks/path.fish | 6 +++--- 3 files changed, 43 insertions(+), 33 deletions(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index 28f7856d0..d6ddf791f 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -19,7 +19,7 @@ Synopsis [(-p | --perm) PERMISSION] [PATH...] path normalize GENERAL_OPTIONS [PATH...] path real GENERAL_OPTIONS [PATH...] - path strip-extension GENERAL_OPTIONS [PATH...] + path change-extension GENERAL_OPTIONS EXTENSION [PATH...] GENERAL_OPTIONS := [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] @@ -268,39 +268,39 @@ Examples # sh here is bash (on an Archlinux system) /usr/bin/bash -"strip-extension" subcommand ----------------------------- +"change-extension" subcommand +----------------------------- :: - path strip-extension [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] + path change-extension [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] EXTENSION [PATH...] -``path strip-extension`` returns the given paths without the extension. The extension is the part after (and excluding) the last ".", unless that "." followed a "/" or the basename is "." or "..", in which case there is no extension and the full path is printed. +``path change-extension`` returns the given paths, with their extension changed to the given new extension. The extension is the part after (and excluding) the last ".", unless that "." followed a "/" or the basename is "." or "..", in which case there is no previous extension and the new one is simply added. -This is, of course, the inverse of ``path extension``. +If the extension is empty, any previous extension is stripped, along with the ".". This is, of course, the inverse of ``path extension``. -It returns 0 if there was an extension. +It returns 0 if it was given any paths. Examples ^^^^^^^^ :: - >_ path strip-extension ./foo.mp4 - ./foo + >_ path change-extension mp4 ./foo.wmv + ./foo.mp4 - >_ path strip-extension ../banana + >_ path change-extension '' ../banana ../banana # but status 1, because there was no extension. - >_ path strip-extension ~/.config + >_ path change-extension '' ~/.config /home/alfa/.config # status 1 - >_ path strip-extension ~/.config.d + >_ path change-extension '' ~/.config.d /home/alfa/.config # status 0 - >_ path strip-extension ~/.config. + >_ path change-extension '' ~/.config. /home/alfa/.config # status 0 diff --git a/src/builtin_path.cpp b/src/builtin_path.cpp index c9f3c123e..76e12001c 100644 --- a/src/builtin_path.cpp +++ b/src/builtin_path.cpp @@ -171,6 +171,7 @@ struct options_t { //!OCLINT(too many fields) bool invert = false; + const wchar_t *arg1 = nullptr; }; static void path_out(io_streams_t &streams, const options_t &opts, const wcstring &str) { @@ -387,7 +388,7 @@ static const std::unordered_map flag_to_function }; /// Parse the arguments for flags recognized by a specific string subcommand. -static int parse_opts(options_t *opts, int *optind, int argc, const wchar_t **argv, +static int parse_opts(options_t *opts, int *optind, int n_req_args, int argc, const wchar_t **argv, parser_t &parser, io_streams_t &streams) { const wchar_t *cmd = argv[0]; wcstring short_opts = construct_short_opts(opts); @@ -414,6 +415,15 @@ static int parse_opts(options_t *opts, int *optind, int argc, const wchar_t **ar *optind = w.woptind; + if (n_req_args) { + assert(n_req_args == 1); + opts->arg1 = path_get_arg_argv(optind, argv); + if (!opts->arg1 && n_req_args == 1) { + path_error(streams, BUILTIN_ERR_ARG_COUNT0, cmd); + return STATUS_INVALID_ARGS; + } + } + // At this point we should not have optional args and be reading args from stdin. if (path_args_from_stdin(streams) && argc > *optind) { path_error(streams, BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd); @@ -427,7 +437,7 @@ static int path_transform(parser_t &parser, io_streams_t &streams, int argc, con wcstring (*func)(wcstring)) { options_t opts; int optind; - int retval = parse_opts(&opts, &optind, argc, argv, parser, streams); + int retval = parse_opts(&opts, &optind, 0, argc, argv, parser, streams); if (retval != STATUS_CMD_OK) return retval; int n_transformed = 0; @@ -560,7 +570,7 @@ static maybe_t find_extension (const wcstring &path) { static int path_extension(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { options_t opts; int optind; - int retval = parse_opts(&opts, &optind, argc, argv, parser, streams); + int retval = parse_opts(&opts, &optind, 0, argc, argv, parser, streams); if (retval != STATUS_CMD_OK) return retval; int n_transformed = 0; @@ -581,10 +591,10 @@ static int path_extension(parser_t &parser, io_streams_t &streams, int argc, con return n_transformed > 0 ? STATUS_CMD_OK : STATUS_CMD_ERROR; } -static int path_strip_extension(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { +static int path_change_extension(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { options_t opts; int optind; - int retval = parse_opts(&opts, &optind, argc, argv, parser, streams); + int retval = parse_opts(&opts, &optind, 1, argc, argv, parser, streams); if (retval != STATUS_CMD_OK) return retval; int n_transformed = 0; @@ -592,18 +602,18 @@ static int path_strip_extension(parser_t &parser, io_streams_t &streams, int arg while (const wcstring *arg = aiter.nextstr()) { auto pos = find_extension(*arg); + wcstring ext; if (!pos) { - path_out(streams, opts, *arg); - continue; + ext = *arg; + } else { + ext = arg->substr(0, *pos); } - // This ends up being empty if the filename ends with ".". - // That's arguably correct, and results in an empty string, - // if we print anything. - wcstring ext = arg->substr(0, *pos); - if (opts.quiet && !ext.empty()) { - // Return 0 if we *had* an extension - return STATUS_CMD_OK; + // Only add on the extension "." if we have something. + // That way specifying an empty extension strips it. + if (*opts.arg1) { + ext.push_back(L'.'); + ext.append(opts.arg1); } path_out(streams, opts, ext); n_transformed++; @@ -615,7 +625,7 @@ static int path_strip_extension(parser_t &parser, io_streams_t &streams, int arg static int path_real(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { options_t opts; int optind; - int retval = parse_opts(&opts, &optind, argc, argv, parser, streams); + int retval = parse_opts(&opts, &optind, 0, argc, argv, parser, streams); if (retval != STATUS_CMD_OK) return retval; int n_transformed = 0; @@ -647,7 +657,7 @@ static int path_filter(parser_t &parser, io_streams_t &streams, int argc, const opts.perm_valid = true; opts.invert_valid = true; int optind; - int retval = parse_opts(&opts, &optind, argc, argv, parser, streams); + int retval = parse_opts(&opts, &optind, 0, argc, argv, parser, streams); if (retval != STATUS_CMD_OK) return retval; // If we have been invoked as "path is", which is "path filter -q". if (is_is) opts.quiet = true; @@ -688,13 +698,13 @@ static constexpr const struct path_subcommand { } path_subcommands[] = { // TODO: Which operations do we want? {L"basename", &path_basename}, + {L"change-extension", &path_change_extension}, {L"dirname", &path_dirname}, {L"extension", &path_extension}, {L"filter", &path_filter}, {L"is", &path_is}, {L"normalize", &path_normalize}, {L"real", &path_real}, - {L"strip-extension", &path_strip_extension}, }; ASSERT_SORTED_BY_NAME(path_subcommands); diff --git a/tests/checks/path.fish b/tests/checks/path.fish index c6b3da8b4..cec500d2e 100644 --- a/tests/checks/path.fish +++ b/tests/checks/path.fish @@ -47,14 +47,14 @@ echo $status # CHECK: # CHECK: 0 -path strip-extension ./foo.mp4 +path change-extension '' ./foo.mp4 # CHECK: ./foo -path strip-extension ../banana +path change-extension '' ../banana # CHECK: ../banana # but status 1, because there was no extension. echo $status # CHECK: 1 -path strip-extension ~/.config +path change-extension '' ~/.config # CHECK: {{.*}}/.config echo $status # CHECK: 1 From 5c284731831043d5956f7c6abca0df12d3b0c383 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Sat, 23 Oct 2021 17:58:05 +0200 Subject: [PATCH 21/63] Update completions --- share/completions/path.fish | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/share/completions/path.fish b/share/completions/path.fish index da549d524..fa6256a94 100644 --- a/share/completions/path.fish +++ b/share/completions/path.fish @@ -1,10 +1,10 @@ # Completion for builtin path # This follows a strict command-then-options approach, so we can just test the number of tokens complete -f -c path -n "test (count (commandline -opc)) -le 2" -s h -l help -d "Display help and exit" -complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a base -d 'Give basename for given paths' -complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a dir -d 'Give dirname for given paths' +complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a basename -d 'Give basename for given paths' +complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a dirname -d 'Give dirname for given paths' complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a extension -d 'Give extension for given paths' -complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a strip-extension -d 'Remove extension for given paths' +complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a change-extension -d 'Change extension for given paths' complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a normalize -d 'Normalize given paths (remove ./, resolve ../ against other components..)' complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a real -d 'Normalize given paths and resolve symlinks' complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a filter -d 'Print paths that match a filter' From de0a64a0169940471978d98c884c3ce6bf447b45 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Sun, 24 Oct 2021 11:15:25 +0200 Subject: [PATCH 22/63] Update tests for change-extension's status --- tests/checks/path.fish | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/checks/path.fish b/tests/checks/path.fish index cec500d2e..e93a153f6 100644 --- a/tests/checks/path.fish +++ b/tests/checks/path.fish @@ -51,13 +51,13 @@ path change-extension '' ./foo.mp4 # CHECK: ./foo path change-extension '' ../banana # CHECK: ../banana -# but status 1, because there was no extension. +# still status 0, because there was an argument echo $status -# CHECK: 1 +# CHECK: 0 path change-extension '' ~/.config # CHECK: {{.*}}/.config echo $status -# CHECK: 1 +# CHECK: 0 path basename ./foo.mp4 # CHECK: foo.mp4 From d991096cb453c55785a0d7c7604f2d96de9f0843 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Sun, 24 Oct 2021 20:01:07 +0200 Subject: [PATCH 23/63] Add some more links in the docs --- doc_src/cmds/path.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index d6ddf791f..a940bcc99 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -135,6 +135,8 @@ Examples >_ path extension ~/.config. # one empty line, status 0 +.. _cmd-path-filter: + "filter" subcommand -------------------- @@ -146,6 +148,8 @@ Examples ``path filter`` returns all of the given paths that match the given checks. In all cases, the paths need to exist, nonexistent paths are always filtered. +This is useful when you have a list of paths that you need to check. To match a list of paths against a glob pattern, see :ref:`path match `. To run a glob pattern to generate paths, see :ref:`path expand `. + The available filters are: - ``-t`` or ``--type`` with the options: "dir", "file", "link", "block", "char", "fifo" and "socket", in which case the path needs to be a directory, file, link, block device, character device, named pipe or socket, respectively. @@ -263,6 +267,7 @@ Examples ^^^^^^^^ :: + >_ path real /bin//sh # The "//" is squashed, and /bin is resolved if your system links it to /usr/bin. # sh here is bash (on an Archlinux system) @@ -272,7 +277,9 @@ Examples ----------------------------- :: - path change-extension [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] EXTENSION [PATH...] + + path change-extension [(-z | --null-in)] [(-Z | --null-out)] \ + [(-q | --quiet)] EXTENSION [PATH...] ``path change-extension`` returns the given paths, with their extension changed to the given new extension. The extension is the part after (and excluding) the last ".", unless that "." followed a "/" or the basename is "." or "..", in which case there is no previous extension and the new one is simply added. From 37fd508a594ad36846a545a535f2c503a1e4234d Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Thu, 28 Oct 2021 18:19:07 +0200 Subject: [PATCH 24/63] Path is also a failglob exception --- doc_src/language.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc_src/language.rst b/doc_src/language.rst index 429eac113..97ad213f3 100644 --- a/doc_src/language.rst +++ b/doc_src/language.rst @@ -475,7 +475,7 @@ Examples: - ``~/.*`` matches all hidden files (also known as "dotfiles") and directories in your home directory. -For most commands, if any wildcard fails to expand, the command is not executed, :ref:`$status ` is set to nonzero, and a warning is printed. This behavior is like what bash does with ``shopt -s failglob``. There are exactly 4 exceptions, namely :ref:`set `, overriding variables in :ref:`overrides `, :ref:`count ` and :ref:`for `. Their globs will instead expand to zero arguments (so the command won't see them at all), like with ``shopt -s nullglob`` in bash. +For most commands, if any wildcard fails to expand, the command is not executed, :ref:`$status ` is set to nonzero, and a warning is printed. This behavior is like what bash does with ``shopt -s failglob``. There are exceptions, namely :ref:`set ` and :ref:`path `, overriding variables in :ref:`overrides `, :ref:`count ` and :ref:`for `. Their globs will instead expand to zero arguments (so the command won't see them at all), like with ``shopt -s nullglob`` in bash. Examples:: From 17a8dd8f6293d62e63f00097a4abcd53f0027305 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Wed, 17 Nov 2021 20:22:21 +0100 Subject: [PATCH 25/63] Move path to src/builtins --- CMakeLists.txt | 2 +- src/builtin.cpp | 2 +- src/{builtin_path.cpp => builtins/path.cpp} | 14 +++++++------- src/{builtin_path.h => builtins/path.h} | 0 4 files changed, 9 insertions(+), 9 deletions(-) rename src/{builtin_path.cpp => builtins/path.cpp} (99%) rename src/{builtin_path.h => builtins/path.h} (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 04d4f39a7..2e90030bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -92,7 +92,7 @@ set(FISH_BUILTIN_SRCS src/builtins/disown.cpp src/builtins/echo.cpp src/builtins/emit.cpp src/builtins/eval.cpp src/builtins/exit.cpp src/builtins/fg.cpp src/builtins/function.cpp src/builtins/functions.cpp src/builtins/history.cpp - src/builtins/jobs.cpp src/builtins/math.cpp src/builtins/printf.cpp src/builtin_path.cpp + src/builtins/jobs.cpp src/builtins/math.cpp src/builtins/printf.cpp src/builtins/path.cpp src/builtins/pwd.cpp src/builtins/random.cpp src/builtins/read.cpp src/builtins/realpath.cpp src/builtins/return.cpp src/builtins/set.cpp src/builtins/set_color.cpp src/builtins/source.cpp src/builtins/status.cpp diff --git a/src/builtin.cpp b/src/builtin.cpp index 007aa12f8..f60ae17cc 100644 --- a/src/builtin.cpp +++ b/src/builtin.cpp @@ -50,6 +50,7 @@ #include "builtins/history.h" #include "builtins/jobs.h" #include "builtins/math.h" +#include "builtins/path.h" #include "builtins/printf.h" #include "builtins/pwd.h" #include "builtins/random.h" @@ -65,7 +66,6 @@ #include "builtins/type.h" #include "builtins/ulimit.h" #include "builtins/wait.h" -#include "builtin_path.h" #include "common.h" #include "complete.h" #include "exec.h" diff --git a/src/builtin_path.cpp b/src/builtins/path.cpp similarity index 99% rename from src/builtin_path.cpp rename to src/builtins/path.cpp index 76e12001c..078c66f68 100644 --- a/src/builtin_path.cpp +++ b/src/builtins/path.cpp @@ -10,13 +10,13 @@ #include #include -#include "builtin.h" -#include "common.h" -#include "fallback.h" // IWYU pragma: keep -#include "io.h" -#include "wcstringutil.h" -#include "wgetopt.h" -#include "wutil.h" // IWYU pragma: keep +#include "../builtin.h" +#include "../common.h" +#include "../fallback.h" // IWYU pragma: keep +#include "../io.h" +#include "../wcstringutil.h" +#include "../wgetopt.h" +#include "../wutil.h" // IWYU pragma: keep // How many bytes we read() at once. // We use PATH_MAX here so we always get at least one path, diff --git a/src/builtin_path.h b/src/builtins/path.h similarity index 100% rename from src/builtin_path.h rename to src/builtins/path.h From 1c1e643218995fcb0399b824d5156afd2470fc2a Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Thu, 9 Dec 2021 16:36:17 +0100 Subject: [PATCH 26/63] WIP path: Make extensions start at the "." This includes the "." in what `path extension` prints. This allows distinguishing between an empty extension (just `.`) and a non-existent extension (no `.` at all). --- doc_src/cmds/path.rst | 21 +++++++++++++-------- src/builtins/path.cpp | 6 ++++-- tests/checks/path.fish | 14 +++++++++----- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index a940bcc99..694daf3df 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -109,9 +109,9 @@ Examples path extension [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] -``path extension`` returns the extension of the given path. This is the part after (and excluding) the last ".", unless that "." followed a "/" or the basename is "." or "..", in which case there is no extension and nothing is printed. +``path extension`` returns the extension of the given path. This is the part after (and including) the last ".", unless that "." followed a "/" or the basename is "." or "..", in which case there is no extension and an empty line is printed. -If the filename ends in a ".", the extension is empty, so an empty line will be printed. +If the filename ends in a ".", only a "." is printed. It returns 0 if there was an extension. @@ -121,19 +121,19 @@ Examples :: >_ path extension ./foo.mp4 - mp4 + .mp4 >_ path extension ../banana - # nothing, status 1 + # an empty line, status 1 >_ path extension ~/.config - # nothing, status 1 + # an empty line, status 1 >_ path extension ~/.config.d - d + .d >_ path extension ~/.config. - # one empty line, status 0 + . .. _cmd-path-filter: @@ -281,10 +281,12 @@ Examples path change-extension [(-z | --null-in)] [(-Z | --null-out)] \ [(-q | --quiet)] EXTENSION [PATH...] -``path change-extension`` returns the given paths, with their extension changed to the given new extension. The extension is the part after (and excluding) the last ".", unless that "." followed a "/" or the basename is "." or "..", in which case there is no previous extension and the new one is simply added. +``path change-extension`` returns the given paths, with their extension changed to the given new extension. The extension is the part after (and including) the last ".", unless that "." followed a "/" or the basename is "." or "..", in which case there is no previous extension and the new one is simply added. If the extension is empty, any previous extension is stripped, along with the ".". This is, of course, the inverse of ``path extension``. +One leading dot on the extension is ignored, so ".mp3" and "mp3" are treated the same. + It returns 0 if it was given any paths. Examples @@ -295,6 +297,9 @@ Examples >_ path change-extension mp4 ./foo.wmv ./foo.mp4 + >_ path change-extension .mp4 ./foo.wmv + ./foo.mp4 + >_ path change-extension '' ../banana ../banana # but status 1, because there was no extension. diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index 078c66f68..574f3fe54 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -580,7 +580,7 @@ static int path_extension(parser_t &parser, io_streams_t &streams, int argc, con if (!pos) continue; - wcstring ext = arg->substr(*pos + 1); + wcstring ext = arg->substr(*pos); if (opts.quiet && !ext.empty()) { return STATUS_CMD_OK; } @@ -612,7 +612,9 @@ static int path_change_extension(parser_t &parser, io_streams_t &streams, int ar // Only add on the extension "." if we have something. // That way specifying an empty extension strips it. if (*opts.arg1) { - ext.push_back(L'.'); + if (opts.arg1[0] != L'.') { + ext.push_back(L'.'); + } ext.append(opts.arg1); } path_out(streams, opts, ext); diff --git a/tests/checks/path.fish b/tests/checks/path.fish index e93a153f6..f53b6c191 100644 --- a/tests/checks/path.fish +++ b/tests/checks/path.fish @@ -21,7 +21,7 @@ or echo None once more # CHECK: None once more path extension /foo.txt and echo Success -# CHECK: txt +# CHECK: .txt # CHECK: Success path extension /foo.txt/bar or echo Not even here @@ -30,7 +30,7 @@ path extension . .. or echo No extension # CHECK: No extension path extension ./foo.mp4 -# CHECK: mp4 +# CHECK: .mp4 path extension ../banana # nothing, status 1 echo $status @@ -40,15 +40,19 @@ path extension ~/.config echo $status # CHECK: 1 path extension ~/.config.d -# CHECK: d +# CHECK: .d path extension ~/.config. echo $status -# one empty line, status 0 -# CHECK: +# status 0 +# CHECK: . # CHECK: 0 path change-extension '' ./foo.mp4 # CHECK: ./foo +path change-extension wmv ./foo.mp4 +# CHECK: ./foo.wmv +path change-extension .wmv ./foo.mp4 +# CHECK: ./foo.wmv path change-extension '' ../banana # CHECK: ../banana # still status 0, because there was an argument From 972ed612663c84a9801c713a138d9a3fdbeff517 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Tue, 25 Jan 2022 18:04:09 +0100 Subject: [PATCH 27/63] path: Docs work --- doc_src/cmds/path.rst | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index 694daf3df..a3c583113 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -134,6 +134,12 @@ Examples >_ path extension ~/.config. . + + >_ set -l path (path change-extension '' ./foo.mp4) + >_ set -l extension (path extension ./foo.mp4) + > echo $path$extension + # reconstructs the original path again. + ./foo.mp4 .. _cmd-path-filter: @@ -153,12 +159,14 @@ This is useful when you have a list of paths that you need to check. To match a The available filters are: - ``-t`` or ``--type`` with the options: "dir", "file", "link", "block", "char", "fifo" and "socket", in which case the path needs to be a directory, file, link, block device, character device, named pipe or socket, respectively. -- ``-d``, ``-f`` and ``-l`` are short for ``--type=dir``, ``--type=file`` and ``--type=link``, respectively. +- ``-d``, ``-f`` and ``-l`` are short for ``--type=dir``, ``--type=file`` and ``--type=link``, respectively. There are no shortcuts for the other types. - ``-p`` or ``--perm`` with the options: "read", "write", and "exec", as well as "suid", "sgid", "sticky", "user" (referring to the path owner) and "group" (referring to the path's group), in which case the path needs to have all of the given permissions for the current user. -- ``-r``, ``-w`` and ``-x`` are short for ``--perm=read``, ``--perm=write`` and ``--perm=exec``, respectively. +- ``-r``, ``-w`` and ``-x`` are short for ``--perm=read``, ``--perm=write`` and ``--perm=exec``, respectively. There are no shortcuts for the other permissions. -Note that the path needs to be *any* of the given types, but have *all* of the given permissions. The filter options can either be given as multiple options, or comma-separated - ``path filter -t dir,file`` or ``path filter --type dir --type file`` are equivalent. +Note that the path needs to be *any* of the given types, but have *all* of the given permissions. This is because having a path that is both writable and executable makes sense, but having a path that is both a directory and a file doesn't. Links will count as the type of the linked-to file, so links to files count as files, links to directories count as directories. + +The filter options can either be given as multiple options, or comma-separated - ``path filter -t dir,file`` or ``path filter --type dir --type file`` are equivalent. If your operating system doesn't support a "sticky" bit, that check will always be false, so no path will pass. From 4fced3ef5a42348db02083651e5f654c7ac232c7 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Tue, 25 Jan 2022 18:05:05 +0100 Subject: [PATCH 28/63] Remove sticky filter This isn't super useful, and having a caveat in the docs that it might cause the entire filter to fail is awkward. So just remove it. --- doc_src/cmds/path.rst | 4 +--- src/builtins/path.cpp | 9 ++------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index a3c583113..e6f2d62a0 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -161,15 +161,13 @@ The available filters are: - ``-t`` or ``--type`` with the options: "dir", "file", "link", "block", "char", "fifo" and "socket", in which case the path needs to be a directory, file, link, block device, character device, named pipe or socket, respectively. - ``-d``, ``-f`` and ``-l`` are short for ``--type=dir``, ``--type=file`` and ``--type=link``, respectively. There are no shortcuts for the other types. -- ``-p`` or ``--perm`` with the options: "read", "write", and "exec", as well as "suid", "sgid", "sticky", "user" (referring to the path owner) and "group" (referring to the path's group), in which case the path needs to have all of the given permissions for the current user. +- ``-p`` or ``--perm`` with the options: "read", "write", and "exec", as well as "suid", "sgid", "user" (referring to the path owner) and "group" (referring to the path's group), in which case the path needs to have all of the given permissions for the current user. - ``-r``, ``-w`` and ``-x`` are short for ``--perm=read``, ``--perm=write`` and ``--perm=exec``, respectively. There are no shortcuts for the other permissions. Note that the path needs to be *any* of the given types, but have *all* of the given permissions. This is because having a path that is both writable and executable makes sense, but having a path that is both a directory and a file doesn't. Links will count as the type of the linked-to file, so links to files count as files, links to directories count as directories. The filter options can either be given as multiple options, or comma-separated - ``path filter -t dir,file`` or ``path filter --type dir --type file`` are equivalent. -If your operating system doesn't support a "sticky" bit, that check will always be false, so no path will pass. - With ``--invert``, the meaning of the filtering is inverted - any path that wouldn't pass (including by not existing) passes, and any path that would pass fails. It returns 0 if at least one path passed the filter. diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index 574f3fe54..33ccc4ee3 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -144,9 +144,8 @@ enum { PERM_EXEC = 1 << 2, PERM_SUID = 1 << 3, PERM_SGID = 1 << 4, - PERM_STICKY = 1 << 5, - PERM_USER = 1 << 6, - PERM_GROUP = 1 << 7, + PERM_USER = 1 << 5, + PERM_GROUP = 1 << 6, }; typedef uint32_t path_perm_flags_t; @@ -268,9 +267,6 @@ static int handle_flag_p(const wchar_t **argv, parser_t &parser, io_streams_t &s } else if (p == L"sgid") { opts->perm |= PERM_SGID; opts->have_special_perm = true; - } else if (p == L"sticky") { - opts->perm |= PERM_STICKY; - opts->have_special_perm = true; } else if (p == L"user") { opts->perm |= PERM_USER; opts->have_special_perm = true; @@ -533,7 +529,6 @@ static bool filter_path(options_t opts, const wcstring &path) { if (opts.perm & PERM_SGID && !(S_ISGID & buf.st_mode)) return false; if (opts.perm & PERM_USER && !(geteuid() == buf.st_uid)) return false; if (opts.perm & PERM_GROUP && !(getegid() == buf.st_gid)) return false; - if (opts.perm & PERM_STICKY && !(S_ISVTX & buf.st_mode)) return false; } } From 479fde27d7aff1da19a3f7310ce8f0b27ebb31ac Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Tue, 25 Jan 2022 21:08:07 +0100 Subject: [PATCH 29/63] path: Make path real "work" with nonexistent paths This just goes back until it finds an existent path, resolves that, and adds the normalized rest on top. So if you try /bin/foo/bar////../baz and /bin exists as a symlink to /usr/bin, it would resolve that, and normalize the rest, giving /usr/bin/foo/baz (note: We might want to add this to realpath as well?) --- src/builtins/path.cpp | 23 ++++++++++++++++++++++- tests/checks/path.fish | 11 +++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index 33ccc4ee3..82b1dd7f7 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -631,7 +631,28 @@ static int path_real(parser_t &parser, io_streams_t &streams, int argc, const wc auto real = wrealpath(*arg); if (!real) { - continue; + // The path doesn't exist, so we go up until we find + // something that does. + wcstring next = *arg; + // First add $PWD if we're relative + if (!next.empty() && next[0] != L'/') { + next = wgetcwd() + L"/" + next; + } + auto rest = wbasename(next); + while(!next.empty() && next != L"/") { + next = wdirname(next); + real = wrealpath(next); + if (real) { + next.push_back(L'/'); + next.append(rest); + real = normalize_path(next, false); + break; + } + rest = wbasename(next) + L'/' + rest; + } + if (!real) { + continue; + } } // Return 0 if we found a realpath. diff --git a/tests/checks/path.fish b/tests/checks/path.fish index f53b6c191..1a8de987b 100644 --- a/tests/checks/path.fish +++ b/tests/checks/path.fish @@ -110,3 +110,14 @@ path real bin//sh | string match -r -- 'bin/bash$' # The "//" is squashed, and the symlink is resolved. # sh here is bash # CHECK: bin/bash + +# `path real` with nonexistent paths +set -l path (path real foo/bar) +string match -rq "^"(string escape --style=regex -- $PWD)'/' -- $path +and echo It matches pwd! +# CHECK: It matches pwd! +string replace -r "^"(string escape --style=regex -- $PWD)'/' "" -- $path +# CHECK: foo/bar + +path real /banana//terracota/terracota/booooo/../pie +# CHECK: /banana/terracota/terracota/pie From 5844164feb7f7feb6398ab981d44a8980ab85ec9 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Tue, 25 Jan 2022 21:25:15 +0100 Subject: [PATCH 30/63] document real change --- doc_src/cmds/path.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index e6f2d62a0..54fa8eb61 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -265,7 +265,7 @@ Examples ``path real`` returns the normalized, physical versions of all paths. That means it resolves symlinks and does what ``path normalize`` does: it squashes duplicate "/" (except for two leading "//"), collapses "../" with earlier components and removes "." components. -It is the same as ``realpath``, as it creates the "real", canonical version of the path. As such it can't operate on nonexistent paths. +It is the same as ``realpath``, as it creates the "real", canonical version of the path. However, for nonexistent paths it will resolve as far as it can and normalize the nonexistent part. It returns 0 if any normalization or resolution was done, i.e. any given path wasn't in canonical form. @@ -279,6 +279,11 @@ Examples # sh here is bash (on an Archlinux system) /usr/bin/bash + >_ path real /bin/foo///bar/../baz + # Assuming /bin exists and is a symlink to /usr/bin, but /bin/foo doesn't. + # This resolves the /bin/ and normalizes the nonexistent rest: + /usr/bin/foo/baz + "change-extension" subcommand ----------------------------- From 2b8bb5bd7f3adbf7e2f1f01ab9bb8d3ac645794c Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Fri, 28 Jan 2022 17:22:16 +0100 Subject: [PATCH 31/63] path: Rename "real" to "resolve" --- doc_src/cmds/path.rst | 20 ++++++++++---------- src/builtins/path.cpp | 4 ++-- tests/checks/path.fish | 8 ++++---- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index 54fa8eb61..458656b0b 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -18,7 +18,7 @@ Synopsis [-d] [-f] [-l] [-r] [-w] [-x] \ [(-p | --perm) PERMISSION] [PATH...] path normalize GENERAL_OPTIONS [PATH...] - path real GENERAL_OPTIONS [PATH...] + path resolve GENERAL_OPTIONS [PATH...] path change-extension GENERAL_OPTIONS EXTENSION [PATH...] GENERAL_OPTIONS := [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] @@ -256,14 +256,14 @@ Examples # The "//" is squashed, but /bin isn't resolved even if your system links it to /usr/bin. /bin/bash -"real" subcommand +"resolve" subcommand -------------------- :: - path real [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] + path resolve [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] -``path real`` returns the normalized, physical versions of all paths. That means it resolves symlinks and does what ``path normalize`` does: it squashes duplicate "/" (except for two leading "//"), collapses "../" with earlier components and removes "." components. +``path resolve`` returns the normalized, physical versions of all paths. That means it resolves symlinks and does what ``path normalize`` does: it squashes duplicate "/" (except for two leading "//"), collapses "../" with earlier components and removes "." components. It is the same as ``realpath``, as it creates the "real", canonical version of the path. However, for nonexistent paths it will resolve as far as it can and normalize the nonexistent part. @@ -274,12 +274,12 @@ Examples :: - >_ path real /bin//sh + >_ path resolve /bin//sh # The "//" is squashed, and /bin is resolved if your system links it to /usr/bin. # sh here is bash (on an Archlinux system) /usr/bin/bash - >_ path real /bin/foo///bar/../baz + >_ path resolve /bin/foo///bar/../baz # Assuming /bin exists and is a symlink to /usr/bin, but /bin/foo doesn't. # This resolves the /bin/ and normalizes the nonexistent rest: /usr/bin/foo/baz @@ -339,13 +339,13 @@ This is why Some examples of combining ``path``:: - # Expand all paths in the current directory, leave only executable files, and print their real path - path expand '*' -Z | path filter -zZ --perm=exec --type=file | path real -z + # Expand all paths in the current directory, leave only executable files, and print their resolved path + path expand '*' -Z | path filter -zZ --perm=exec --type=file | path resolve -z # The same thing, but using find (note -maxdepth needs to come first or find will scream) # (this also depends on your particular version of find) # Note the `-z` is unnecessary for any sensible version of find - if `path` sees a NULL, # it will split on NULL automatically. - find . -maxdepth 1 -type f -executable -print0 | path real -z + find . -maxdepth 1 -type f -executable -print0 | path resolve -z - set -l paths (path filter -p exec $PATH/fish -Z | path real) + set -l paths (path filter -p exec $PATH/fish -Z | path resolve) diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index 82b1dd7f7..b930a5c62 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -619,7 +619,7 @@ static int path_change_extension(parser_t &parser, io_streams_t &streams, int ar return n_transformed > 0 ? STATUS_CMD_OK : STATUS_CMD_ERROR; } -static int path_real(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { +static int path_resolve(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { options_t opts; int optind; int retval = parse_opts(&opts, &optind, 0, argc, argv, parser, streams); @@ -722,7 +722,7 @@ static constexpr const struct path_subcommand { {L"filter", &path_filter}, {L"is", &path_is}, {L"normalize", &path_normalize}, - {L"real", &path_real}, + {L"resolve", &path_resolve}, }; ASSERT_SORTED_BY_NAME(path_subcommands); diff --git a/tests/checks/path.fish b/tests/checks/path.fish index 1a8de987b..bd8564de3 100644 --- a/tests/checks/path.fish +++ b/tests/checks/path.fish @@ -106,18 +106,18 @@ path normalize /bin//bash # CHECK: /bin/bash # We need to remove the rest of the path because we have no idea what its value looks like. -path real bin//sh | string match -r -- 'bin/bash$' +path resolve bin//sh | string match -r -- 'bin/bash$' # The "//" is squashed, and the symlink is resolved. # sh here is bash # CHECK: bin/bash -# `path real` with nonexistent paths -set -l path (path real foo/bar) +# `path resolve` with nonexistent paths +set -l path (path resolve foo/bar) string match -rq "^"(string escape --style=regex -- $PWD)'/' -- $path and echo It matches pwd! # CHECK: It matches pwd! string replace -r "^"(string escape --style=regex -- $PWD)'/' "" -- $path # CHECK: foo/bar -path real /banana//terracota/terracota/booooo/../pie +path resolve /banana//terracota/terracota/booooo/../pie # CHECK: /banana/terracota/terracota/pie From 80e04a1e86191894e1144c10ccb26a9611a1bbdf Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Tue, 1 Feb 2022 21:12:53 +0100 Subject: [PATCH 32/63] Rename real to resolve also in completions --- share/completions/path.fish | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/completions/path.fish b/share/completions/path.fish index fa6256a94..c8dbfe92a 100644 --- a/share/completions/path.fish +++ b/share/completions/path.fish @@ -6,7 +6,7 @@ complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a dirname -d 'Gi complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a extension -d 'Give extension for given paths' complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a change-extension -d 'Change extension for given paths' complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a normalize -d 'Normalize given paths (remove ./, resolve ../ against other components..)' -complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a real -d 'Normalize given paths and resolve symlinks' +complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a resolve -d 'Normalize given paths and resolve symlinks' complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a filter -d 'Print paths that match a filter' complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a is -d 'Return true if any path matched a filter' complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a match -d 'Match paths against a glob' From d13ba046b01fefa1fe5242d375fc949969915ef4 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Wed, 2 Feb 2022 19:25:40 +0100 Subject: [PATCH 33/63] resolve: Use the new real path This failed for /bin/foo/bar if /bin is a symlink to /usr/bin and foo doesn't exist. It returned /bin/foo/bar instead of the correct /usr/bin/foo/bar. --- src/builtins/path.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index b930a5c62..de6a28c33 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -643,9 +643,9 @@ static int path_resolve(parser_t &parser, io_streams_t &streams, int argc, const next = wdirname(next); real = wrealpath(next); if (real) { - next.push_back(L'/'); - next.append(rest); - real = normalize_path(next, false); + real->push_back(L'/'); + real->append(rest); + real = normalize_path(*real, false); break; } rest = wbasename(next) + L'/' + rest; From 23a5e53247291d071a4549a4e56f7c36422d2004 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Wed, 2 Feb 2022 20:30:43 +0100 Subject: [PATCH 34/63] tests: Print $PWD if resolving fails Seems to be a macOS issue --- tests/checks/path.fish | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/checks/path.fish b/tests/checks/path.fish index bd8564de3..a0f4d894f 100644 --- a/tests/checks/path.fish +++ b/tests/checks/path.fish @@ -115,6 +115,7 @@ path resolve bin//sh | string match -r -- 'bin/bash$' set -l path (path resolve foo/bar) string match -rq "^"(string escape --style=regex -- $PWD)'/' -- $path and echo It matches pwd! +or echo pwd is \'$PWD\' resolved path is \'$path\' # CHECK: It matches pwd! string replace -r "^"(string escape --style=regex -- $PWD)'/' "" -- $path # CHECK: foo/bar From 55c34cbb7c38e93aa413a166148009fc89e8773e Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Wed, 2 Feb 2022 20:46:32 +0100 Subject: [PATCH 35/63] Use physical $PWD Yeah, the macOS tests fail because it's started in /private/var... with a $PWD of /var.... So resolve canonicalizes the path, which makes it no longer match $PWD. Simply use pwd -P --- tests/checks/path.fish | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/checks/path.fish b/tests/checks/path.fish index a0f4d894f..b68b9f87e 100644 --- a/tests/checks/path.fish +++ b/tests/checks/path.fish @@ -113,11 +113,11 @@ path resolve bin//sh | string match -r -- 'bin/bash$' # `path resolve` with nonexistent paths set -l path (path resolve foo/bar) -string match -rq "^"(string escape --style=regex -- $PWD)'/' -- $path +string match -rq "^"(pwd -P | string escape --style=regex)'/' -- $path and echo It matches pwd! or echo pwd is \'$PWD\' resolved path is \'$path\' # CHECK: It matches pwd! -string replace -r "^"(string escape --style=regex -- $PWD)'/' "" -- $path +string replace -r "^"(pwd -P | string escape --style=regex)'/' "" -- $path # CHECK: foo/bar path resolve /banana//terracota/terracota/booooo/../pie From 83a993a28e37545e00984c46231778f6ceaf0225 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Sun, 27 Feb 2022 11:36:40 +0100 Subject: [PATCH 36/63] Remove references to match/expand in the docs --- doc_src/cmds/path.rst | 4 +--- doc_src/fish_for_bash_users.rst | 2 +- doc_src/language.rst | 14 -------------- 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index 458656b0b..a3ac92e1a 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -154,8 +154,6 @@ Examples ``path filter`` returns all of the given paths that match the given checks. In all cases, the paths need to exist, nonexistent paths are always filtered. -This is useful when you have a list of paths that you need to check. To match a list of paths against a glob pattern, see :ref:`path match `. To run a glob pattern to generate paths, see :ref:`path expand `. - The available filters are: - ``-t`` or ``--type`` with the options: "dir", "file", "link", "block", "char", "fifo" and "socket", in which case the path needs to be a directory, file, link, block device, character device, named pipe or socket, respectively. @@ -340,7 +338,7 @@ This is why Some examples of combining ``path``:: # Expand all paths in the current directory, leave only executable files, and print their resolved path - path expand '*' -Z | path filter -zZ --perm=exec --type=file | path resolve -z + path filter -zZ -xf -- * | path resolve -z # The same thing, but using find (note -maxdepth needs to come first or find will scream) # (this also depends on your particular version of find) diff --git a/doc_src/fish_for_bash_users.rst b/doc_src/fish_for_bash_users.rst index 68f743c37..66bacac73 100644 --- a/doc_src/fish_for_bash_users.rst +++ b/doc_src/fish_for_bash_users.rst @@ -94,7 +94,7 @@ will not match any files. There are no options to control globbing so it always behaves like that. -See :ref:`Wildcards ` for more. For more involved globbing, the :ref:`path ` builtin has the ``path expand`` and ``path match`` subcommands that feature the familiar globs from bash, plus ``**``. +See :ref:`Wildcards ` for more. Quoting ------- diff --git a/doc_src/language.rst b/doc_src/language.rst index 97ad213f3..060e51cf2 100644 --- a/doc_src/language.rst +++ b/doc_src/language.rst @@ -492,20 +492,6 @@ Unlike bash (by default), fish will not pass on the literal glob character if no apt install "ncurses-*" -For more capable wildcards, see the :ref:`path ` builtin, that features the ``path expand`` and ``path match`` subcommands that have full-featured globs, including ``[a-z]`` character ranges (and sets), ``[[:alnum:]]`` character classes and ``?`` for single-character matches. An example:: - - # I want all photos I took in October to December 2019, but not the ".raw" versions - > path expand 'IMG_20191[012]*' | path match -v '*.raw' - IMG_20191002_154337675_HDR.jpg - IMG_20191002_193313306.png - IMG_20191102_195530400_HDR.gif - IMG_20191104_122747460_HDR.jpg - IMG_20191105_195601152 (1).jpg - IMG_20191201_195601152.jpg - - # Okay, now delete them - > rm (path expand 'IMG_20191[012]*' | path match -v '*.raw') - .. _expand-variable: Variable expansion From e429f76e9fffac82e11ea1b544e4c3835bf95824 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Sun, 13 Mar 2022 18:44:25 +0100 Subject: [PATCH 37/63] append_with_separation: Default to wanting a newline The recent change to skip the newline for `string` changed this, and it also hit builtin path (which is in development separately, so it's not like it broke master). Let's pick a good default here. --- src/io.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/io.h b/src/io.h index b1aa81558..9ed9e9ba6 100644 --- a/src/io.h +++ b/src/io.h @@ -368,10 +368,10 @@ class output_stream_t : noncopyable_t, nonmovable_t { /// \param want_newline this is true if the output item should be ended with a newline. This /// is only relevant if we are printing the output to a stream, virtual void append_with_separation(const wchar_t *s, size_t len, separation_type_t type, - bool want_newline); + bool want_newline = true); /// The following are all convenience overrides. - void append_with_separation(const wcstring &s, separation_type_t type, bool want_newline) { + void append_with_separation(const wcstring &s, separation_type_t type, bool want_newline = true) { append_with_separation(s.data(), s.size(), type, want_newline); } From 9fdfad1d45448b170b819828ef983c72633eeda3 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Tue, 22 Mar 2022 18:18:29 +0100 Subject: [PATCH 38/63] WIP Add path sort This sorts paths by basename, dirname or full path - in future possibly size or age. It takes --invert to invert the sort and "--what=basename|dirname|..." to specify what to sort This can be used to implement better conf.d sorting, with something like ```fish set -l sourcelist for file in (path sort --what=basename $__fish_config_dir/conf.d/*.fish $__fish_sysconf_dir/conf.d/*.fish $vendor_confdirs/*.fish) ``` which will iterate over the files by their basename. Then we keep a list of their basenames to skip over anything that was already sourced, like before. --- src/builtins/path.cpp | 77 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index de6a28c33..a019d3e23 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -14,6 +14,7 @@ #include "../common.h" #include "../fallback.h" // IWYU pragma: keep #include "../io.h" +#include "../util.h" #include "../wcstringutil.h" #include "../wgetopt.h" #include "../wutil.h" // IWYU pragma: keep @@ -155,6 +156,9 @@ struct options_t { //!OCLINT(too many fields) bool perm_valid = false; bool type_valid = false; bool invert_valid = false; + bool what_valid = false; + bool have_what = false; + const wchar_t *what = nullptr; bool null_in = false; bool null_out = false; @@ -344,6 +348,16 @@ static int handle_flag_v(const wchar_t **argv, parser_t &parser, io_streams_t &s return STATUS_INVALID_ARGS; } +static int handle_flag_what(const wchar_t **argv, parser_t &parser, io_streams_t &streams, + const wgetopter_t &w, options_t *opts) { + UNUSED(argv); + UNUSED(parser); + UNUSED(streams); + opts->have_what = true; + opts->what = w.woptarg; + return STATUS_CMD_OK; +} + /// This constructs the wgetopt() short options string based on which arguments are valid for the /// subcommand. We have to do this because many short flags have multiple meanings and may or may /// not require an argument depending on the meaning. @@ -372,6 +386,7 @@ static const struct woption long_options[] = { {L"perm", required_argument, nullptr, 'p'}, {L"type", required_argument, nullptr, 't'}, {L"invert", required_argument, nullptr, 't'}, + {L"what", required_argument, nullptr, 1}, {nullptr, 0, nullptr, 0}}; static const std::unordered_map flag_to_function = { @@ -381,6 +396,7 @@ static const std::unordered_map flag_to_function {'r', handle_flag_r}, {'w', handle_flag_w}, {'x', handle_flag_x}, {'f', handle_flag_f}, {'l', handle_flag_l}, {'d', handle_flag_d}, + {1, handle_flag_what}, }; /// Parse the arguments for flags recognized by a specific string subcommand. @@ -666,6 +682,66 @@ static int path_resolve(parser_t &parser, io_streams_t &streams, int argc, const return n_transformed > 0 ? STATUS_CMD_OK : STATUS_CMD_ERROR; } +static int path_sort(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { + options_t opts; + opts.invert_valid = true; + opts.what_valid = true; + int optind; + int retval = parse_opts(&opts, &optind, 0, argc, argv, parser, streams); + if (retval != STATUS_CMD_OK) return retval; + + auto func = +[] (const wcstring &x) { + return wbasename(x.c_str()); + }; + if (opts.have_what) { + if (std::wcscmp(opts.what, L"basename") == 0) { + // Do nothing, this is the default + } else if (std::wcscmp(opts.what, L"dirname") == 0) { + func = +[] (const wcstring &x) { + return wdirname(x.c_str()); + }; + } else if (std::wcscmp(opts.what, L"path") == 0) { + // Act as if --what hadn't been given. + opts.have_what = false; + } else { + path_error(streams, _(L"%ls: Invalid sort key '%ls'\n"), argv[0], opts.what); + return STATUS_INVALID_ARGS; + } + } + + wcstring_list_t list; + arg_iterator_t aiter(argv, optind, streams, opts.null_in); + while (const wcstring *arg = aiter.nextstr()) { + list.push_back(*arg); + } + + if (opts.have_what) { + // Keep a map to avoid repeated func calls and to keep things alive. + std::map funced; + for (const auto &arg : list) { + funced[arg] = func(arg); + } + + std::sort(list.begin(), list.end(), + [&](const wcstring &a, const wcstring &b) { + return (wcsfilecmp_glob(funced[a].c_str(), funced[b].c_str()) < 0) != opts.invert; + }); + } else { + // Without --what, we just sort by the entire path, + // so we have no need to transform and such. + std::sort(list.begin(), list.end(), + [&](const wcstring &a, const wcstring &b) { + return (wcsfilecmp_glob(a.c_str(), b.c_str()) < 0) != opts.invert; + }); + } + + for (const auto &entry : list) { + path_out(streams, opts, entry); + } + + /* TODO: Return true only if already sorted? */ + return STATUS_CMD_OK; +} // All strings are taken to be filenames, and if they match the type/perms/etc (and exist!) // they are passed along. @@ -723,6 +799,7 @@ static constexpr const struct path_subcommand { {L"is", &path_is}, {L"normalize", &path_normalize}, {L"resolve", &path_resolve}, + {L"sort", &path_sort}, }; ASSERT_SORTED_BY_NAME(path_subcommands); From bb3700997ce31ea6eaaf0c84295536c4934eabc2 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Sat, 26 Mar 2022 10:41:20 +0100 Subject: [PATCH 39/63] Correct docs for normalize/resolve Resolve absolutizes, normalize doesn't --- doc_src/cmds/path.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index a3ac92e1a..d6d2a0003 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -237,7 +237,7 @@ Examples ``path normalize`` returns the normalized versions of all paths. That means it squashes duplicate "/" (except for two leading "//"), collapses "../" with earlier components and removes "." components. -It is the same as ``realpath --no-symlinks``, as it creates the "real", canonical version of the path but doesn't resolve any symlinks. As such it can operate on nonexistent paths. +Unlike ``realpath`` or ``path resolve``, it does not make the paths absolute. It also does not resolve any symlinks. As such it can operate on non-existent paths. It returns 0 if any normalization was done, i.e. any given path wasn't in canonical form. @@ -254,6 +254,9 @@ Examples # The "//" is squashed, but /bin isn't resolved even if your system links it to /usr/bin. /bin/bash + >_ path normalize ./my/subdirs/../sub2 + my/sub2 + "resolve" subcommand -------------------- @@ -261,9 +264,9 @@ Examples path resolve [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] -``path resolve`` returns the normalized, physical versions of all paths. That means it resolves symlinks and does what ``path normalize`` does: it squashes duplicate "/" (except for two leading "//"), collapses "../" with earlier components and removes "." components. +``path resolve`` returns the normalized, physical and absolute versions of all paths. That means it resolves symlinks and does what ``path normalize`` does: it squashes duplicate "/" (except for two leading "//"), collapses "../" with earlier components and removes "." components. Then it turns that path into the absolute path starting from the filesystem root "/". -It is the same as ``realpath``, as it creates the "real", canonical version of the path. However, for nonexistent paths it will resolve as far as it can and normalize the nonexistent part. +It is similar to ``realpath``, as it creates the "real", canonical version of the path. However, for nonexistent paths it will resolve as far as it can and normalize the nonexistent part. It returns 0 if any normalization or resolution was done, i.e. any given path wasn't in canonical form. From b961afed49050276c11c95e494700a6dedc228d3 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Sat, 26 Mar 2022 10:45:22 +0100 Subject: [PATCH 40/63] normalize: Add "./" if a path starts with a "-" --- doc_src/cmds/path.rst | 5 +++++ src/builtins/path.cpp | 6 +++++- tests/checks/path.fish | 7 +++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index d6d2a0003..1bcc95349 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -239,6 +239,8 @@ Examples Unlike ``realpath`` or ``path resolve``, it does not make the paths absolute. It also does not resolve any symlinks. As such it can operate on non-existent paths. +Leading "./" components are usually removed. But when a path starts with ``-``, ``path normalize`` will add it instead to avoid confusion with options. + It returns 0 if any normalization was done, i.e. any given path wasn't in canonical form. Examples @@ -257,6 +259,9 @@ Examples >_ path normalize ./my/subdirs/../sub2 my/sub2 + >_ path normalize -- -/foo + ./-/foo + "resolve" subcommand -------------------- diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index a019d3e23..4ae2c498c 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -482,7 +482,11 @@ static int path_dirname(parser_t &parser, io_streams_t &streams, int argc, const // Not a constref because this must have the same type as wdirname. // cppcheck-suppress passedByValue static wcstring normalize_helper(wcstring path) { - return normalize_path(path, false); + wcstring np = normalize_path(path, false); + if (!np.empty() && np[0] == L'-') { + np = L"./" + np; + } + return np; } static bool filter_path(options_t opts, const wcstring &path) { diff --git a/tests/checks/path.fish b/tests/checks/path.fish index b68b9f87e..ae14e5022 100644 --- a/tests/checks/path.fish +++ b/tests/checks/path.fish @@ -105,6 +105,13 @@ path normalize /bin//bash # The "//" is squashed, but /bin isn't resolved even if your system links it to /usr/bin. # CHECK: /bin/bash +# Paths with "-" get a "./": +path normalize -- -/foo -foo/foo +# CHECK: ./-/foo +# CHECK: ./-foo/foo +path normalize -- ../-foo +# CHECK: ../-foo + # We need to remove the rest of the path because we have no idea what its value looks like. path resolve bin//sh | string match -r -- 'bin/bash$' # The "//" is squashed, and the symlink is resolved. From dfded633c6cb734147908dec9ecd9c31db67266a Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Sun, 3 Apr 2022 20:45:27 +0200 Subject: [PATCH 41/63] Fix woption --- src/builtins/path.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index 4ae2c498c..b1840d9fd 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -387,7 +387,7 @@ static const struct woption long_options[] = { {L"type", required_argument, nullptr, 't'}, {L"invert", required_argument, nullptr, 't'}, {L"what", required_argument, nullptr, 1}, - {nullptr, 0, nullptr, 0}}; + {}}; static const std::unordered_map flag_to_function = { {'q', handle_flag_q}, {'v', handle_flag_v}, From 5cce6d01add5fbe2c3293f6d1da44d0348a46e47 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Thu, 7 Apr 2022 15:13:10 +0200 Subject: [PATCH 42/63] resolve: Normalize This means "../" components are cancelled out even after non-existent paths or files. (the alternative is to error out, but being able to say `path resolve /path/to/file/../../` over `path resolve (path dirname /path/to/file)/../../` seems worth it?) --- src/builtins/path.cpp | 5 +++++ tests/checks/path.fish | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index b1840d9fd..fca03bd82 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -675,6 +675,11 @@ static int path_resolve(parser_t &parser, io_streams_t &streams, int argc, const } } + // Normalize the path so "../" components are eliminated even after + // nonexistent or non-directory components. + // Otherwise `path resolve foo/../` will be `$PWD/foo/../` if foo is a file. + real = normalize_path(*real, false); + // Return 0 if we found a realpath. if (opts.quiet) { return STATUS_CMD_OK; diff --git a/tests/checks/path.fish b/tests/checks/path.fish index ae14e5022..7334f0e7d 100644 --- a/tests/checks/path.fish +++ b/tests/checks/path.fish @@ -118,6 +118,10 @@ path resolve bin//sh | string match -r -- 'bin/bash$' # sh here is bash # CHECK: bin/bash +# "../" cancels out even files. +path resolve bin//sh/../ | string match -r -- 'bin$' +# CHECK: bin + # `path resolve` with nonexistent paths set -l path (path resolve foo/bar) string match -rq "^"(pwd -P | string escape --style=regex)'/' -- $path From 640bd7b18397f65ad8438773da53c2d0024436e5 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Thu, 7 Apr 2022 15:16:05 +0200 Subject: [PATCH 43/63] extension: Print empty entry if there is no extension Because we now count the extension including the ".", we print an empty entry. This makes e.g. ```fish set -l base (path change-extension '' $somefile) set -l ext (path extension $somefile) echo $base$ext ``` reconstruct the filename, and makes it easier to deal with files with no extension. --- src/builtins/path.cpp | 7 ++++++- tests/checks/path.fish | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index fca03bd82..af734a4f6 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -593,7 +593,12 @@ static int path_extension(parser_t &parser, io_streams_t &streams, int argc, con while (const wcstring *arg = aiter.nextstr()) { auto pos = find_extension(*arg); - if (!pos) continue; + if (!pos) { + // If there is no extension the extension is empty. + // This is unambiguous because we include the ".". + path_out(streams, opts, L""); + continue; + } wcstring ext = arg->substr(*pos); if (opts.quiet && !ext.empty()) { diff --git a/tests/checks/path.fish b/tests/checks/path.fish index 7334f0e7d..426625fcc 100644 --- a/tests/checks/path.fish +++ b/tests/checks/path.fish @@ -4,20 +4,24 @@ # Extension - for figuring out the file extension of a given path. path extension / or echo None +# CHECK: # CHECK: None # No extension path extension /. or echo Filename is just a dot, no extension +# CHECK: # CHECK: Filename is just a dot, no extension # No extension - ".foo" is the filename path extension /.foo or echo None again +# CHECK: # CHECK: None again path extension /foo or echo None once more +# CHECK: # CHECK: None once more path extension /foo.txt and echo Success @@ -25,17 +29,21 @@ and echo Success # CHECK: Success path extension /foo.txt/bar or echo Not even here +# CHECK: # CHECK: Not even here path extension . .. or echo No extension +# CHECK: # CHECK: No extension path extension ./foo.mp4 # CHECK: .mp4 path extension ../banana +# CHECK: # nothing, status 1 echo $status # CHECK: 1 path extension ~/.config +# CHECK: # nothing, status 1 echo $status # CHECK: 1 From 4fec045073a9964996f41e13454334290e1b6ab0 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Thu, 7 Apr 2022 16:09:28 +0200 Subject: [PATCH 44/63] sort: Use a stable sort This allows e.g. sorting first by dirname and then by basename. --- src/builtins/path.cpp | 16 ++++++++++++---- tests/checks/path.fish | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index af734a4f6..59b91fcfd 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -736,16 +736,24 @@ static int path_sort(parser_t &parser, io_streams_t &streams, int argc, const wc funced[arg] = func(arg); } - std::sort(list.begin(), list.end(), + // We use a stable sort here, and also explicit < and >, + // to avoid changing the order so you can chain calls. + std::stable_sort(list.begin(), list.end(), [&](const wcstring &a, const wcstring &b) { - return (wcsfilecmp_glob(funced[a].c_str(), funced[b].c_str()) < 0) != opts.invert; + if (!opts.invert) + return (wcsfilecmp_glob(funced[a].c_str(), funced[b].c_str()) < 0); + else + return (wcsfilecmp_glob(funced[a].c_str(), funced[b].c_str()) > 0); }); } else { // Without --what, we just sort by the entire path, // so we have no need to transform and such. - std::sort(list.begin(), list.end(), + std::stable_sort(list.begin(), list.end(), [&](const wcstring &a, const wcstring &b) { - return (wcsfilecmp_glob(a.c_str(), b.c_str()) < 0) != opts.invert; + if (!opts.invert) + return (wcsfilecmp_glob(a.c_str(), b.c_str()) < 0); + else + return (wcsfilecmp_glob(a.c_str(), b.c_str()) > 0); }); } diff --git a/tests/checks/path.fish b/tests/checks/path.fish index 426625fcc..7ba7f126e 100644 --- a/tests/checks/path.fish +++ b/tests/checks/path.fish @@ -141,3 +141,17 @@ string replace -r "^"(pwd -P | string escape --style=regex)'/' "" -- $path path resolve /banana//terracota/terracota/booooo/../pie # CHECK: /banana/terracota/terracota/pie + +path sort --what=basename {def,abc}/{456,123,789,abc,def,0} | path sort --what=dirname -v +# CHECK: def/0 +# CHECK: def/123 +# CHECK: def/456 +# CHECK: def/789 +# CHECK: def/abc +# CHECK: def/def +# CHECK: abc/0 +# CHECK: abc/123 +# CHECK: abc/456 +# CHECK: abc/789 +# CHECK: abc/abc +# CHECK: abc/def From 54778f65f86c44268cfb5c40ef607c8fe14a14c1 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Thu, 7 Apr 2022 17:08:50 +0200 Subject: [PATCH 45/63] Some sort docs --- doc_src/cmds/path.rst | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index 1bcc95349..da99da089 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -20,6 +20,8 @@ Synopsis path normalize GENERAL_OPTIONS [PATH...] path resolve GENERAL_OPTIONS [PATH...] path change-extension GENERAL_OPTIONS EXTENSION [PATH...] + path sort GENERAL_OPTIONS [(-v | --invert)] \ + [--what=basename|dirname|path] [([PATH...] GENERAL_OPTIONS := [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] @@ -333,6 +335,39 @@ Examples /home/alfa/.config # status 0 +"sort" subcommand +----------------------------- + +:: + + path sort [(-z | --null-in)] [(-Z | --null-out)] \ + [(-q | --quiet)] [(-v | --invert)] \ + [--what=basename|dirname|path] [([PATH...] + + +``path sort`` returns the given paths in sorted order. They are sorted in the same order as globs - alphabetically, but with runs of numerical digits compared numerically. + +With ``--invert`` or ``-v`` the sort is reversed. + +With ``--what=`` only the given path of the path is compared, e.g. ``--what=dirname`` causes only the dirname to be compared, ``--what=basename`` only the basename and ``--what=path`` causes the entire path to be compared (this is the default). + +The sort used is stable, so sorting first by basename and then by dirname works and causes the files to be grouped according to directory. + +It currently returns 0 if it was given any paths. + +Examples +^^^^^^^^ + +:: + + >_ path sort 10-foo 2-bar + 2-bar + 10-foo + + >_ path sort --invert 10-foo 2-bar + 10-foo + 2-bar + Combining ``path`` ------------------- From c88f648cdff3551682e46f00b358d344f3c16253 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Tue, 26 Apr 2022 21:10:54 +0200 Subject: [PATCH 46/63] Add sort --unique --- doc_src/cmds/path.rst | 8 +++++++- src/builtins/path.cpp | 28 +++++++++++++++++++++++++++- tests/checks/path.fish | 13 +++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index da99da089..ccb90504c 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -21,7 +21,7 @@ Synopsis path resolve GENERAL_OPTIONS [PATH...] path change-extension GENERAL_OPTIONS EXTENSION [PATH...] path sort GENERAL_OPTIONS [(-v | --invert)] \ - [--what=basename|dirname|path] [([PATH...] + [-u | --unique] [--what=basename|dirname|path] [([PATH...] GENERAL_OPTIONS := [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] @@ -351,6 +351,8 @@ With ``--invert`` or ``-v`` the sort is reversed. With ``--what=`` only the given path of the path is compared, e.g. ``--what=dirname`` causes only the dirname to be compared, ``--what=basename`` only the basename and ``--what=path`` causes the entire path to be compared (this is the default). +With ``--unique`` or ``-u`` the sort is deduplicated, meaning only the first of a run that have the same key is kept. So if you are sorting by basename, then only the first of each basename is used. + The sort used is stable, so sorting first by basename and then by dirname works and causes the files to be grouped according to directory. It currently returns 0 if it was given any paths. @@ -368,6 +370,10 @@ Examples 10-foo 2-bar + >_ path sort --unique --what=basename $fish_function_path/*.fish + # prints a list of all function files fish would use, sorted by name. + + Combining ``path`` ------------------- diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index 59b91fcfd..b674523f3 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -157,6 +157,8 @@ struct options_t { //!OCLINT(too many fields) bool type_valid = false; bool invert_valid = false; bool what_valid = false; + bool unique_valid = false; + bool unique = false; bool have_what = false; const wchar_t *what = nullptr; @@ -348,6 +350,16 @@ static int handle_flag_v(const wchar_t **argv, parser_t &parser, io_streams_t &s return STATUS_INVALID_ARGS; } +static int handle_flag_u(const wchar_t **argv, parser_t &parser, io_streams_t &streams, + const wgetopter_t &w, options_t *opts) { + if (opts->unique_valid) { + opts->unique = true; + return STATUS_CMD_OK; + } + path_unknown_option(parser, streams, argv[0], argv[w.woptind - 1]); + return STATUS_INVALID_ARGS; +} + static int handle_flag_what(const wchar_t **argv, parser_t &parser, io_streams_t &streams, const wgetopter_t &w, options_t *opts) { UNUSED(argv); @@ -373,6 +385,7 @@ static wcstring construct_short_opts(options_t *opts) { //!OCLINT(high npath co short_opts.append(L"fld"); } if (opts->invert_valid) short_opts.append(L"v"); + if (opts->unique_valid) short_opts.append(L"u"); return short_opts; } @@ -386,6 +399,7 @@ static const struct woption long_options[] = { {L"perm", required_argument, nullptr, 'p'}, {L"type", required_argument, nullptr, 't'}, {L"invert", required_argument, nullptr, 't'}, + {L"unique", no_argument, nullptr, 'u'}, {L"what", required_argument, nullptr, 1}, {}}; @@ -396,7 +410,8 @@ static const std::unordered_map flag_to_function {'r', handle_flag_r}, {'w', handle_flag_w}, {'x', handle_flag_x}, {'f', handle_flag_f}, {'l', handle_flag_l}, {'d', handle_flag_d}, - {1, handle_flag_what}, + {'l', handle_flag_l}, {'d', handle_flag_d}, + {'u', handle_flag_u}, {1, handle_flag_what}, }; /// Parse the arguments for flags recognized by a specific string subcommand. @@ -700,6 +715,7 @@ static int path_sort(parser_t &parser, io_streams_t &streams, int argc, const wc options_t opts; opts.invert_valid = true; opts.what_valid = true; + opts.unique_valid = true; int optind; int retval = parse_opts(&opts, &optind, 0, argc, argv, parser, streams); if (retval != STATUS_CMD_OK) return retval; @@ -745,6 +761,13 @@ static int path_sort(parser_t &parser, io_streams_t &streams, int argc, const wc else return (wcsfilecmp_glob(funced[a].c_str(), funced[b].c_str()) > 0); }); + if (opts.unique) { + list.erase(std::unique(list.begin(), list.end(), + [&](const wcstring &a, const wcstring &b) { + return funced[a] == funced[b]; + }), + list.end()); + } } else { // Without --what, we just sort by the entire path, // so we have no need to transform and such. @@ -755,6 +778,9 @@ static int path_sort(parser_t &parser, io_streams_t &streams, int argc, const wc else return (wcsfilecmp_glob(a.c_str(), b.c_str()) > 0); }); + if (opts.unique) { + list.erase(std::unique(list.begin(), list.end()), list.end()); + } } for (const auto &entry : list) { diff --git a/tests/checks/path.fish b/tests/checks/path.fish index 7ba7f126e..6f1ce78c2 100644 --- a/tests/checks/path.fish +++ b/tests/checks/path.fish @@ -155,3 +155,16 @@ path sort --what=basename {def,abc}/{456,123,789,abc,def,0} | path sort --what=d # CHECK: abc/789 # CHECK: abc/abc # CHECK: abc/def + +path sort --unique --what=basename {def,abc}/{456,123,789} def/{abc,def,0} abc/{foo,bar,baz} +# CHECK: def/0 +# CHECK: def/123 +# CHECK: def/456 +# CHECK: def/789 +# CHECK: def/abc +# CHECK: abc/bar +# CHECK: abc/baz +# CHECK: def/def +# CHECK: abc/foo + + From bc3d3de30a0f1c73381a7864bb1061216b95454b Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Wed, 11 May 2022 17:00:59 +0200 Subject: [PATCH 47/63] Also prepend "./" for filter if a filename starts with "-" This is now added to the two commands that definitely deal with relative paths. It doesn't work for e.g. `path basename`, because after removing the dirname prepending a "./" doesn't refer to the same file, and the basename is also expected to not contain any slashes. --- doc_src/cmds/path.rst | 2 ++ src/builtins/path.cpp | 10 +++++++++- tests/checks/path.fish | 5 +++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index ccb90504c..c02c45edd 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -170,6 +170,8 @@ The filter options can either be given as multiple options, or comma-separated - With ``--invert``, the meaning of the filtering is inverted - any path that wouldn't pass (including by not existing) passes, and any path that would pass fails. +When a path starts with ``-``, ``path filter`` will prepend ``./`` to avoid it being interpreted as an option otherwise. + It returns 0 if at least one path passed the filter. ``path is`` is shorthand for ``path filter -q``, i.e. just checking without producing output, see :ref:`The is subcommand `. diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index b674523f3..a7a912ac2 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -815,7 +815,15 @@ static int path_filter(parser_t &parser, io_streams_t &streams, int argc, const if (ok == opts.invert) continue; } - path_out(streams, opts, *arg); + // We *know* this is a filename, + // and so if it starts with a `-` we *know* it is relative + // to $PWD. So we can add `./`. + if (!arg->empty() && arg->front() == L'-') { + wcstring out = L"./" + *arg; + path_out(streams, opts, out); + } else { + path_out(streams, opts, *arg); + } n_transformed++; if (opts.quiet) return STATUS_CMD_OK; } diff --git a/tests/checks/path.fish b/tests/checks/path.fish index 6f1ce78c2..d9ef9897a 100644 --- a/tests/checks/path.fish +++ b/tests/checks/path.fish @@ -120,6 +120,11 @@ path normalize -- -/foo -foo/foo path normalize -- ../-foo # CHECK: ../-foo +# This goes for filter as well +touch -- -foo +path filter -f -- -foo +# CHECK: ./-foo + # We need to remove the rest of the path because we have no idea what its value looks like. path resolve bin//sh | string match -r -- 'bin/bash$' # The "//" is squashed, and the symlink is resolved. From a9034610e19305b849a95a9b0047c9503a489d37 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Wed, 11 May 2022 17:09:24 +0200 Subject: [PATCH 48/63] Fix --invert long form --- src/builtins/path.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index a7a912ac2..be062e23f 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -398,7 +398,7 @@ static const struct woption long_options[] = { {L"null-out", no_argument, nullptr, 'Z'}, {L"perm", required_argument, nullptr, 'p'}, {L"type", required_argument, nullptr, 't'}, - {L"invert", required_argument, nullptr, 't'}, + {L"invert", required_argument, nullptr, 'v'}, {L"unique", no_argument, nullptr, 'u'}, {L"what", required_argument, nullptr, 1}, {}}; From e088c974dd1c53ac4f8522eea4370b299a3000eb Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Wed, 11 May 2022 17:10:25 +0200 Subject: [PATCH 49/63] Fix path filter --invert This would still remove non-existent paths, which isn't a strict inversion and contradicts the docs. Currently, to only allow paths that exist but don't pass a type check, you'd have to filter twice: path filter -Z foo bar | path filter -vfz If a shortcut for this becomes necessary we can add it later. --- src/builtins/path.cpp | 3 +-- tests/checks/path.fish | 11 +++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index be062e23f..5cf368652 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -807,9 +807,8 @@ static int path_filter(parser_t &parser, io_streams_t &streams, int argc, const int n_transformed = 0; arg_iterator_t aiter(argv, optind, streams, opts.null_in); while (const wcstring *arg = aiter.nextstr()) { - if ((!opts.invert || (!opts.have_perm && !opts.have_type)) && filter_path(opts, *arg)) { + if ((!opts.have_perm && !opts.have_type) || (filter_path(opts, *arg) != opts.invert)) { // If we don't have filters, check if it exists. - // (for match this is done by the glob already) if (!opts.have_type && !opts.have_perm) { bool ok = !waccess(*arg, F_OK); if (ok == opts.invert) continue; diff --git a/tests/checks/path.fish b/tests/checks/path.fish index d9ef9897a..3f2aa2f48 100644 --- a/tests/checks/path.fish +++ b/tests/checks/path.fish @@ -94,6 +94,17 @@ chmod +x bin/* path filter bin argagagji # The (hopefully) nonexistent argagagji is filtered implicitly: # CHECK: bin + +# With --invert, the existing bin is filtered +path filter --invert bin argagagji +# CHECK: argagagji + +# With --invert and a type, bin fails the type, +# and argagagji doesn't exist, so both are printed. +path filter -vf bin argagagji +# CHECK: bin +# CHECK: argagagji + path filter --type file bin bin/fish # Only fish is a file # CHECK: bin/fish From e5858522e35f7bbbe57ba642d6e343f9fdaa15c3 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Wed, 11 May 2022 17:13:15 +0200 Subject: [PATCH 50/63] Document ./- more. --- doc_src/cmds/path.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index c02c45edd..0bbdba901 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -34,6 +34,8 @@ PATH arguments are taken from the command line unless standard input is connecte Arguments starting with ``-`` are normally interpreted as switches; ``--`` causes the following arguments not to be treated as switches even if they begin with ``-``. Switches and required arguments are recognized only on the command line. +When a path starts with ``-``, ``path filter`` and ``path normalize`` will prepend ``./`` on output to avoid it being interpreted as an option otherwise, so it's safe to pass path's output to other commands that can handle relative paths. + All subcommands accept a ``-q`` or ``--quiet`` switch, which suppresses the usual output but exits with the documented status. In this case these commands will quit early, without reading all of the available input. All subcommands also accept a ``-Z`` or ``--null-out`` switch, which makes them print output separated with NULL instead of newlines. This is for further processing, e.g. passing to another ``path``, or ``xargs -0``. This is not recommended when the output goes to the terminal or a command substitution. From b6ebf15c75bf1f09095de6b122db428433b45a15 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Thu, 19 May 2022 21:01:55 +0200 Subject: [PATCH 51/63] Refer to asci 0x00 as "NUL" it is the american standard code for information, after all --- doc_src/cmds/path.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index 0bbdba901..b6a15bd82 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -38,7 +38,7 @@ When a path starts with ``-``, ``path filter`` and ``path normalize`` will prepe All subcommands accept a ``-q`` or ``--quiet`` switch, which suppresses the usual output but exits with the documented status. In this case these commands will quit early, without reading all of the available input. -All subcommands also accept a ``-Z`` or ``--null-out`` switch, which makes them print output separated with NULL instead of newlines. This is for further processing, e.g. passing to another ``path``, or ``xargs -0``. This is not recommended when the output goes to the terminal or a command substitution. +All subcommands also accept a ``-Z`` or ``--null-out`` switch, which makes them print output separated with NUL instead of newlines. This is for further processing, e.g. passing to another ``path``, or ``xargs -0``. This is not recommended when the output goes to the terminal or a command substitution. All subcommands also accept a ``-z`` or ``--null-in`` switch, which makes them accept arguments from stdin separated with NULL-bytes. Since Unix paths can't contain NULL, that makes it possible to handle all possible paths and read input from e.g. ``find -print0``. If arguments are given on the commandline this has no effect. This should mostly be unnecessary since ``path`` automatically starts splitting on NULL if one appears in the first PATH_MAX bytes, PATH_MAX being the operating system's maximum length for a path plus a NULL byte. From e87ad48f9b87586f4927231bd0ef1b89db030ce1 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Thu, 19 May 2022 21:08:11 +0200 Subject: [PATCH 52/63] Test and document symlink loop --- doc_src/cmds/path.rst | 2 +- tests/checks/path.fish | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index b6a15bd82..e2a3d0324 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -277,7 +277,7 @@ Examples ``path resolve`` returns the normalized, physical and absolute versions of all paths. That means it resolves symlinks and does what ``path normalize`` does: it squashes duplicate "/" (except for two leading "//"), collapses "../" with earlier components and removes "." components. Then it turns that path into the absolute path starting from the filesystem root "/". -It is similar to ``realpath``, as it creates the "real", canonical version of the path. However, for nonexistent paths it will resolve as far as it can and normalize the nonexistent part. +It is similar to ``realpath``, as it creates the "real", canonical version of the path. However, for paths that can't be resolved, e.g. if they don't exist or form a symlink loop, it will resolve as far as it can and normalize the rest. It returns 0 if any normalization or resolution was done, i.e. any given path wasn't in canonical form. diff --git a/tests/checks/path.fish b/tests/checks/path.fish index 3f2aa2f48..4a5599d66 100644 --- a/tests/checks/path.fish +++ b/tests/checks/path.fish @@ -184,3 +184,17 @@ path sort --unique --what=basename {def,abc}/{456,123,789} def/{abc,def,0} abc/{ # CHECK: abc/foo + +# Symlink loop. +# It goes brrr. +ln -s target link +ln -s link target + +test (path resolve target) = (pwd -P)/target +and echo target resolves to target +# CHECK: target resolves to target + +test (path resolve link) = (pwd -P)/link +and echo link resolves to link +# CHECK: link resolves to link + From b9bd0ce3a3ba98967bbd173407be1dcba6e889ec Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Thu, 19 May 2022 21:08:33 +0200 Subject: [PATCH 53/63] Use path_apply_working_directory Using getcwd is naughty here because we want to separate these things in future. --- src/builtins/path.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index 5cf368652..117c1de02 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -14,6 +14,8 @@ #include "../common.h" #include "../fallback.h" // IWYU pragma: keep #include "../io.h" +#include "../parser.h" +#include "../path.h" #include "../util.h" #include "../wcstringutil.h" #include "../wgetopt.h" @@ -676,7 +678,8 @@ static int path_resolve(parser_t &parser, io_streams_t &streams, int argc, const wcstring next = *arg; // First add $PWD if we're relative if (!next.empty() && next[0] != L'/') { - next = wgetcwd() + L"/" + next; + // Note pwd can have symlinks, but we are about to resolve it anyway. + next = path_apply_working_directory(*arg, parser.vars().get_pwd_slash()); } auto rest = wbasename(next); while(!next.empty() && next != L"/") { From 633fd5000eb5faebdbdd4844f157a056bbd114a4 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Thu, 19 May 2022 21:10:12 +0200 Subject: [PATCH 54/63] Remove useless c_str --- src/builtins/path.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index 117c1de02..c0ffe8301 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -724,14 +724,14 @@ static int path_sort(parser_t &parser, io_streams_t &streams, int argc, const wc if (retval != STATUS_CMD_OK) return retval; auto func = +[] (const wcstring &x) { - return wbasename(x.c_str()); + return wbasename(x); }; if (opts.have_what) { if (std::wcscmp(opts.what, L"basename") == 0) { // Do nothing, this is the default } else if (std::wcscmp(opts.what, L"dirname") == 0) { func = +[] (const wcstring &x) { - return wdirname(x.c_str()); + return wdirname(x); }; } else if (std::wcscmp(opts.what, L"path") == 0) { // Act as if --what hadn't been given. From 3991af9ed6a06c62b935fe65fb889a5f1b2cfc8e Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Thu, 19 May 2022 21:10:20 +0200 Subject: [PATCH 55/63] Use += instead of temporaries clang-tidy explains this is better. I hate C++. --- src/builtins/path.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index c0ffe8301..41a2c53b4 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -691,7 +691,9 @@ static int path_resolve(parser_t &parser, io_streams_t &streams, int argc, const real = normalize_path(*real, false); break; } - rest = wbasename(next) + L'/' + rest; + rest = wbasename(next); + rest += L'/'; + rest += rest; } if (!real) { continue; From 00949fccdab8813ac6c798eb0d808fcacc0ae876 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Thu, 19 May 2022 21:11:52 +0200 Subject: [PATCH 56/63] Rename --what to --key More sorty, less generic. --- doc_src/cmds/path.rst | 8 ++++---- src/builtins/path.cpp | 36 ++++++++++++++++++------------------ tests/checks/path.fish | 4 ++-- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index e2a3d0324..799ea4db8 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -21,7 +21,7 @@ Synopsis path resolve GENERAL_OPTIONS [PATH...] path change-extension GENERAL_OPTIONS EXTENSION [PATH...] path sort GENERAL_OPTIONS [(-v | --invert)] \ - [-u | --unique] [--what=basename|dirname|path] [([PATH...] + [-u | --unique] [--key=basename|dirname|path] [([PATH...] GENERAL_OPTIONS := [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] @@ -346,14 +346,14 @@ Examples path sort [(-z | --null-in)] [(-Z | --null-out)] \ [(-q | --quiet)] [(-v | --invert)] \ - [--what=basename|dirname|path] [([PATH...] + [--key=basename|dirname|path] [([PATH...] ``path sort`` returns the given paths in sorted order. They are sorted in the same order as globs - alphabetically, but with runs of numerical digits compared numerically. With ``--invert`` or ``-v`` the sort is reversed. -With ``--what=`` only the given path of the path is compared, e.g. ``--what=dirname`` causes only the dirname to be compared, ``--what=basename`` only the basename and ``--what=path`` causes the entire path to be compared (this is the default). +With ``--key=`` only the given path of the path is compared, e.g. ``--key=dirname`` causes only the dirname to be compared, ``--key=basename`` only the basename and ``--key=path`` causes the entire path to be compared (this is the default). With ``--unique`` or ``-u`` the sort is deduplicated, meaning only the first of a run that have the same key is kept. So if you are sorting by basename, then only the first of each basename is used. @@ -374,7 +374,7 @@ Examples 10-foo 2-bar - >_ path sort --unique --what=basename $fish_function_path/*.fish + >_ path sort --unique --key=basename $fish_function_path/*.fish # prints a list of all function files fish would use, sorted by name. diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index 41a2c53b4..4ddfbba96 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -158,11 +158,11 @@ struct options_t { //!OCLINT(too many fields) bool perm_valid = false; bool type_valid = false; bool invert_valid = false; - bool what_valid = false; + bool key_valid = false; bool unique_valid = false; bool unique = false; - bool have_what = false; - const wchar_t *what = nullptr; + bool have_key = false; + const wchar_t *key = nullptr; bool null_in = false; bool null_out = false; @@ -362,13 +362,13 @@ static int handle_flag_u(const wchar_t **argv, parser_t &parser, io_streams_t &s return STATUS_INVALID_ARGS; } -static int handle_flag_what(const wchar_t **argv, parser_t &parser, io_streams_t &streams, +static int handle_flag_key(const wchar_t **argv, parser_t &parser, io_streams_t &streams, const wgetopter_t &w, options_t *opts) { UNUSED(argv); UNUSED(parser); UNUSED(streams); - opts->have_what = true; - opts->what = w.woptarg; + opts->have_key = true; + opts->key = w.woptarg; return STATUS_CMD_OK; } @@ -402,7 +402,7 @@ static const struct woption long_options[] = { {L"type", required_argument, nullptr, 't'}, {L"invert", required_argument, nullptr, 'v'}, {L"unique", no_argument, nullptr, 'u'}, - {L"what", required_argument, nullptr, 1}, + {L"key", required_argument, nullptr, 1}, {}}; static const std::unordered_map flag_to_function = { @@ -413,7 +413,7 @@ static const std::unordered_map flag_to_function {'x', handle_flag_x}, {'f', handle_flag_f}, {'l', handle_flag_l}, {'d', handle_flag_d}, {'l', handle_flag_l}, {'d', handle_flag_d}, - {'u', handle_flag_u}, {1, handle_flag_what}, + {'u', handle_flag_u}, {1, handle_flag_key}, }; /// Parse the arguments for flags recognized by a specific string subcommand. @@ -719,7 +719,7 @@ static int path_resolve(parser_t &parser, io_streams_t &streams, int argc, const static int path_sort(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { options_t opts; opts.invert_valid = true; - opts.what_valid = true; + opts.key_valid = true; opts.unique_valid = true; int optind; int retval = parse_opts(&opts, &optind, 0, argc, argv, parser, streams); @@ -728,18 +728,18 @@ static int path_sort(parser_t &parser, io_streams_t &streams, int argc, const wc auto func = +[] (const wcstring &x) { return wbasename(x); }; - if (opts.have_what) { - if (std::wcscmp(opts.what, L"basename") == 0) { + if (opts.have_key) { + if (std::wcscmp(opts.key, L"basename") == 0) { // Do nothing, this is the default - } else if (std::wcscmp(opts.what, L"dirname") == 0) { + } else if (std::wcscmp(opts.key, L"dirname") == 0) { func = +[] (const wcstring &x) { return wdirname(x); }; - } else if (std::wcscmp(opts.what, L"path") == 0) { - // Act as if --what hadn't been given. - opts.have_what = false; + } else if (std::wcscmp(opts.key, L"path") == 0) { + // Act as if --key hadn't been given. + opts.have_key = false; } else { - path_error(streams, _(L"%ls: Invalid sort key '%ls'\n"), argv[0], opts.what); + path_error(streams, _(L"%ls: Invalid sort key '%ls'\n"), argv[0], opts.key); return STATUS_INVALID_ARGS; } } @@ -750,7 +750,7 @@ static int path_sort(parser_t &parser, io_streams_t &streams, int argc, const wc list.push_back(*arg); } - if (opts.have_what) { + if (opts.have_key) { // Keep a map to avoid repeated func calls and to keep things alive. std::map funced; for (const auto &arg : list) { @@ -774,7 +774,7 @@ static int path_sort(parser_t &parser, io_streams_t &streams, int argc, const wc list.end()); } } else { - // Without --what, we just sort by the entire path, + // Without --key, we just sort by the entire path, // so we have no need to transform and such. std::stable_sort(list.begin(), list.end(), [&](const wcstring &a, const wcstring &b) { diff --git a/tests/checks/path.fish b/tests/checks/path.fish index 4a5599d66..41ebc6492 100644 --- a/tests/checks/path.fish +++ b/tests/checks/path.fish @@ -158,7 +158,7 @@ string replace -r "^"(pwd -P | string escape --style=regex)'/' "" -- $path path resolve /banana//terracota/terracota/booooo/../pie # CHECK: /banana/terracota/terracota/pie -path sort --what=basename {def,abc}/{456,123,789,abc,def,0} | path sort --what=dirname -v +path sort --key=basename {def,abc}/{456,123,789,abc,def,0} | path sort --key=dirname -v # CHECK: def/0 # CHECK: def/123 # CHECK: def/456 @@ -172,7 +172,7 @@ path sort --what=basename {def,abc}/{456,123,789,abc,def,0} | path sort --what=d # CHECK: abc/abc # CHECK: abc/def -path sort --unique --what=basename {def,abc}/{456,123,789} def/{abc,def,0} abc/{foo,bar,baz} +path sort --unique --key=basename {def,abc}/{456,123,789} def/{abc,def,0} abc/{foo,bar,baz} # CHECK: def/0 # CHECK: def/123 # CHECK: def/456 From 8e38ee884f3c11f5edf4884553757d4931116558 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Thu, 19 May 2022 22:55:42 +0200 Subject: [PATCH 57/63] Undo "+=" thing oh no this made no sense given that it was *prepending* to `rest`. --- src/builtins/path.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index 4ddfbba96..5e09cbcfa 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -691,9 +691,7 @@ static int path_resolve(parser_t &parser, io_streams_t &streams, int argc, const real = normalize_path(*real, false); break; } - rest = wbasename(next); - rest += L'/'; - rest += rest; + rest = wbasename(next) + L'/' + rest; } if (!real) { continue; From 5d96f5d00b42f29693d5973b230169a9f93c2107 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Thu, 26 May 2022 13:15:36 +0200 Subject: [PATCH 58/63] Update completions --- share/completions/path.fish | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/share/completions/path.fish b/share/completions/path.fish index c8dbfe92a..a3810208c 100644 --- a/share/completions/path.fish +++ b/share/completions/path.fish @@ -9,20 +9,23 @@ complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a normalize -d ' complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a resolve -d 'Normalize given paths and resolve symlinks' complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a filter -d 'Print paths that match a filter' complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a is -d 'Return true if any path matched a filter' -complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a match -d 'Match paths against a glob' -complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a expand -d 'Expand globs' +complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a sort -d 'Sort paths' complete -f -c path -n "test (count (commandline -opc)) -ge 2" -s q -l quiet -d "Only return status, no output" complete -f -c path -n "test (count (commandline -opc)) -ge 2" -s z -l null-in -d "Handle NULL-delimited input" complete -f -c path -n "test (count (commandline -opc)) -ge 2" -s Z -l null-out -d "Print NULL-delimited output" -complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is match" -s v -l invert -d "Invert meaning of filters" -complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is match expand" -s t -l type -d "Filter by type" -x -a '(__fish_append , file link dir block char fifo socket)' -complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is match expand" -s f -d "Filter files" -x -a '(__fish_append , read write exec suid sgid sticky user group)' -complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is match expand" -s d -d "Filter directories" -x -a '(__fish_append , read write exec suid sgid sticky user group)' -complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is match expand" -s l -d "Filter symlinks" -x -a '(__fish_append , read write exec suid sgid sticky user group)' -complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is match expand" -s p -l perm -d "Filter by permission" -x -a '(__fish_append , read write exec suid sgid sticky user group)' -complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is match expand" -s r -d "Filter readable paths" -x -a '(__fish_append , read write exec suid sgid sticky user group)' -complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is match expand" -s w -d "Filter writable paths" -x -a '(__fish_append , read write exec suid sgid sticky user group)' -complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is match expand" -s x -d "Filter executale paths" -x -a '(__fish_append , read write exec suid sgid sticky user group)' +complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is" -s v -l invert -d "Invert meaning of filters" +complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is" -s t -l type -d "Filter by type" -x -a '(__fish_append , file link dir block char fifo socket)' +complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is" -s f -d "Filter files" +complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is" -s d -d "Filter directories" +complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is" -s l -d "Filter symlinks" +complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is" -s p -l perm -d "Filter by permission" -x -a '(__fish_append , read write exec suid sgid user group)' +complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is" -s r -d "Filter readable paths" +complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is" -s w -d "Filter writable paths" +complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is" -s x -d "Filter executable paths" +complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] sort" \ + -l key -x -a 'basename\t"Sort only by basename" dirname\t"Sort only by dirname" path\t"Sort by full path"' +complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] sort" -s u -l unique -d 'Only leave the first of each run with the same key' + # Turn on file completions again. # match takes a glob as first arg, expand takes only globs. # We still want files completed then! From c87d0632117452770799292dca09ebce07c35ed2 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Thu, 26 May 2022 13:20:23 +0200 Subject: [PATCH 59/63] Update docs --- doc_src/cmds/path.rst | 58 +++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index 799ea4db8..256592d94 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -8,22 +8,22 @@ Synopsis :: - path basename GENERAL_OPTIONS [PATH...] - path dirname GENERAL_OPTIONS [PATH...] - path extension GENERAL_OPTIONS [PATH...] - path filter GENERAL_OPTIONS [(-v | --invert)] \ + path basename GENERAL_OPTIONS [PATH ...] + path dirname GENERAL_OPTIONS [PATH ...] + path extension GENERAL_OPTIONS [PATH ...] + path filter GENERAL_OPTIONS [-v | --invert] [-d] [-f] [-l] [-r] [-w] [-x] \ - [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH...] - path is GENERAL_OPTIONS [(-v | --invert)] [(-t | --type) TYPE] \ - [-d] [-f] [-l] [-r] [-w] [-x] \ - [(-p | --perm) PERMISSION] [PATH...] - path normalize GENERAL_OPTIONS [PATH...] - path resolve GENERAL_OPTIONS [PATH...] - path change-extension GENERAL_OPTIONS EXTENSION [PATH...] - path sort GENERAL_OPTIONS [(-v | --invert)] \ - [-u | --unique] [--key=basename|dirname|path] [([PATH...] + [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH ...] + path is GENERAL_OPTIONS [(-v | --invert)] [(-t | --type) TYPE] + [-d] [-f] [-l] [-r] [-w] [-x] + [(-p | --perm) PERMISSION] [PATH ...] + path normalize GENERAL_OPTIONS [PATH ...] + path resolve GENERAL_OPTIONS [PATH ...] + path change-extension GENERAL_OPTIONS EXTENSION [PATH ...] + path sort GENERAL_OPTIONS [-v | --invert] + [-u | --unique] [--key=basename|dirname|path] [PATH ...] - GENERAL_OPTIONS := [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] + GENERAL_OPTIONS := [-z | --null-in] [-Z | --null-out] [-q | --quiet] Description ----------- @@ -53,7 +53,7 @@ The following subcommands are available. :: - path basename [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] + path basename [-z | --null-in] [-Z | --null-out] [-q | --quiet] [PATH ...] ``path basename`` returns the last path component of the given path, by removing the directory prefix and removing trailing slashes. In other words, it is the part that is not the dirname. For files you might call it the "filename". @@ -86,7 +86,7 @@ Examples :: - path dirname [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] + path dirname [-z | --null-in] [-Z | --null-out] [-q | --quiet] [PATH ...] ``path dirname`` returns the dirname for the given path. This is the part before the last "/", discounting trailing slashes. In other words, it is the part that is not the basename (discounting superfluous slashes). @@ -111,7 +111,7 @@ Examples :: - path extension [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] + path extension [-z | --null-in] [-Z | --null-out] [-q | --quiet] [PATH ...] ``path extension`` returns the extension of the given path. This is the part after (and including) the last ".", unless that "." followed a "/" or the basename is "." or "..", in which case there is no extension and an empty line is printed. @@ -152,9 +152,9 @@ Examples :: - path filter [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] \ + path filter [-z | --null-in] [-Z | --null-out] [-q | --quiet] \ [-d] [-f] [-l] [-r] [-w] [-x] \ - [(-v | --invert)] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH...] + [-v | --invert] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH ...] ``path filter`` returns all of the given paths that match the given checks. In all cases, the paths need to exist, nonexistent paths are always filtered. @@ -214,9 +214,9 @@ Examples :: - path is [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] \ + path is [-z | --null-in] [-Z | --null-out] [-q | --quiet] \ [-d] [-f] [-l] [-r] [-w] [-x] \ - [(-v | --invert)] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH...] + [-v | --invert] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH ...] ``path is`` is short for ``path filter -q``. It returns true if any of the given files passes the filter, but does not produce any output. @@ -239,7 +239,7 @@ Examples :: - path normalize [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] + path normalize [-z | --null-in] [-Z | --null-out] [-q | --quiet] [PATH ...] ``path normalize`` returns the normalized versions of all paths. That means it squashes duplicate "/" (except for two leading "//"), collapses "../" with earlier components and removes "." components. @@ -273,7 +273,7 @@ Examples :: - path resolve [(-z | --null-in)] [(-Z | --null-out)] [(-q | --quiet)] [PATH...] + path resolve [-z | --null-in] [-Z | --null-out] [-q | --quiet] [PATH ...] ``path resolve`` returns the normalized, physical and absolute versions of all paths. That means it resolves symlinks and does what ``path normalize`` does: it squashes duplicate "/" (except for two leading "//"), collapses "../" with earlier components and removes "." components. Then it turns that path into the absolute path starting from the filesystem root "/". @@ -288,7 +288,7 @@ Examples >_ path resolve /bin//sh # The "//" is squashed, and /bin is resolved if your system links it to /usr/bin. - # sh here is bash (on an Archlinux system) + # sh here is bash (this is common on linux systems) /usr/bin/bash >_ path resolve /bin/foo///bar/../baz @@ -301,8 +301,8 @@ Examples :: - path change-extension [(-z | --null-in)] [(-Z | --null-out)] \ - [(-q | --quiet)] EXTENSION [PATH...] + path change-extension [-z | --null-in] [-Z | --null-out] \ + [-q | --quiet] EXTENSION [PATH ...] ``path change-extension`` returns the given paths, with their extension changed to the given new extension. The extension is the part after (and including) the last ".", unless that "." followed a "/" or the basename is "." or "..", in which case there is no previous extension and the new one is simply added. @@ -344,9 +344,9 @@ Examples :: - path sort [(-z | --null-in)] [(-Z | --null-out)] \ - [(-q | --quiet)] [(-v | --invert)] \ - [--key=basename|dirname|path] [([PATH...] + path sort [-z | --null-in] [-Z | --null-out] \ + [-q | --quiet] [-v | --invert] \ + [--key=basename|dirname|path] [PATH ...] ``path sort`` returns the given paths in sorted order. They are sorted in the same order as globs - alphabetically, but with runs of numerical digits compared numerically. From 1d4d238577cd0bc69478385c3604e36da207f44d Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Thu, 26 May 2022 13:22:12 +0200 Subject: [PATCH 60/63] Rename func to keyfunc --- src/builtins/path.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index 5e09cbcfa..c220bec4f 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -723,14 +723,14 @@ static int path_sort(parser_t &parser, io_streams_t &streams, int argc, const wc int retval = parse_opts(&opts, &optind, 0, argc, argv, parser, streams); if (retval != STATUS_CMD_OK) return retval; - auto func = +[] (const wcstring &x) { + auto keyfunc = +[] (const wcstring &x) { return wbasename(x); }; if (opts.have_key) { if (std::wcscmp(opts.key, L"basename") == 0) { // Do nothing, this is the default } else if (std::wcscmp(opts.key, L"dirname") == 0) { - func = +[] (const wcstring &x) { + keyfunc = +[] (const wcstring &x) { return wdirname(x); }; } else if (std::wcscmp(opts.key, L"path") == 0) { @@ -749,10 +749,10 @@ static int path_sort(parser_t &parser, io_streams_t &streams, int argc, const wc } if (opts.have_key) { - // Keep a map to avoid repeated func calls and to keep things alive. - std::map funced; + // Keep a map to avoid repeated keyfunc calls and to keep things alive. + std::map key; for (const auto &arg : list) { - funced[arg] = func(arg); + key[arg] = keyfunc(arg); } // We use a stable sort here, and also explicit < and >, @@ -760,14 +760,14 @@ static int path_sort(parser_t &parser, io_streams_t &streams, int argc, const wc std::stable_sort(list.begin(), list.end(), [&](const wcstring &a, const wcstring &b) { if (!opts.invert) - return (wcsfilecmp_glob(funced[a].c_str(), funced[b].c_str()) < 0); + return (wcsfilecmp_glob(key[a].c_str(), key[b].c_str()) < 0); else - return (wcsfilecmp_glob(funced[a].c_str(), funced[b].c_str()) > 0); + return (wcsfilecmp_glob(key[a].c_str(), key[b].c_str()) > 0); }); if (opts.unique) { list.erase(std::unique(list.begin(), list.end(), [&](const wcstring &a, const wcstring &b) { - return funced[a] == funced[b]; + return key[a] == key[b]; }), list.end()); } From c6bffe7ceb80a9ed6132837a0edbe3be6dee5a68 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Sun, 29 May 2022 17:43:03 +0200 Subject: [PATCH 61/63] Clarify comment for resolve --- src/builtins/path.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index c220bec4f..7e9071b0e 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -673,8 +673,8 @@ static int path_resolve(parser_t &parser, io_streams_t &streams, int argc, const auto real = wrealpath(*arg); if (!real) { - // The path doesn't exist, so we go up until we find - // something that does. + // The path doesn't exist, isn't readable or a symlink loop. + // We go up until we find something that works. wcstring next = *arg; // First add $PWD if we're relative if (!next.empty() && next[0] != L'/') { From c5aa796d91632d8d43756ebf2466d9ba4fca5eb0 Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Sun, 29 May 2022 17:44:31 +0200 Subject: [PATCH 62/63] Invert takes no argument --- src/builtins/path.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index 7e9071b0e..690d56d47 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -400,7 +400,7 @@ static const struct woption long_options[] = { {L"null-out", no_argument, nullptr, 'Z'}, {L"perm", required_argument, nullptr, 'p'}, {L"type", required_argument, nullptr, 't'}, - {L"invert", required_argument, nullptr, 'v'}, + {L"invert", no_argument, nullptr, 'v'}, {L"unique", no_argument, nullptr, 'u'}, {L"key", required_argument, nullptr, 1}, {}}; From 67b0860fe7c438742d77ae074a3f774d35bb7f4b Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Sun, 29 May 2022 17:47:25 +0200 Subject: [PATCH 63/63] Rename sort --invert to sort --reverse/-r To match sort(1). --- doc_src/cmds/path.rst | 6 +++--- src/builtins/path.cpp | 20 ++++++++++++++++---- tests/checks/path.fish | 2 +- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index 256592d94..4a5da7cb1 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -20,7 +20,7 @@ Synopsis path normalize GENERAL_OPTIONS [PATH ...] path resolve GENERAL_OPTIONS [PATH ...] path change-extension GENERAL_OPTIONS EXTENSION [PATH ...] - path sort GENERAL_OPTIONS [-v | --invert] + path sort GENERAL_OPTIONS [-r | --reverse] [-u | --unique] [--key=basename|dirname|path] [PATH ...] GENERAL_OPTIONS := [-z | --null-in] [-Z | --null-out] [-q | --quiet] @@ -345,13 +345,13 @@ Examples :: path sort [-z | --null-in] [-Z | --null-out] \ - [-q | --quiet] [-v | --invert] \ + [-q | --quiet] [-r | --reverse] \ [--key=basename|dirname|path] [PATH ...] ``path sort`` returns the given paths in sorted order. They are sorted in the same order as globs - alphabetically, but with runs of numerical digits compared numerically. -With ``--invert`` or ``-v`` the sort is reversed. +With ``--reverse`` or ``-r`` the sort is reversed. With ``--key=`` only the given path of the path is compared, e.g. ``--key=dirname`` causes only the dirname to be compared, ``--key=basename`` only the basename and ``--key=path`` causes the entire path to be compared (this is the default). diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index 690d56d47..12cf9441f 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -158,6 +158,7 @@ struct options_t { //!OCLINT(too many fields) bool perm_valid = false; bool type_valid = false; bool invert_valid = false; + bool reverse_valid = false; bool key_valid = false; bool unique_valid = false; bool unique = false; @@ -177,6 +178,7 @@ struct options_t { //!OCLINT(too many fields) path_perm_flags_t perm = 0; bool invert = false; + bool reverse = false; const wchar_t *arg1 = nullptr; }; @@ -306,8 +308,16 @@ static int handle_flag_perms(const wchar_t **argv, parser_t &parser, io_streams_ static int handle_flag_r(const wchar_t **argv, parser_t &parser, io_streams_t &streams, const wgetopter_t &w, options_t *opts) { - return handle_flag_perms(argv, parser, streams, w, opts, PERM_READ); + if (opts->reverse_valid) { + opts->reverse = true; + return STATUS_CMD_OK; + } else if (opts->perm_valid) { + return handle_flag_perms(argv, parser, streams, w, opts, PERM_READ); + } + path_unknown_option(parser, streams, argv[0], argv[w.woptind - 1]); + return STATUS_INVALID_ARGS; } + static int handle_flag_w(const wchar_t **argv, parser_t &parser, io_streams_t &streams, const wgetopter_t &w, options_t *opts) { return handle_flag_perms(argv, parser, streams, w, opts, PERM_WRITE); @@ -387,6 +397,7 @@ static wcstring construct_short_opts(options_t *opts) { //!OCLINT(high npath co short_opts.append(L"fld"); } if (opts->invert_valid) short_opts.append(L"v"); + if (opts->reverse_valid) short_opts.append(L"r"); if (opts->unique_valid) short_opts.append(L"u"); return short_opts; } @@ -401,6 +412,7 @@ static const struct woption long_options[] = { {L"perm", required_argument, nullptr, 'p'}, {L"type", required_argument, nullptr, 't'}, {L"invert", no_argument, nullptr, 'v'}, + {L"reverse", no_argument, nullptr, 'r'}, {L"unique", no_argument, nullptr, 'u'}, {L"key", required_argument, nullptr, 1}, {}}; @@ -716,7 +728,7 @@ static int path_resolve(parser_t &parser, io_streams_t &streams, int argc, const static int path_sort(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { options_t opts; - opts.invert_valid = true; + opts.reverse_valid = true; opts.key_valid = true; opts.unique_valid = true; int optind; @@ -759,7 +771,7 @@ static int path_sort(parser_t &parser, io_streams_t &streams, int argc, const wc // to avoid changing the order so you can chain calls. std::stable_sort(list.begin(), list.end(), [&](const wcstring &a, const wcstring &b) { - if (!opts.invert) + if (!opts.reverse) return (wcsfilecmp_glob(key[a].c_str(), key[b].c_str()) < 0); else return (wcsfilecmp_glob(key[a].c_str(), key[b].c_str()) > 0); @@ -776,7 +788,7 @@ static int path_sort(parser_t &parser, io_streams_t &streams, int argc, const wc // so we have no need to transform and such. std::stable_sort(list.begin(), list.end(), [&](const wcstring &a, const wcstring &b) { - if (!opts.invert) + if (!opts.reverse) return (wcsfilecmp_glob(a.c_str(), b.c_str()) < 0); else return (wcsfilecmp_glob(a.c_str(), b.c_str()) > 0); diff --git a/tests/checks/path.fish b/tests/checks/path.fish index 41ebc6492..410e12d0b 100644 --- a/tests/checks/path.fish +++ b/tests/checks/path.fish @@ -158,7 +158,7 @@ string replace -r "^"(pwd -P | string escape --style=regex)'/' "" -- $path path resolve /banana//terracota/terracota/booooo/../pie # CHECK: /banana/terracota/terracota/pie -path sort --key=basename {def,abc}/{456,123,789,abc,def,0} | path sort --key=dirname -v +path sort --key=basename {def,abc}/{456,123,789,abc,def,0} | path sort --key=dirname -r # CHECK: def/0 # CHECK: def/123 # CHECK: def/456