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.
This commit is contained in:
Lily Ballard 2019-06-15 22:30:31 -07:00
parent 5fd3bf79f5
commit b41e5cbbb7
7 changed files with 132 additions and 4 deletions

View File

@ -15,6 +15,7 @@
- The `--debug` option has been extended to allow specifying categories. Categories may be listed via `fish --print-debug-categories`. - 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). - `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). - 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 ### 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). - 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).

View File

@ -1,7 +1,7 @@
# Completion for builtin string # Completion for builtin string
# This follows a strict command-then-options approach, so we can just test the number of tokens # 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
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 "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 "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 "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 -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 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)) -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 "join"
complete -f -c string -n "test (count (commandline -opc)) -lt 2" -a "join0" complete -f -c string -n "test (count (commandline -opc)) -lt 2" -a "join0"

View File

@ -6,6 +6,8 @@ string - manipulate strings
Synopsis Synopsis
-------- --------
``string collect [(-n | --trim-newline)] [STRING...]``
``string escape [(-n | --no-quoted)] [--style=xxx] [STRING...]`` ``string escape [(-n | --no-quoted)] [--style=xxx] [STRING...]``
``string join [(-q | --quiet)] SEP [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. 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 "escape" and "unescape" subcommands
----------------------------------- -----------------------------------
@ -329,6 +342,22 @@ Examples
a1_20b2__c_E6_85_A1 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 Match Glob Examples
------------------- -------------------

View File

@ -157,6 +157,7 @@ typedef struct { //!OCLINT(too many fields)
bool start_valid = false; bool start_valid = false;
bool style_valid = false; bool style_valid = false;
bool no_empty_valid = false; bool no_empty_valid = false;
bool trim_newline_valid = false;
bool all = false; bool all = false;
bool entire = false; bool entire = false;
@ -171,6 +172,7 @@ typedef struct { //!OCLINT(too many fields)
bool regex = false; bool regex = false;
bool right = false; bool right = false;
bool no_empty = false; bool no_empty = false;
bool trim_newline = false;
long count = 0; long count = 0;
long length = 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) { } else if (opts->no_empty_valid) {
opts->no_empty = true; opts->no_empty = true;
return STATUS_CMD_OK; 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]); string_unknown_option(parser, streams, argv[0], argv[w.woptind - 1]);
return STATUS_INVALID_ARGS; 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->right_valid) short_opts.append(L"r");
if (opts->start_valid) short_opts.append(L"s:"); if (opts->start_valid) short_opts.append(L"s:");
if (opts->no_empty_valid) short_opts.append(L"n"); if (opts->no_empty_valid) short_opts.append(L"n");
if (opts->trim_newline_valid) short_opts.append(L"n");
return short_opts; return short_opts;
} }
// Note that several long flags share the same short flag. That is okay. The caller is expected // 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. // 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[] = { static const struct woption long_options[] = {
{L"all", no_argument, NULL, 'a'}, {L"chars", required_argument, NULL, 'c'}, {L"all", no_argument, NULL, 'a'}, {L"chars", required_argument, NULL, 'c'},
{L"count", required_argument, NULL, 'n'}, {L"entire", no_argument, NULL, 'e'}, {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"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"quiet", no_argument, NULL, 'q'}, {L"regex", no_argument, NULL, 'r'},
{L"right", no_argument, NULL, 'r'}, {L"start", required_argument, NULL, 's'}, {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<char, decltype(*handle_flag_N)> flag_to_function = { static const std::unordered_map<char, decltype(*handle_flag_N)> flag_to_function = {
{'N', handle_flag_N}, {'a', handle_flag_a}, {'c', handle_flag_c}, {'e', handle_flag_e}, {'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 */); 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 // Helper function to abstract the repeat logic from string_repeat
// returns the to_repeat string, repeated count times. // returns the to_repeat string, repeated count times.
static wcstring wcsrepeat(const wcstring &to_repeat, size_t count) { 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"length", &string_length}, {L"match", &string_match}, {L"replace", &string_replace},
{L"split", &string_split}, {L"split0", &string_split0}, {L"sub", &string_sub}, {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"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. /// The string builtin, for manipulating strings.
int builtin_string(parser_t &parser, io_streams_t &streams, wchar_t **argv) { int builtin_string(parser_t &parser, io_streams_t &streams, wchar_t **argv) {

View File

@ -309,3 +309,9 @@ string repeat: Unknown option '-l'
#################### ####################
# string split0 in functions # string split0 in functions
####################
# string collect
####################
# string collect in functions

View File

@ -392,4 +392,31 @@ function dualsplit
end end
count (dualsplit) 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 exit 0

View File

@ -483,3 +483,37 @@ Split something
#################### ####################
# string split0 in functions # string split0 in functions
4 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