add string replace --filter flag

Fixes #3348
This commit is contained in:
Kurtis Rader 2017-04-27 21:53:39 -07:00
parent 48d5342601
commit 16816a1202
6 changed files with 93 additions and 60 deletions

View file

@ -16,6 +16,7 @@
- Fish is now more forgiving of missing or invalid $TERM values (#3850).
- The `string` command now supports a `repeat` subcommand with the obvious behavior (#3864).
- The `string match` command now supports a `--filter` flag to emit the entire string partially matched by a pattern (#3957).
- The `string replace` command now supports a `--filter` flag to limit output to strings which underwent a replacement (#3348).
- The `functions --details --verbose` output now includes the function description (#597).
- Completions for `helm` added (#3829).
- Empty components in $CDPATH, $MANPATH and $PATH are now converted to "." (#2106, #3914).

View file

@ -13,7 +13,7 @@ string trim [(-l | --left)] [(-r | --right)] [(-c | --chars CHARS)]
string escape [(-n | --no-quoted)] [STRING...]
string match [(-a | --all)] [((-f | --filter)] [(-i | --ignore-case)] [(-r | --regex)]
[(-n | --index)] [(-q | --quiet)] [(-v | --invert)] PATTERN [STRING...]
string replace [(-a | --all)] [(-i | --ignore-case)] [(-r | --regex)]
string replace [(-a | --all)] [(-f | --filter)] [(-i | --ignore-case)] [(-r | --regex)]
[(-q | --quiet)] PATTERN REPLACEMENT [STRING...]
string repeat [(-n | --count)] [(-m | --max)] [(-N | --no-newline)]
[(-q | --quiet)] [STRING...]
@ -76,7 +76,13 @@ Exit status: 0 if at least one match was found, or 1 otherwise.
\subsection string-replace "replace" subcommand
`string replace` is similar to `string 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.
`string replace` is similar to `string 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}`.
If you specify the `-f` or `--filter` flag then each input string is printed only if a replacement was done. This is useful where you would otherwise use this idiom: `a_cmd | string match pattern | string replace pattern new_pattern`. You can instead just write `a_cmd | string replace --filter pattern new_pattern`.
Exit status: 0 if at least one replacement was performed, or 1 otherwise.
\subsection string-repeat "repeat" subcommand

View file

@ -307,13 +307,11 @@ class wildcard_matcher_t : public string_matcher_t {
wcpattern[i] = towlower(wcpattern[i]);
}
}
if (opts.filter) {
if (!wcpattern.empty()) {
if (opts.filter && !wcpattern.empty()) {
if (wcpattern.front() != ANY_STRING) wcpattern.insert(0, 1, ANY_STRING);
if (wcpattern.back() != ANY_STRING) wcpattern.push_back(ANY_STRING);
}
}
}
virtual ~wildcard_matcher_t() {}
@ -607,10 +605,11 @@ static int string_match(parser_t &parser, io_streams_t &streams, int argc, wchar
struct replace_options_t {
bool all;
bool filter;
bool ignore_case;
bool quiet;
replace_options_t() : all(false), ignore_case(false), quiet(false) {}
replace_options_t() : all(false), filter(false), ignore_case(false), quiet(false) {}
};
class string_replacer_t {
@ -625,8 +624,8 @@ class string_replacer_t {
: argv0(argv0_), opts(opts_), total_replaced(0), streams(streams_) {}
virtual ~string_replacer_t() {}
virtual bool replace_matches(const wchar_t *arg) = 0;
int replace_count() { return total_replaced; }
virtual bool replace_matches(const wchar_t *arg) = 0;
};
class literal_replacer_t : public string_replacer_t {
@ -643,34 +642,7 @@ class literal_replacer_t : public string_replacer_t {
patlen(wcslen(pattern)) {}
virtual ~literal_replacer_t() {}
bool replace_matches(const wchar_t *arg) {
wcstring result;
if (patlen == 0) {
result = arg;
} else {
int replaced = 0;
const wchar_t *cur = arg;
while (*cur != L'\0') {
if ((opts.all || replaced == 0) &&
(opts.ignore_case ? wcsncasecmp(cur, pattern, patlen)
: wcsncmp(cur, pattern, patlen)) == 0) {
result += replacement;
cur += patlen;
replaced++;
total_replaced++;
} else {
result += *cur;
cur++;
}
}
}
if (!opts.quiet) {
streams.out.append(result);
streams.out.append(L'\n');
}
return true;
}
bool replace_matches(const wchar_t *arg);
};
class regex_replacer_t : public string_replacer_t {
@ -704,11 +676,41 @@ class regex_replacer_t : public string_replacer_t {
/// A return value of true means all is well (even if no replacements were performed), false
/// indicates an unrecoverable error.
bool regex_replacer_t::replace_matches(const wchar_t *arg) {
if (regex.code == 0) {
// pcre2_compile() failed
return false;
bool literal_replacer_t::replace_matches(const wchar_t *arg) {
wcstring result;
bool replacement_occurred = false;
if (patlen == 0) {
replacement_occurred = true;
result = arg;
} else {
auto &cmp_func = opts.ignore_case ? wcsncasecmp : wcsncmp;
const wchar_t *cur = arg;
while (*cur != L'\0') {
if ((opts.all || !replacement_occurred) && cmp_func(cur, pattern, patlen) == 0) {
result += replacement;
cur += patlen;
replacement_occurred = true;
total_replaced++;
} else {
result += *cur;
cur++;
}
}
}
if (!opts.quiet && (!opts.filter || replacement_occurred)) {
streams.out.append(result);
streams.out.append(L'\n');
}
return true;
}
/// A return value of true means all is well (even if no replacements were performed), false
/// indicates an unrecoverable error.
bool regex_replacer_t::replace_matches(const wchar_t *arg) {
if (!regex.code) return false; // pcre2_compile() failed
uint32_t options = PCRE2_SUBSTITUTE_OVERFLOW_LENGTH | PCRE2_SUBSTITUTE_EXTENDED |
(opts.all ? PCRE2_SUBSTITUTE_GLOBAL : 0);
@ -744,7 +746,8 @@ bool regex_replacer_t::replace_matches(const wchar_t *arg) {
pcre2_strerror(pcre2_rc).c_str());
rc = false;
} else {
if (!opts.quiet) {
bool replacement_occurred = pcre2_rc > 0;
if (!opts.quiet && (!opts.filter || replacement_occurred)) {
streams.out.append(output);
streams.out.append(L'\n');
}
@ -756,30 +759,26 @@ bool regex_replacer_t::replace_matches(const wchar_t *arg) {
}
static int string_replace(parser_t &parser, io_streams_t &streams, int argc, wchar_t **argv) {
const wchar_t *short_options = L"aiqr";
const struct woption long_options[] = {{L"all", no_argument, 0, 'a'},
{L"ignore-case", no_argument, 0, 'i'},
{L"quiet", no_argument, 0, 'q'},
{L"regex", no_argument, 0, 'r'},
{0, 0, 0, 0}};
const wchar_t *short_options = L"afiqr";
const struct woption long_options[] = {
{L"all", no_argument, NULL, 'a'}, {L"filter", no_argument, NULL, 'f'},
{L"ignore-case", no_argument, NULL, 'i'}, {L"quiet", no_argument, NULL, 'q'},
{L"regex", no_argument, 0, 'r'}, {NULL, 0, NULL, 0}};
replace_options_t opts;
bool regex = false;
int opt;
wgetopter_t w;
for (;;) {
int opt = w.wgetopt_long(argc, argv, short_options, long_options, 0);
if (opt == -1) {
break;
}
while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, NULL)) != -1) {
switch (opt) {
case 0: {
break;
}
case 'a': {
opts.all = true;
break;
}
case 'f': {
opts.filter = true;
break;
}
case 'i': {
opts.ignore_case = true;
break;
@ -797,7 +796,7 @@ static int string_replace(parser_t &parser, io_streams_t &streams, int argc, wch
return BUILTIN_STRING_ERROR;
}
default: {
DIE("unexpected opt");
DIE("unexpected retval from wgetopt_long");
break;
}
}

View file

@ -5,7 +5,7 @@ string match: ^
# string invalidarg
string: Unknown subcommand 'invalidarg'
Standard input (line 167):
Standard input (line 183):
string invalidarg; and echo "unexpected exit 0" >&2
^
@ -29,6 +29,6 @@ string repeat: Expected argument
# string repeat -l fakearg 2>&1
string repeat: Unknown option '-l'
Standard input (line 268):
Standard input (line 284):
string repeat -l fakearg
^

View file

@ -158,6 +158,22 @@ echo
echo '# string replace -r -a "(\w)" "\$1\$1" ab'
string replace -r -a "(\w)" "\$1\$1" ab
echo
echo '# string replace --filter x X abc axc x def jkx'
string replace --filter x X abc axc x def jkx
or echo Unexpected exit status at line (status --current-line-number)
string replace --filter y Y abc axc x def jkx
and echo Unexpected exit status at line (status --current-line-number)
echo
echo '# string replace --regex -f "\d" X 1bc axc 2 d3f jk4 xyz'
string replace --regex -f "\d" X 1bc axc 2 d3f jk4 xyz
or echo Unexpected exit status at line (status --current-line-number)
string replace --regex -f "Z" X 1bc axc 2 d3f jk4 xyz
and echo Unexpected exit status at line (status --current-line-number)
# test some failure cases
echo '# string match -r "[" "a[sd"' >&2
string match -r "[" "a[sd"; and echo "unexpected exit 0" >&2

View file

@ -133,6 +133,17 @@ here
# string replace -r -a "(\w)" "\$1\$1" ab
aabb
# string replace --filter x X abc axc x def jkx
aXc
X
jkX
# string replace --regex -f "\d" X 1bc axc 2 d3f jk4 xyz
Xbc
X
dXf
jkX
# string length
missing argument returns 1