diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d91cb8f2..bedb588d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### Scripting improvements - Range limits in index range expansions like `$x[$start..$end]` may be omitted: `$start` and `$end` default to 1 and -1 (the last item) respectively. +- `string sub` has a new `--end` option to specify the end index of a substring (#6765). ### Interactive improvements diff --git a/doc_src/cmds/string-sub.rst b/doc_src/cmds/string-sub.rst index 16cc2eb82..a10d8f0fa 100644 --- a/doc_src/cmds/string-sub.rst +++ b/doc_src/cmds/string-sub.rst @@ -8,7 +8,7 @@ Synopsis :: - string sub [(-s | --start) START] [(-l | --length) LENGTH] [(-q | --quiet)] [STRING...] + string sub [(-s | --start) START] [(-e | --end) END] [(-l | --length) LENGTH] [(-q | --quiet)] [STRING...] .. END SYNOPSIS @@ -17,7 +17,7 @@ Description .. BEGIN DESCRIPTION -``string sub`` prints a substring of each string argument. The start of the substring can be specified with ``-s`` or ``--start`` followed by a 1-based index value. Positive index values are relative to the start of the string and negative index values are relative to the end of the string. The default start value is 1. The length of the substring can be specified with ``-l`` or ``--length``. If the length is not specified, the substring continues to the end of each STRING. Exit status: 0 if at least one substring operation was performed, 1 otherwise. +``string sub`` prints a substring of each string argument. The start/end of the substring can be specified with ``-s``/``-e`` or ``--start``/``--end`` followed by a 1-based index value. Positive index values are relative to the start of the string and negative index values are relative to the end of the string. The default start value is 1. The length of the substring can be specified with ``-l`` or ``--length``. If the length or end is not specified, the substring continues to the end of each STRING. Exit status: 0 if at least one substring operation was performed, 1 otherwise. ``--length`` is mutually exclusive with ``--end``. .. END DESCRIPTION @@ -37,4 +37,16 @@ Examples >_ string sub --start=-2 abcde de + >_ string sub --end=3 abcde + abc + + >_ string sub -e -1 abcde + abcd + + >_ string sub -s 2 -e -1 abcde + bcd + + >_ string sub -s -3 -e -2 abcde + c + .. END EXAMPLES diff --git a/share/completions/string.fish b/share/completions/string.fish index 55188a063..6c4de96f1 100644 --- a/share/completions/string.fish +++ b/share/completions/string.fish @@ -8,6 +8,7 @@ complete -f -c string -n "test (count (commandline -opc)) -lt 2" -a upper complete -f -c string -n "test (count (commandline -opc)) -lt 2" -a length complete -f -c string -n "test (count (commandline -opc)) -lt 2" -a sub complete -x -c string -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] sub" -s s -l start -xa "(seq 1 10)" +complete -x -c string -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] sub" -s e -l end -xa "(seq 1 10)" complete -x -c string -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] sub" -s l -l length -xa "(seq 1 10)" complete -f -c string -n "test (count (commandline -opc)) -lt 2" -a split complete -f -c string -n "test (count (commandline -opc)) -lt 2" -a split0 diff --git a/src/builtin_string.cpp b/src/builtin_string.cpp index bfdbd334b..8476b307c 100644 --- a/src/builtin_string.cpp +++ b/src/builtin_string.cpp @@ -151,6 +151,7 @@ typedef struct { //!OCLINT(too many fields) bool regex_valid = false; bool right_valid = false; bool start_valid = false; + bool end_valid = false; bool style_valid = false; bool no_empty_valid = false; bool no_trim_newlines_valid = false; @@ -174,6 +175,7 @@ typedef struct { //!OCLINT(too many fields) long length = 0; long max = 0; long start = 0; + long end = 0; const wchar_t *chars_to_trim = L" \f\n\r\t"; const wchar_t *arg1 = nullptr; @@ -242,7 +244,17 @@ static int handle_flag_c(wchar_t **argv, parser_t &parser, io_streams_t &streams static int handle_flag_e(wchar_t **argv, parser_t &parser, io_streams_t &streams, const wgetopter_t &w, options_t *opts) { - if (opts->entire_valid) { + if (opts->end_valid) { + opts->end = fish_wcstol(w.woptarg); + if (opts->end == 0 || opts->end == LONG_MIN || errno == ERANGE) { + string_error(streams, _(L"%ls: Invalid end value '%ls'\n"), argv[0], w.woptarg); + return STATUS_INVALID_ARGS; + } else if (errno) { + string_error(streams, BUILTIN_ERR_NOT_NUMBER, argv[0], w.woptarg); + return STATUS_INVALID_ARGS; + } + return STATUS_CMD_OK; + } else if (opts->entire_valid) { opts->entire = true; return STATUS_CMD_OK; } @@ -408,6 +420,7 @@ static wcstring construct_short_opts(options_t *opts) { //!OCLINT(high npath co if (opts->regex_valid) short_opts.append(L"r"); if (opts->right_valid) short_opts.append(L"r"); if (opts->start_valid) short_opts.append(L"s:"); + if (opts->end_valid) short_opts.append(L"e:"); if (opts->no_empty_valid) short_opts.append(L"n"); if (opts->no_trim_newlines_valid) short_opts.append(L"N"); return short_opts; @@ -420,6 +433,7 @@ static const struct woption long_options[] = {{L"all", no_argument, nullptr, 'a' {L"chars", required_argument, nullptr, 'c'}, {L"count", required_argument, nullptr, 'n'}, {L"entire", no_argument, nullptr, 'e'}, + {L"end", required_argument, nullptr, 'e'}, {L"filter", no_argument, nullptr, 'f'}, {L"ignore-case", no_argument, nullptr, 'i'}, {L"index", no_argument, nullptr, 'n'}, @@ -1201,21 +1215,31 @@ static int string_repeat(parser_t &parser, io_streams_t &streams, int argc, wcha } static int string_sub(parser_t &parser, io_streams_t &streams, int argc, wchar_t **argv) { + wchar_t *cmd = argv[0]; + options_t opts; opts.length_valid = true; opts.quiet_valid = true; opts.start_valid = true; + opts.end_valid = true; opts.length = -1; int optind; int retval = parse_opts(&opts, &optind, 0, argc, argv, parser, streams); if (retval != STATUS_CMD_OK) return retval; + if (opts.length != -1 && opts.end != 0) { + streams.err.append_format(BUILTIN_ERR_COMBO2, cmd, + _(L"--end and --length are mutually exclusive")); + return STATUS_INVALID_ARGS; + } + int nsub = 0; arg_iterator_t aiter(argv, optind, streams); while (const wcstring *s = aiter.nextstr()) { using size_type = wcstring::size_type; size_type pos = 0; size_type count = wcstring::npos; + if (opts.start > 0) { pos = static_cast(opts.start - 1); } else if (opts.start < 0) { @@ -1223,12 +1247,23 @@ static int string_sub(parser_t &parser, io_streams_t &streams, int argc, wchar_t size_type n = static_cast(-opts.start); pos = n > s->length() ? 0 : s->length() - n; } + if (pos > s->length()) { pos = s->length(); } if (opts.length >= 0) { count = static_cast(opts.length); + } else if (opts.end != 0) { + size_type n; + if (opts.end > 0) { + n = static_cast(opts.end); + } else { + assert(opts.end != LONG_MIN); // checked above + n = static_cast(-opts.end); + n = n > s->length() ? 0 : s->length() - n; + } + count = n < pos ? 0 : n - pos; } // Note that std::string permits count to extend past end of string. diff --git a/tests/checks/string.fish b/tests/checks/string.fish index da967c93a..b3d598a49 100644 --- a/tests/checks/string.fish +++ b/tests/checks/string.fish @@ -51,6 +51,30 @@ string sub -s 2 -l 2 abcde string sub --start=-2 abcde # CHECK: de +string sub --end=3 abcde +# CHECK: abc + +string sub --end=-4 abcde +# CHECK: a + +string sub --start=2 --end=-2 abcde +# CHECK: bc + +string sub -s -5 -e -2 abcdefgh +# CHECK: def + +string sub -s -100 -e -2 abcde +# CHECK: abc + +string sub -s -5 -e 2 abcde +# CHECK: ab + +string sub -s -50 -e -100 abcde +# CHECK: + +string sub -s 2 -e -5 abcde +# CHECK: + string split . example.com # CHECK: example # CHECK: com