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