From 98f4e496690e0d1c7c562df9b4445e18cc3bd70e Mon Sep 17 00:00:00 2001 From: Greynad Date: Tue, 7 Mar 2017 15:39:21 +0100 Subject: [PATCH] Add string 'repeat' subcommand This feature add the ability to repeat a string a given number of times. For example: string repeat -n 3 foo --- doc_src/string.txt | 20 ++++++ share/completions/string.fish | 4 ++ src/builtin_string.cpp | 115 +++++++++++++++++++++++++++++++++- tests/string.err | 7 +++ tests/string.in | 43 +++++++++++++ tests/string.out | 20 ++++++ 6 files changed, 208 insertions(+), 1 deletion(-) diff --git a/doc_src/string.txt b/doc_src/string.txt index 92b0cb75e..0aa108035 100644 --- a/doc_src/string.txt +++ b/doc_src/string.txt @@ -15,6 +15,8 @@ string match [(-a | --all)] [(-i | --ignore-case)] [(-r | --regex)] [(-n | --index)] [(-q | --quiet)] [(-v | --invert)] PATTERN [STRING...] string replace [(-a | --all)] [(-i | --ignore-case)] [(-r | --regex)] [(-q | --quiet)] PATTERN REPLACEMENT [STRING...] +string repeat [(-n | --count)] [(-m | --max)] [(-N | --no-newline)] + [(-q | --quiet)] [STRING...] \endfish @@ -48,6 +50,8 @@ The following subcommands are available: - `replace` is similar to `match` but replaces non-overlapping matching substrings with a replacement string and prints the result. By default, PATTERN is treated as a literal substring to be matched. If `-r` or `--regex` is given, PATTERN is interpreted as a Perl-compatible regular expression, and REPLACEMENT can contain C-style escape sequences like `\t` as well as references to capturing groups by number or name as `$n` or `${n}`. Exit status: 0 if at least one replacement was performed, or 1 otherwise. +- `repeat` repeats the STRING `-n` or `--count` times. The `-m` or `--max` option will limit the number of outputed char (excluding the newline). This option can be used by itself or in conjuction with `--count`. If both `--count` and `--max` are present, max char will be outputed unless the final repeated string size is less than max, in that case, the string will repeat until count has been reached. Both `--count` and `--max` will accept a number greater than or equal to zero, in the case of zero, nothing will be outputed. If `-N` or `--no-newline` is given, the output won't contain a newline character at the end. Exit status: 0 if yielded string is not empty, 1 otherwise. + \subsection regular-expressions Regular Expressions Both the `match` and `replace` subcommand support regular expressions when used with the `-r` or `--regex` option. The dialect is that of PCRE2. @@ -190,3 +194,19 @@ In general, special characters are special by default, so `a+` matches one or mo put a here \endfish + +\subsection string-example-repeat Repeat Examples + +\fish{cli-dark} +>_ string repeat -n 2 'foo ' +foo foo + +>_ echo foo | string repeat -n 2 +foofoo + +>_ string repeat -n 2 -m 5 'foo' +foofo + +>_ string repeat -m 5 'foo' +foofo +\endfish diff --git a/share/completions/string.fish b/share/completions/string.fish index f207817d8..9d43d20af 100644 --- a/share/completions/string.fish +++ b/share/completions/string.fish @@ -24,3 +24,7 @@ complete -f -c string -n "test (count (commandline -opc)) -lt 2" -a "replace" complete -f -c string -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] match replace" -s a -l all -d "Report all matches per line/string" complete -f -c string -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] match replace" -s i -l ignore-case -d "Case insensitive" complete -f -c string -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] match replace" -s r -l regex -d "Use regex instead of globs" +complete -f -c string -n "test (count (commandline -opc)) -lt 2" -a "repeat" +complete -f -c string -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] repeat" -s n -l count -a "(seq 1 10)" -d "Repetition count" +complete -f -c string -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] repeat" -s m -l max -a "(seq 1 10)" -d "Maximum number of printed char" +complete -f -c string -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] repeat" -s N -l no-newline -d "Remove newline" diff --git a/src/builtin_string.cpp b/src/builtin_string.cpp index f2b565fda..1b899d34d 100644 --- a/src/builtin_string.cpp +++ b/src/builtin_string.cpp @@ -949,6 +949,118 @@ static int string_split(parser_t &parser, io_streams_t &streams, int argc, wchar return splits.size() > arg_count ? BUILTIN_STRING_OK : BUILTIN_STRING_NONE; } +// Helper function to abstract the repeat logic from string_repeat +// returns the to_repeat string, repeated count times. +static wcstring wcsrepeat(const wcstring &to_repeat, size_t count) { + wcstring repeated; + repeated.reserve(to_repeat.length() * count); + + for (size_t j = 0; j < count; j++) { + repeated += to_repeat; + } + + return repeated; +} + +// Helper function to abstract the repeat until logic from string_repeat +// returns the to_repeat string, repeated until max char has been reached. +static wcstring wcsrepeat_until(const wcstring &to_repeat, size_t max) { + size_t count = max / to_repeat.length(); + size_t mod = max % to_repeat.length(); + + return wcsrepeat(to_repeat, count) + to_repeat.substr(0, mod); +} + +static int string_repeat(parser_t &parser, io_streams_t &streams, int argc, wchar_t **argv) { + const wchar_t *short_options = L":n:m:Nq"; + const struct woption long_options[] = {{L"count", required_argument, 0, 'n'}, + {L"max", required_argument, 0, 'm'}, + {L"no-newline", no_argument, 0, 'N'}, + {L"quiet", no_argument, 0, 'q'}, + {0, 0, 0, 0}}; + + size_t count = 0; + size_t max = 0; + bool newline = true; + bool quiet = false; + int opt; + wgetopter_t w; + + while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, NULL)) != -1) { + switch (opt) { + case 'n': { + long lcount = fish_wcstol(w.woptarg); + if (lcount < 0 || errno == ERANGE) { + string_error(streams, _(L"%ls: Invalid count value '%ls'\n"), argv[0], w.woptarg); + return BUILTIN_STRING_ERROR; + } else if (errno) { + string_error(streams, BUILTIN_ERR_NOT_NUMBER, argv[0], w.woptarg); + return BUILTIN_STRING_ERROR; + } + count = static_cast(lcount); + break; + } + case 'm': { + long lmax = fish_wcstol(w.woptarg); + if (lmax < 0 || errno == ERANGE) { + string_error(streams, _(L"%ls: Invalid max value '%ls'\n"), argv[0], w.woptarg); + return BUILTIN_STRING_ERROR; + } else if (errno) { + string_error(streams, BUILTIN_ERR_NOT_NUMBER, argv[0], w.woptarg); + return BUILTIN_STRING_ERROR; + } + max = static_cast(lmax); + break; + } + case 'N': { + newline = false; + break; + } + case 'q': { + quiet = true; + break; + } + case ':': { + string_error(streams, STRING_ERR_MISSING, argv[0]); + return BUILTIN_STRING_ERROR; + } + case '?': { + string_unknown_option(parser, streams, argv[0], argv[w.woptind - 1]); + return BUILTIN_STRING_ERROR; + } + default: { + DIE("unexpected opt"); + break; + } + } + } + + int i = w.woptind; + + if (string_args_from_stdin(streams) && argc > i) { + string_error(streams, BUILTIN_ERR_TOO_MANY_ARGUMENTS, argv[0]); + return BUILTIN_STRING_ERROR; + } + + const wchar_t *to_repeat; + wcstring storage; + bool is_empty = true; + + if ((to_repeat = string_get_arg(&i, argv, &storage, streams)) != NULL) { + const wcstring word(to_repeat); + const bool rep_until = (0 < max && word.length()*count > max) || !count; + const wcstring repeated = rep_until ? wcsrepeat_until(word, max) : wcsrepeat(word, count); + is_empty = repeated.empty(); + + if (!quiet && !is_empty) { + streams.out.append(repeated); + if (newline) streams.out.append(L"\n"); + } + } + + return !is_empty ? BUILTIN_STRING_OK : BUILTIN_STRING_NONE; +} + static int string_sub(parser_t &parser, io_streams_t &streams, int argc, wchar_t **argv) { const wchar_t *short_options = L":l:qs:"; const struct woption long_options[] = {{L"length", required_argument, 0, 'l'}, @@ -1157,7 +1269,8 @@ static const struct string_subcommand { string_subcommands[] = { {L"escape", &string_escape}, {L"join", &string_join}, {L"length", &string_length}, {L"match", &string_match}, {L"replace", &string_replace}, {L"split", &string_split}, - {L"sub", &string_sub}, {L"trim", &string_trim}, {0, 0}}; + {L"sub", &string_sub}, {L"trim", &string_trim}, {L"repeat", &string_repeat}, + {0, 0}}; /// The string builtin, for manipulating strings. int builtin_string(parser_t &parser, io_streams_t &streams, wchar_t **argv) { diff --git a/tests/string.err b/tests/string.err index e69de29bb..429761793 100644 --- a/tests/string.err +++ b/tests/string.err @@ -0,0 +1,7 @@ +string repeat: Invalid count value '-1' +string repeat: Invalid max value '-1' +string repeat: Argument 'notanumber' is not a number +string repeat: Argument 'notanumber' is not a number +string repeat: Too many arguments +string repeat: Expected argument +string repeat: Unknown option '-l' diff --git a/tests/string.in b/tests/string.in index 15feb4f55..ed07b9318 100644 --- a/tests/string.in +++ b/tests/string.in @@ -90,3 +90,46 @@ string match -r -v "[dcantg].*" dog can cat diz; or echo "no regexp invert match string match -v "???" dog can cat diz; or echo "no glob invert match" string match -rvn a bbb + +# test repeat subcommand +string repeat -n 2 'foo' + +string repeat --count 2 'foo' + +echo foo | string repeat -n 2 + +string repeat -n2 -q 'foo'; and echo "exit 0" + +string repeat -n2 --quiet 'foo'; and echo "exit 0" + +string repeat -n0 'foo'; or echo "exit 1" + +string repeat -n0; or echo "exit 1" + +string repeat -m0; or echo "exit 1" + +string repeat -n1 -N 'there is '; echo "no newline" + +string repeat -n1 --no-newline 'there is '; echo "no newline" + +string repeat -n10 -m4 'foo' + +string repeat -n10 --max 5 'foo' + +string repeat -n3 -m20 'foo' + +string repeat -m4 'foo' + +string repeat -n-1 'foo'; or echo "exit 2" + +string repeat -m-1 'foo'; or echo "exit 2" + +string repeat -n notanumber 'foo'; or echo "exit 2" + +string repeat -m notanumber 'foo'; or echo "exit 2" + +echo 'stdin' | string repeat -n1 'and arg'; or echo "exit 2" + +string repeat -n; or echo "exit 2" + +string repeat -l fakearg 2>&1 | head -n1 1>&2 \ No newline at end of file diff --git a/tests/string.out b/tests/string.out index 7afc267ff..ca37ff360 100644 --- a/tests/string.out +++ b/tests/string.out @@ -63,3 +63,23 @@ missing argument returns 0 no regexp invert match no glob invert match 1 3 +foofoo +foofoo +foofoo +exit 0 +exit 0 +exit 1 +exit 1 +exit 1 +there is no newline +there is no newline +foof +foofo +foofoofoo +foof +exit 2 +exit 2 +exit 2 +exit 2 +exit 2 +exit 2