From b41e5cbbb7bb5918e3735dec629c86be6ed49e8a Mon Sep 17 00:00:00 2001 From: Lily Ballard Date: Sat, 15 Jun 2019 22:30:31 -0700 Subject: [PATCH] Add string collect The `string collect` subcommand behaves quite similarly in practice to `string split0 -m 0` in that it doesn't split its output, but it also takes an optional `--trim-newline` flag to trim a single trailing newline off of the output. See issue #159. --- CHANGELOG.md | 1 + share/completions/string.fish | 4 +++- sphinx_doc_src/cmds/string.rst | 29 ++++++++++++++++++++++++++++ src/builtin_string.cpp | 35 +++++++++++++++++++++++++++++++--- tests/string.err | 6 ++++++ tests/string.in | 27 ++++++++++++++++++++++++++ tests/string.out | 34 +++++++++++++++++++++++++++++++++ 7 files changed, 132 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02a86507d..0fc752dd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - The `--debug` option has been extended to allow specifying categories. Categories may be listed via `fish --print-debug-categories`. - `string replace` had an additional round of escaping in the replacement (not the match!), so escaping backslashes would require `string replace -ra '([ab])' '\\\\\\\$1' a`. A new feature flag `string-replace-fewer-backslashes` can be used to disable this, so that it becomes `string replace -ra '([ab])' '\\\\$1' a` (#5556). - Some parser errors did not set `$status` to non-zero. This has been corrected (b2a1da602f79878f4b0adc4881216c928a542608). +- `string` has a new `collect` subcommand that disables newline-splitting on its input. This is meant to be used as the end of a command substitution pipeline to produce a single output argument potentially containing newlines, such as `set contents (cat filename | string collect)`. It also supports a `--trim-newline` flag to trim a single trailing newline from the output (#159). ### Syntax changes and new commands - Brace expansion now only takes place if the braces include a "," or a variable expansion, so things like `git reset HEAD@{0}` now work (#5869). diff --git a/share/completions/string.fish b/share/completions/string.fish index 890abf870..016c39d9a 100644 --- a/share/completions/string.fish +++ b/share/completions/string.fish @@ -1,7 +1,7 @@ # Completion for builtin string # This follows a strict command-then-options approach, so we can just test the number of tokens complete -f -c string -complete -f -c string -n "test (count (commandline -opc)) -ge 2; and not contains -- (commandline -opc)[2] escape" -s q -l quiet -d "Do not print output" +complete -f -c string -n "test (count (commandline -opc)) -ge 2; and not contains -- (commandline -opc)[2] escape collect" -s q -l quiet -d "Do not print output" complete -f -c string -n "test (count (commandline -opc)) -lt 2" -a "lower" 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" @@ -13,6 +13,8 @@ complete -f -c string -n "test (count (commandline -opc)) -lt 2" -a "split0" complete -x -c string -n 'test (count (commandline -opc)) -ge 2; and string match -qr split0\?\$ -- (commandline -opc)[2]' -s m -l max -a "(seq 1 10)" -d "Specify maximum number of splits" complete -f -c string -n 'test (count (commandline -opc)) -ge 2; and string match -qr split0\?\$ -- (commandline -opc)[2]' -s r -l right -d "Split right-to-left" complete -f -c string -n 'test (count (commandline -opc)) -ge 2; and string match -qr split0\?\$ -- (commandline -opc)[2]' -s n -l no-empty -d "Empty results excluded" +complete -f -c string -n "test (count (commandline -opc)) -lt 2" -a "collect" +complete -f -c string -n 'test (count (commandline -opc)) -ge 2; and string match -qr collect\$ -- (commandline -opc)[2]' -s n -l trim-newline -d "Remove trailing newline" complete -f -c string -n "test (count (commandline -opc)) -lt 2" -a "join" complete -f -c string -n "test (count (commandline -opc)) -lt 2" -a "join0" diff --git a/sphinx_doc_src/cmds/string.rst b/sphinx_doc_src/cmds/string.rst index 1761673d1..5b68ce84e 100644 --- a/sphinx_doc_src/cmds/string.rst +++ b/sphinx_doc_src/cmds/string.rst @@ -6,6 +6,8 @@ string - manipulate strings Synopsis -------- +``string collect [(-n | --trim-newline)] [STRING...]`` + ``string escape [(-n | --no-quoted)] [--style=xxx] [STRING...]`` ``string join [(-q | --quiet)] SEP [STRING...]`` @@ -48,6 +50,17 @@ Most subcommands accept a ``-q`` or ``--quiet`` switch, which suppresses the usu The following subcommands are available. +"collect" subcommand +-------------------- + +``string collect [(-n | --trim-newline)] [STRING...]`` + +``string collect`` collects its input into a single output argument, without splitting the output when used in a command substitution. This is useful when trying to collect multiline output from another command into a variable. Exit status: 0 if any output argument is non-empty, or 1 otherwise. + +If invoked with multiple arguments instead of input, ``string collect`` preserves each argument separately, where the number of output arguments is equal to the number of arguments given to ``string collect``. + +``--trim-newline`` trims a single trailing newline off of each output argument. This is useful when collecting the output from another command as the trailing newline is frequently not desired. + "escape" and "unescape" subcommands ----------------------------------- @@ -329,6 +342,22 @@ Examples a1_20b2__c_E6_85_A1 + + +:: + + >_ echo \"(echo one\ntwo\nthree | string collect)\" + "one + two + three + " + + >_ echo \"(ech one\ntwo\nthree | string collect -n)\" + "one + two + three" + + Match Glob Examples ------------------- diff --git a/src/builtin_string.cpp b/src/builtin_string.cpp index d35cb276c..7a581c8a7 100644 --- a/src/builtin_string.cpp +++ b/src/builtin_string.cpp @@ -157,6 +157,7 @@ typedef struct { //!OCLINT(too many fields) bool start_valid = false; bool style_valid = false; bool no_empty_valid = false; + bool trim_newline_valid = false; bool all = false; bool entire = false; @@ -171,6 +172,7 @@ typedef struct { //!OCLINT(too many fields) bool regex = false; bool right = false; bool no_empty = false; + bool trim_newline = false; long count = 0; long length = 0; @@ -330,6 +332,9 @@ static int handle_flag_n(wchar_t **argv, parser_t &parser, io_streams_t &streams } else if (opts->no_empty_valid) { opts->no_empty = true; return STATUS_CMD_OK; + } else if (opts->trim_newline_valid) { + opts->trim_newline = true; + return STATUS_CMD_OK; } string_unknown_option(parser, streams, argv[0], argv[w.woptind - 1]); return STATUS_INVALID_ARGS; @@ -408,12 +413,13 @@ static wcstring construct_short_opts(options_t *opts) { //!OCLINT(high npath co if (opts->right_valid) short_opts.append(L"r"); if (opts->start_valid) short_opts.append(L"s:"); if (opts->no_empty_valid) short_opts.append(L"n"); + if (opts->trim_newline_valid) short_opts.append(L"n"); 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/functions/string.fish when `string` options change +// Remember: adjust share/completions/string.fish when `string` options change static const struct woption long_options[] = { {L"all", no_argument, NULL, 'a'}, {L"chars", required_argument, NULL, 'c'}, {L"count", required_argument, NULL, 'n'}, {L"entire", no_argument, NULL, 'e'}, @@ -424,7 +430,8 @@ static const struct woption long_options[] = { {L"no-newline", no_argument, NULL, 'N'}, {L"no-quoted", no_argument, NULL, 'n'}, {L"quiet", no_argument, NULL, 'q'}, {L"regex", no_argument, NULL, 'r'}, {L"right", no_argument, NULL, 'r'}, {L"start", required_argument, NULL, 's'}, - {L"style", required_argument, NULL, 1}, {NULL, 0, NULL, 0}}; + {L"style", required_argument, NULL, 1}, {L"trim-newline", no_argument, NULL, 'n'}, + {NULL, 0, NULL, 0}}; static const std::unordered_map flag_to_function = { {'N', handle_flag_N}, {'a', handle_flag_a}, {'c', handle_flag_c}, {'e', handle_flag_e}, @@ -1125,6 +1132,27 @@ static int string_split0(parser_t &parser, io_streams_t &streams, int argc, wcha return string_split_maybe0(parser, streams, argc, argv, true /* is_split0 */); } +static int string_collect(parser_t &parser, io_streams_t &streams, int argc, wchar_t **argv) { + options_t opts; + opts.trim_newline_valid = true; + int optind; + int retval = parse_opts(&opts, &optind, 0, argc, argv, parser, streams); + if (retval != STATUS_CMD_OK) return retval; + + auto &buff = streams.out.buffer(); + arg_iterator_t aiter(argv, optind, streams, /* don't split */ false); + while (const wcstring *arg = aiter.nextstr()) { + auto end = arg->cend(); + if (opts.trim_newline && !arg->empty() && arg->back() == L'\n') { + --end; + } + + buff.append(arg->cbegin(), end, separation_type_t::explicitly); + } + + return buff.size() > 0 ? STATUS_CMD_OK : STATUS_CMD_ERROR; +} + // 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) { @@ -1305,7 +1333,8 @@ string_subcommands[] = { {L"length", &string_length}, {L"match", &string_match}, {L"replace", &string_replace}, {L"split", &string_split}, {L"split0", &string_split0}, {L"sub", &string_sub}, {L"trim", &string_trim}, {L"lower", &string_lower}, {L"upper", &string_upper}, - {L"repeat", &string_repeat}, {L"unescape", &string_unescape}, {NULL, NULL}}; + {L"repeat", &string_repeat}, {L"unescape", &string_unescape}, {L"collect", &string_collect}, + {NULL, NULL}}; /// 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 478007e2d..9718fd86c 100644 --- a/tests/string.err +++ b/tests/string.err @@ -309,3 +309,9 @@ string repeat: Unknown option '-l' #################### # string split0 in functions + +#################### +# string collect + +#################### +# string collect in functions diff --git a/tests/string.in b/tests/string.in index 7bb386d0e..b4f324023 100644 --- a/tests/string.in +++ b/tests/string.in @@ -392,4 +392,31 @@ function dualsplit end count (dualsplit) +logmsg string collect +count (echo one\ntwo\nthree\nfour | string collect) +count (echo one | string collect) +echo [(echo one\ntwo\nthree | string collect)] +echo [(echo one\ntwo\nthree | string collect -n)] +printf '[%s]\n' (string collect one\n\n two\n) +printf '[%s]\n' (string collect -n one\n\n two\n) +printf '[%s]\n' (string collect --trim-newline one\n\n two\n) +# string collect returns 0 when it has any output, otherwise 1 +string collect >/dev/null; and echo unexpected success; or echo expected failure +echo -n | string collect >/dev/null; and echo unexpected success; or echo expected failure +echo | string collect >/dev/null; and echo expected success; or echo unexpected failure +echo | string collect -n >/dev/null; and echo unexpected success; or echo expected failure +string collect a >/dev/null; and echo expected success; or echo unexpected failure +string collect '' >/dev/null; and echo unexpected success; or echo expected failure +string collect -n \n >/dev/null; and echo unexpected success; or echo expected failure + +logmsg string collect in functions +# This function outputs some newline-separated content, and some +# explicitly un-separated content. +function dualcollect + echo alpha + echo beta + echo gamma\ndelta\nomega | string collect +end +count (dualcollect) + exit 0 diff --git a/tests/string.out b/tests/string.out index bdd0cb568..ecbe5ef1d 100644 --- a/tests/string.out +++ b/tests/string.out @@ -483,3 +483,37 @@ Split something #################### # string split0 in functions 4 + +#################### +# string collect +1 +1 +[one +two +three +] +[one +two +three] +[one + +] +[two +] +[one +] +[two] +[one +] +[two] +expected failure +expected failure +expected success +expected failure +expected success +expected failure +expected failure + +#################### +# string collect in functions +3