Remove support for the ? wildcard

Fixes #4520
This commit is contained in:
ridiculousfish 2018-03-31 16:48:57 -07:00
parent 4b079e16e5
commit 6e56637cf0
15 changed files with 74 additions and 117 deletions

View file

@ -51,6 +51,7 @@ This section is for changes merged to the `major` branch that are not also merge
- The machine hostname, where available, is now exposed as `$hostname` which is now a reserved variable. This drops the dependency on the `hostname` executable (#4422). - The machine hostname, where available, is now exposed as `$hostname` which is now a reserved variable. This drops the dependency on the `hostname` executable (#4422).
- `functions --handlers` can be used to show event handlers (#4694). - `functions --handlers` can be used to show event handlers (#4694).
- Variables set in `if` and `while` conditions are available outside the block (#4820). - Variables set in `if` and `while` conditions are available outside the block (#4820).
- The `?` wildcard has been removed (#4520).
## Other significant changes ## Other significant changes
- Command substitution output is now limited to 10 MB by default (#3822). - Command substitution output is now limited to 10 MB by default (#3822).

View file

@ -92,7 +92,6 @@ Some characters can not be written directly on the command line. For these chara
- '<code>\\$</code>' escapes the dollar character - '<code>\\$</code>' escapes the dollar character
- '<code>\\\\</code>' escapes the backslash character - '<code>\\\\</code>' escapes the backslash character
- '<code>\\*</code>' escapes the star character - '<code>\\*</code>' escapes the star character
- '<code>\\?</code>' escapes the question mark character
- '<code>\\~</code>' escapes the tilde character - '<code>\\~</code>' escapes the tilde character
- '<code>\\#</code>' escapes the hash character - '<code>\\#</code>' escapes the hash character
- '<code>\\(</code>' escapes the left parenthesis character - '<code>\\(</code>' escapes the left parenthesis character
@ -330,7 +329,7 @@ These are the general purpose tab completions that `fish` provides:
- Completion of usernames for tilde expansion. - Completion of usernames for tilde expansion.
- Completion of filenames, even on strings with wildcards such as '`*`', '`**`' and '`?`'. - Completion of filenames, even on strings with wildcards such as '`*`' and '`**`'.
`fish` provides a large number of program specific completions. Most of these completions are simple options like the `-l` option for `ls`, but some are more advanced. The latter include: `fish` provides a large number of program specific completions. Most of these completions are simple options like the `-l` option for `ls`, but some are more advanced. The latter include:
@ -418,9 +417,7 @@ When an argument for a program is given on the commandline, it undergoes the pro
\subsection expand-wildcard Wildcards \subsection expand-wildcard Wildcards
If a star (`*`) or a question mark (`?`) is present in the parameter, `fish` attempts to match the given parameter to any files in such a way that: If a star (`*`) is present in the parameter, `fish` attempts to match the given parameter to any files in such a way that:
- `?` can match any single character except '/'.
- `*` can match any string of characters not containing '/'. This includes matching an empty string. - `*` can match any string of characters not containing '/'. This includes matching an empty string.
@ -446,8 +443,6 @@ Examples:
- `a*` matches any files beginning with an 'a' in the current directory. - `a*` matches any files beginning with an 'a' in the current directory.
- `???` matches any file in the current directory whose name is exactly three characters long.
- `**` matches any files and directories in the current directory and all of its subdirectories. - `**` matches any files and directories in the current directory and all of its subdirectories.
Note that for most commands, if any wildcard fails to expand, the command is not executed, <a href='#variables-status'>`$status`</a> is set to nonzero, and a warning is printed. This behavior is consistent with setting `shopt -s failglob` in bash. There are exactly 3 exceptions, namely <a href="commands.html#set">`set`</a>, <a href="commands.html#count">`count`</a> and <a href="commands.html#for">`for`</a>. Their globs are permitted to expand to zero arguments, as with `shopt -s nullglob` in bash. Note that for most commands, if any wildcard fails to expand, the command is not executed, <a href='#variables-status'>`$status`</a> is set to nonzero, and a warning is printed. This behavior is consistent with setting `shopt -s failglob` in bash. There are exactly 3 exceptions, namely <a href="commands.html#set">`set`</a>, <a href="commands.html#count">`count`</a> and <a href="commands.html#for">`for`</a>. Their globs are permitted to expand to zero arguments, as with `shopt -s nullglob` in bash.

View file

@ -6,25 +6,18 @@ function __fish_git_commits
# This allows filtering by subject with the new pager! # This allows filtering by subject with the new pager!
# Because even subject lines can be quite long, # Because even subject lines can be quite long,
# trim them (abbrev'd hash+tab+subject) to 73 characters # trim them (abbrev'd hash+tab+subject) to 73 characters
command git log --pretty=tformat:"%h"\t"%s" --all --max-count=1000 ^/dev/null \ command git log --pretty=tformat:"%h"\t"%s" --all --max-count=1000 ^/dev/null | string replace -r '(.{73}).+' '$1…'
| string replace -r '(.{73}).+' '$1…'
end end
function __fish_git_recent_commits function __fish_git_recent_commits
# Like __fish_git_commits, but not on all branches and limited to # Like __fish_git_commits, but not on all branches and limited to
# the last 50 commits. Used for fixup, where only the current branch # the last 50 commits. Used for fixup, where only the current branch
# and the latest commits make sense. # and the latest commits make sense.
command git log --pretty=tformat:"%h"\t"%s" --max-count=50 ^/dev/null \ command git log --pretty=tformat:"%h"\t"%s" --max-count=50 ^/dev/null | string replace -r '(.{73}).+' '$1…'
| string replace -r '(.{73}).+' '$1…'
end end
function __fish_git_branches function __fish_git_branches
command git branch --no-color -a $argv ^/dev/null \ command git branch --no-color -a $argv ^/dev/null # Filter out detached heads and such ("(HEAD detached at SOMESHA)", localized). | string match -v '\* (*)' | string match -r -v ' -> ' | string trim -c "* " # We assume anything that's not remote is a local branch. | string replace -r '^(?!remotes/)(.*)' '$1\tLocal Branch' | string replace -r "^remotes/(.*)" '$1\tRemote Branch'
# Filter out detached heads and such ("(HEAD detached at SOMESHA)", localized).
| string match -v '\* (*)' | string match -r -v ' -> ' | string trim -c "* " \
# We assume anything that's not remote is a local branch.
| string replace -r '^(?!remotes/)(.*)' '$1\tLocal Branch' \
| string replace -r "^remotes/(.*)" '$1\tRemote Branch'
end end
function __fish_git_tags function __fish_git_tags
@ -96,8 +89,7 @@ function __fish_git_files
# E.g. `git reset $submodule` won't do anything (not even print an error). # E.g. `git reset $submodule` won't do anything (not even print an error).
# --ignore-submodules=all was added in git 1.7.2, released July 2010. # --ignore-submodules=all was added in git 1.7.2, released July 2010.
set -l use_next set -l use_next
command git status --porcelain -z --ignore-submodules=all \ command git status --porcelain -z --ignore-submodules=all | while read -lz -d '' line
| while read -lz -d '' line
# The entire line is the "from" from a rename. # The entire line is the "from" from a rename.
if set -q use_next[1] if set -q use_next[1]
if contains -- $use_next $argv if contains -- $use_next $argv
@ -144,28 +136,31 @@ function __fish_git_files
case 'A ' AM AD case 'A ' AM AD
# Additions are only shown here if they are staged. # Additions are only shown here if they are staged.
# Otherwise it's an untracked file. # Otherwise it's an untracked file.
contains -- added $argv; or contains -- all-staged $argv contains -- added $argv
or contains -- all-staged $argv
and printf '%s\t%s\n' "$file" $added_desc and printf '%s\t%s\n' "$file" $added_desc
case '?M' case '*M'
# Modified # Modified
contains -- modified $argv contains -- modified $argv
and printf '%s\t%s\n' "$file" $modified_desc and printf '%s\t%s\n' "$file" $modified_desc
case 'M?' case 'M*'
# If the character is first ("M "), then that means it's "our" change, # If the character is first ("M "), then that means it's "our" change,
# which means it is staged. # which means it is staged.
# This is useless for many commands - e.g. `checkout` won't do anything with this. # This is useless for many commands - e.g. `checkout` won't do anything with this.
# So it needs to be requested explicitly. # So it needs to be requested explicitly.
contains -- modified-staged $argv; or contains -- all-staged $argv contains -- modified-staged $argv
or contains -- all-staged $argv
and printf '%s\t%s\n' "$file" $staged_modified_desc and printf '%s\t%s\n' "$file" $staged_modified_desc
case '?D' case '*D'
contains -- deleted $argv contains -- deleted $argv
and printf '%s\t%s\n' "$file" $deleted_desc and printf '%s\t%s\n' "$file" $deleted_desc
case 'D?' case 'D*'
# TODO: The docs are unclear on this. # TODO: The docs are unclear on this.
# There is both X unmodified and Y either M or D ("not updated") # There is both X unmodified and Y either M or D ("not updated")
# and Y is D and X is unmodified or [MARC] ("deleted in work tree"). # and Y is D and X is unmodified or [MARC] ("deleted in work tree").
# For our purposes, we assume this is a staged deletion. # For our purposes, we assume this is a staged deletion.
contains -- deleted-staged $argv; or contains -- all-staged $argv contains -- deleted-staged $argv
or contains -- all-staged $argv
and printf '%s\t%s\n' "$file" $staged_deleted_desc and printf '%s\t%s\n' "$file" $staged_deleted_desc
case '\?\?' case '\?\?'
# Untracked # Untracked
@ -201,7 +196,8 @@ end
function __fish_git_needs_command function __fish_git_needs_command
set cmd (commandline -opc) set cmd (commandline -opc)
set -l skip_next 1 set -l skip_next 1
set -q cmd[2]; or return 0 set -q cmd[2]
or return 0
# Skip first word because it's "git" or a wrapper # Skip first word because it's "git" or a wrapper
for c in $cmd[2..-1] for c in $cmd[2..-1]
test $skip_next -eq 0 test $skip_next -eq 0
@ -555,7 +551,7 @@ complete -f -c git -n '__fish_git_needs_command' -a init -d 'Create an empty git
# TODO options # TODO options
### log ### log
complete -c git -n '__fish_git_needs_command' -a shortlog -d 'Show commit shortlog' complete -c git -n '__fish_git_needs_command' -a shortlog -d 'Show commit shortlog'
complete -c git -n '__fish_git_needs_command' -a log -d 'Show commit logs' complete -c git -n '__fish_git_needs_command' -a log -d 'Show commit logs'
complete -c git -n '__fish_git_using_command log; and not contains -- -- (commandline -op)' -a '(__fish_git_refs) (__fish_git_ranges)' complete -c git -n '__fish_git_using_command log; and not contains -- -- (commandline -op)' -a '(__fish_git_refs) (__fish_git_ranges)'
@ -721,8 +717,8 @@ complete -c git -n '__fish_git_using_command log' -l irreversible-delete -s D
complete -f -c git -n '__fish_git_using_command log' -s l complete -f -c git -n '__fish_git_using_command log' -s l
function __fish__git_append_letters_nosep function __fish__git_append_letters_nosep
set -l token (commandline -tc) set -l token (commandline -tc)
printf "%s\n" $token$argv printf "%s\n" $token$argv
end end
complete -x -c git -n '__fish_git_using_command log' -l diff-filter -a '(__fish__git_append_letters_nosep a\tExclude\ added c\tExclude\ copied d\tExclude\ deleted m\tExclude\ modified r\tExclude\ renamed t\tExclude\ type\ changed u\tExclude\ unmerged x\tExclude\ unknown b\tExclude\ broken A\tAdded C\tCopied D\tDeleted M\tModified R\tRenamed T\tType\ Changed U\tUnmerged X\tUnknown B\tBroken)' complete -x -c git -n '__fish_git_using_command log' -l diff-filter -a '(__fish__git_append_letters_nosep a\tExclude\ added c\tExclude\ copied d\tExclude\ deleted m\tExclude\ modified r\tExclude\ renamed t\tExclude\ type\ changed u\tExclude\ unmerged x\tExclude\ unknown b\tExclude\ broken A\tAdded C\tCopied D\tDeleted M\tModified R\tRenamed T\tType\ Changed U\tUnmerged X\tUnknown B\tBroken)'

View file

@ -995,12 +995,6 @@ static void escape_string_script(const wchar_t *orig_in, size_t in_len, wcstring
out += *in; out += *in;
break; break;
} }
case ANY_CHAR: {
// Experimental fix for #1614. The hope is that any time these appear in a
// string, they came from wildcard expansion.
out += L'?';
break;
}
case ANY_STRING: { case ANY_STRING: {
out += L'*'; out += L'*';
break; break;
@ -1022,7 +1016,6 @@ static void escape_string_script(const wchar_t *orig_in, size_t in_len, wcstring
case L']': case L']':
case L'{': case L'{':
case L'}': case L'}':
case L'?':
case L'*': case L'*':
case L'|': case L'|':
case L';': case L';':
@ -1356,12 +1349,6 @@ static bool unescape_string_internal(const wchar_t *const input, const size_t in
} }
break; break;
} }
case L'?': {
if (unescape_special) {
to_append_or_none = ANY_CHAR;
}
break;
}
case L'$': { case L'$': {
if (unescape_special) { if (unescape_special) {
to_append_or_none = VARIABLE_EXPAND; to_append_or_none = VARIABLE_EXPAND;

View file

@ -823,15 +823,11 @@ static void remove_internal_separator(wcstring *str, bool conv) {
// Remove all instances of INTERNAL_SEPARATOR. // Remove all instances of INTERNAL_SEPARATOR.
str->erase(std::remove(str->begin(), str->end(), (wchar_t)INTERNAL_SEPARATOR), str->end()); str->erase(std::remove(str->begin(), str->end(), (wchar_t)INTERNAL_SEPARATOR), str->end());
// If conv is true, replace all instances of ANY_CHAR with '?', ANY_STRING with '*', // If conv is true, replace all instances of ANY_STRING with '*',
// ANY_STRING_RECURSIVE with '*'. // ANY_STRING_RECURSIVE with '*'.
if (conv) { if (conv) {
for (size_t idx = 0; idx < str->size(); idx++) { for (size_t idx = 0; idx < str->size(); idx++) {
switch (str->at(idx)) { switch (str->at(idx)) {
case ANY_CHAR: {
str->at(idx) = L'?';
break;
}
case ANY_STRING: case ANY_STRING:
case ANY_STRING_RECURSIVE: { case ANY_STRING_RECURSIVE: {
str->at(idx) = L'*'; str->at(idx) = L'*';
@ -918,7 +914,7 @@ static expand_error_t expand_stage_wildcards(const wcstring &input, std::vector<
wcstring path_to_expand = input; wcstring path_to_expand = input;
remove_internal_separator(&path_to_expand, flags & EXPAND_SKIP_WILDCARDS); remove_internal_separator(&path_to_expand, flags & EXPAND_SKIP_WILDCARDS);
const bool has_wildcard = wildcard_has(path_to_expand, true /* internal, i.e. ANY_CHAR */); const bool has_wildcard = wildcard_has(path_to_expand, true /* internal, i.e. ANY_STRING */);
if (has_wildcard && (flags & EXECUTABLES_ONLY)) { if (has_wildcard && (flags & EXECUTABLES_ONLY)) {
; // don't do wildcard expansion for executables, see issue #785 ; // don't do wildcard expansion for executables, see issue #785

View file

@ -4120,15 +4120,16 @@ static void test_string() {
{{L"string", L"match", 0}, STATUS_INVALID_ARGS, L""}, {{L"string", L"match", 0}, STATUS_INVALID_ARGS, L""},
{{L"string", L"match", L"", 0}, STATUS_CMD_ERROR, L""}, {{L"string", L"match", L"", 0}, STATUS_CMD_ERROR, L""},
{{L"string", L"match", L"", L"", 0}, STATUS_CMD_OK, L"\n"}, {{L"string", L"match", L"", L"", 0}, STATUS_CMD_OK, L"\n"},
{{L"string", L"match", L"?", L"a", 0}, STATUS_CMD_OK, L"a\n"}, {{L"string", L"match", L"?", L"a", 0}, STATUS_CMD_ERROR, L""},
{{L"string", L"match", L"*", L"", 0}, STATUS_CMD_OK, L"\n"}, {{L"string", L"match", L"*", L"", 0}, STATUS_CMD_OK, L"\n"},
{{L"string", L"match", L"**", L"", 0}, STATUS_CMD_OK, L"\n"}, {{L"string", L"match", L"**", L"", 0}, STATUS_CMD_OK, L"\n"},
{{L"string", L"match", L"*", L"xyzzy", 0}, STATUS_CMD_OK, L"xyzzy\n"}, {{L"string", L"match", L"*", L"xyzzy", 0}, STATUS_CMD_OK, L"xyzzy\n"},
{{L"string", L"match", L"**", L"plugh", 0}, STATUS_CMD_OK, L"plugh\n"}, {{L"string", L"match", L"**", L"plugh", 0}, STATUS_CMD_OK, L"plugh\n"},
{{L"string", L"match", L"a*b", L"axxb", 0}, STATUS_CMD_OK, L"axxb\n"}, {{L"string", L"match", L"a*b", L"axxb", 0}, STATUS_CMD_OK, L"axxb\n"},
{{L"string", L"match", L"a??b", L"axxb", 0}, STATUS_CMD_OK, L"axxb\n"}, {{L"string", L"match", L"a??b", L"axxb", 0}, STATUS_CMD_ERROR, L""},
{{L"string", L"match", L"-i", L"a??B", L"axxb", 0}, STATUS_CMD_OK, L"axxb\n"}, {{L"string", L"match", L"a??b", L"a??b", 0}, STATUS_CMD_OK, L"a??b\n"},
{{L"string", L"match", L"-i", L"a??b", L"Axxb", 0}, STATUS_CMD_OK, L"Axxb\n"}, {{L"string", L"match", L"-i", L"a??B", L"axxb", 0}, STATUS_CMD_ERROR, L""},
{{L"string", L"match", L"-i", L"a??b", L"Axxb", 0}, STATUS_CMD_ERROR, L""},
{{L"string", L"match", L"a*", L"axxb", 0}, STATUS_CMD_OK, L"axxb\n"}, {{L"string", L"match", L"a*", L"axxb", 0}, STATUS_CMD_OK, L"axxb\n"},
{{L"string", L"match", L"*a", L"xxa", 0}, STATUS_CMD_OK, L"xxa\n"}, {{L"string", L"match", L"*a", L"xxa", 0}, STATUS_CMD_OK, L"xxa\n"},
{{L"string", L"match", L"*a*", L"axa", 0}, STATUS_CMD_OK, L"axa\n"}, {{L"string", L"match", L"*a*", L"axa", 0}, STATUS_CMD_OK, L"axa\n"},
@ -4137,14 +4138,14 @@ static void test_string() {
{{L"string", L"match", L"*a", L"a", 0}, STATUS_CMD_OK, L"a\n"}, {{L"string", L"match", L"*a", L"a", 0}, STATUS_CMD_OK, L"a\n"},
{{L"string", L"match", L"a*", L"a", 0}, STATUS_CMD_OK, L"a\n"}, {{L"string", L"match", L"a*", L"a", 0}, STATUS_CMD_OK, L"a\n"},
{{L"string", L"match", L"a*b*c", L"axxbyyc", 0}, STATUS_CMD_OK, L"axxbyyc\n"}, {{L"string", L"match", L"a*b*c", L"axxbyyc", 0}, STATUS_CMD_OK, L"axxbyyc\n"},
{{L"string", L"match", L"a*b?c", L"axxbyc", 0}, STATUS_CMD_OK, L"axxbyc\n"}, {{L"string", L"match", L"a*b?c", L"axxb?c", 0}, STATUS_CMD_OK, L"axxb?c\n"},
{{L"string", L"match", L"*?", L"a", 0}, STATUS_CMD_OK, L"a\n"}, {{L"string", L"match", L"*?", L"a", 0}, STATUS_CMD_ERROR, L""},
{{L"string", L"match", L"*?", L"ab", 0}, STATUS_CMD_OK, L"ab\n"}, {{L"string", L"match", L"*?", L"ab", 0}, STATUS_CMD_ERROR, L""},
{{L"string", L"match", L"?*", L"a", 0}, STATUS_CMD_OK, L"a\n"}, {{L"string", L"match", L"?*", L"a", 0}, STATUS_CMD_ERROR, L""},
{{L"string", L"match", L"?*", L"ab", 0}, STATUS_CMD_OK, L"ab\n"}, {{L"string", L"match", L"?*", L"ab", 0}, STATUS_CMD_ERROR, L""},
{{L"string", L"match", L"\\*", L"*", 0}, STATUS_CMD_OK, L"*\n"}, {{L"string", L"match", L"\\*", L"*", 0}, STATUS_CMD_OK, L"*\n"},
{{L"string", L"match", L"a*\\", L"abc\\", 0}, STATUS_CMD_OK, L"abc\\\n"}, {{L"string", L"match", L"a*\\", L"abc\\", 0}, STATUS_CMD_OK, L"abc\\\n"},
{{L"string", L"match", L"a*\\?", L"abc?", 0}, STATUS_CMD_OK, L"abc?\n"}, {{L"string", L"match", L"a*\\?", L"abc?", 0}, STATUS_CMD_ERROR, L""},
{{L"string", L"match", L"?", L"", 0}, STATUS_CMD_ERROR, L""}, {{L"string", L"match", L"?", L"", 0}, STATUS_CMD_ERROR, L""},
{{L"string", L"match", L"?", L"ab", 0}, STATUS_CMD_ERROR, L""}, {{L"string", L"match", L"?", L"ab", 0}, STATUS_CMD_ERROR, L""},
@ -4428,24 +4429,27 @@ static void test_illegal_command_exit_code() {
}; };
const command_result_tuple_t tests[] = { const command_result_tuple_t tests[] = {
{L"echo -n", STATUS_CMD_OK}, {L"pwd", STATUS_CMD_OK}, {L"echo -n", STATUS_CMD_OK},
// a `)` without a matching `(` is now a tokenizer error, and cannot be executed even as an illegal command {L"pwd", STATUS_CMD_OK},
// a `)` without a matching `(` is now a tokenizer error, and cannot be executed even as an
// illegal command
// {L")", STATUS_ILLEGAL_CMD}, {L") ", STATUS_ILLEGAL_CMD}, {L") ", STATUS_ILLEGAL_CMD} // {L")", STATUS_ILLEGAL_CMD}, {L") ", STATUS_ILLEGAL_CMD}, {L") ", STATUS_ILLEGAL_CMD}
{L"*", STATUS_ILLEGAL_CMD}, {L"**", STATUS_ILLEGAL_CMD}, {L"*", STATUS_ILLEGAL_CMD},
{L"?", STATUS_ILLEGAL_CMD}, {L"abc?def", STATUS_ILLEGAL_CMD}, {L"**", STATUS_ILLEGAL_CMD},
{L"?", STATUS_CMD_UNKNOWN},
{L"abc?def", STATUS_CMD_UNKNOWN},
}; };
int res = 0; int res = 0;
const io_chain_t empty_ios; const io_chain_t empty_ios;
parser_t &parser = parser_t::principal_parser(); parser_t &parser = parser_t::principal_parser();
size_t i = 0; for (const auto &test : tests) {
for (i = 0; i < sizeof tests / sizeof *tests; i++) { res = parser.eval(test.txt, empty_ios, TOP);
res = parser.eval(tests[i].txt, empty_ios, TOP);
int exit_status = res ? STATUS_CMD_UNKNOWN : proc_get_last_status(); int exit_status = res ? STATUS_CMD_UNKNOWN : proc_get_last_status();
if (exit_status != tests[i].result) { if (exit_status != test.result) {
err(L"command '%ls': expected exit code %d , got %d", tests[i].txt, tests[i].result, err(L"command '%ls': expected exit code %d , got %d", test.txt, test.result,
exit_status); exit_status);
} }
} }

View file

@ -125,7 +125,6 @@ bool is_potential_path(const wcstring &potential_path_fragment, const wcstring_l
case BRACE_BEGIN: case BRACE_BEGIN:
case BRACE_END: case BRACE_END:
case BRACE_SEP: case BRACE_SEP:
case ANY_CHAR:
case ANY_STRING: case ANY_STRING:
case ANY_STRING_RECURSIVE: { case ANY_STRING_RECURSIVE: {
has_magic = 1; has_magic = 1;
@ -549,7 +548,6 @@ static void color_argument_internal(const wcstring &buffstr,
break; break;
} }
case L'*': case L'*':
case L'?':
case L'(': case L'(':
case L')': { case L')': {
colors[in_pos] = highlight_spec_operator; colors[in_pos] = highlight_spec_operator;

View file

@ -423,9 +423,7 @@ wcstring parse_util_unescape_wildcards(const wcstring &str) {
for (size_t i = 0; cs[i] != L'\0'; i++) { for (size_t i = 0; cs[i] != L'\0'; i++) {
if (cs[i] == L'*') { if (cs[i] == L'*') {
result.push_back(ANY_STRING); result.push_back(ANY_STRING);
} else if (cs[i] == L'?') { } else if (cs[i] == L'\\' && cs[i + 1] == L'*') {
result.push_back(ANY_CHAR);
} else if (cs[i] == L'\\' && (cs[i + 1] == L'*' || cs[i + 1] == L'?')) {
result.push_back(cs[i + 1]); result.push_back(cs[i + 1]);
i += 1; i += 1;
} else if (cs[i] == L'\\' && cs[i + 1] == L'\\') { } else if (cs[i] == L'\\' && cs[i + 1] == L'\\') {
@ -892,9 +890,7 @@ void parse_util_expand_variable_error(const wcstring &token, size_t global_token
default: { default: {
wchar_t token_stop_char = char_after_dollar; wchar_t token_stop_char = char_after_dollar;
// Unescape (see issue #50). // Unescape (see issue #50).
if (token_stop_char == ANY_CHAR) if (token_stop_char == ANY_STRING || token_stop_char == ANY_STRING_RECURSIVE)
token_stop_char = L'?';
else if (token_stop_char == ANY_STRING || token_stop_char == ANY_STRING_RECURSIVE)
token_stop_char = L'*'; token_stop_char = L'*';
// Determine which error message to use. The format string may not consume all the // Determine which error message to use. The format string may not consume all the

View file

@ -51,7 +51,7 @@
/// Finds an internal (ANY_STRING, etc.) style wildcard, or wcstring::npos. /// Finds an internal (ANY_STRING, etc.) style wildcard, or wcstring::npos.
static size_t wildcard_find(const wchar_t *wc) { static size_t wildcard_find(const wchar_t *wc) {
for (size_t i = 0; wc[i] != L'\0'; i++) { for (size_t i = 0; wc[i] != L'\0'; i++) {
if (wc[i] == ANY_CHAR || wc[i] == ANY_STRING || wc[i] == ANY_STRING_RECURSIVE) { if (wc[i] == ANY_STRING || wc[i] == ANY_STRING_RECURSIVE) {
return i; return i;
} }
} }
@ -64,13 +64,12 @@ static bool wildcard_has_impl(const wchar_t *str, size_t len, bool internal) {
const wchar_t *end = str + len; const wchar_t *end = str + len;
if (internal) { if (internal) {
for (; str < end; str++) { for (; str < end; str++) {
if ((*str == ANY_CHAR) || (*str == ANY_STRING) || (*str == ANY_STRING_RECURSIVE)) if (*str == ANY_STRING || *str == ANY_STRING_RECURSIVE) return true;
return true;
} }
} else { } else {
wchar_t prev = 0; wchar_t prev = 0;
for (; str < end; str++) { for (; str < end; str++) {
if (((*str == L'*') || (*str == L'?')) && (prev != L'\\')) return true; if (*str == L'*' && prev != L'\\') return true;
prev = *str; prev = *str;
} }
} }
@ -128,13 +127,6 @@ static enum fuzzy_match_type_t wildcard_match_internal(const wchar_t *str, const
restart_is_out_of_str = (*str_x == 0); restart_is_out_of_str = (*str_x == 0);
wc_x++; wc_x++;
continue; continue;
} else if (*wc_x == ANY_CHAR && *str_x != 0) {
if (is_first && *str_x == L'.') {
return fuzzy_match_none;
}
wc_x++;
str_x++;
continue;
} else if (*str_x != 0 && *str_x == *wc_x) { // ordinary character } else if (*str_x != 0 && *str_x == *wc_x) { // ordinary character
wc_x++; wc_x++;
str_x++; str_x++;
@ -212,7 +204,7 @@ static bool wildcard_complete_internal(const wchar_t *str, const wchar_t *wc,
return false; return false;
} }
// Locate the next wildcard character position, e.g. ANY_CHAR or ANY_STRING. // Locate the next wildcard character position, e.g. ANY_STRING.
const size_t next_wc_char_pos = wildcard_find(wc); const size_t next_wc_char_pos = wildcard_find(wc);
// Maybe we have no more wildcards at all. This includes the empty string. // Maybe we have no more wildcards at all. This includes the empty string.
@ -265,12 +257,6 @@ static bool wildcard_complete_internal(const wchar_t *str, const wchar_t *wc,
// Our first character is a wildcard. // Our first character is a wildcard.
assert(next_wc_char_pos == 0); assert(next_wc_char_pos == 0);
switch (wc[0]) { switch (wc[0]) {
case ANY_CHAR: {
if (str[0] == L'\0') {
return false;
}
return wildcard_complete_internal(str + 1, wc + 1, params, flags, out);
}
case ANY_STRING: { case ANY_STRING: {
// Hackish. If this is the last character of the wildcard, then just complete with // Hackish. If this is the last character of the wildcard, then just complete with
// the empty string. This fixes cases like "f*<tab>" -> "f*o". // the empty string. This fixes cases like "f*<tab>" -> "f*o".
@ -789,7 +775,7 @@ void wildcard_expander_t::expand_last_segment(const wcstring &base_dir, DIR *bas
/// ///
/// Args: /// Args:
/// base_dir: the "working directory" against which the wildcard is to be resolved /// base_dir: the "working directory" against which the wildcard is to be resolved
/// wc: the wildcard string itself, e.g. foo*bar/baz (where * is acutally ANY_CHAR) /// wc: the wildcard string itself, e.g. foo*bar/baz (where * is acutally ANY_STRING)
/// prefix: the string that should be prepended for completions that replace their token. /// prefix: the string that should be prepended for completions that replace their token.
// This is usually the same thing as the original wildcard, but for fuzzy matching, we // This is usually the same thing as the original wildcard, but for fuzzy matching, we
// expand intermediate segments. effective_prefix is always either empty, or ends with a slash // expand intermediate segments. effective_prefix is always either empty, or ends with a slash
@ -810,7 +796,7 @@ void wildcard_expander_t::expand(const wcstring &base_dir, const wchar_t *wc,
const size_t wc_segment_len = next_slash ? next_slash - wc : wc_len; const size_t wc_segment_len = next_slash ? next_slash - wc : wc_len;
const wcstring wc_segment = wcstring(wc, wc_segment_len); const wcstring wc_segment = wcstring(wc, wc_segment_len);
const bool segment_has_wildcards = const bool segment_has_wildcards =
wildcard_has(wc_segment, true /* internal, i.e. look for ANY_CHAR instead of ? */); wildcard_has(wc_segment, true /* internal, i.e. look for ANY_STRING instead of * */);
const wchar_t *const wc_remainder = next_slash ? next_slash + 1 : NULL; const wchar_t *const wc_remainder = next_slash ? next_slash + 1 : NULL;
if (wc_segment.empty()) { if (wc_segment.empty()) {

View file

@ -11,10 +11,8 @@
// Enumeration of all wildcard types. // Enumeration of all wildcard types.
enum { enum {
/// Character representing any character except '/' (slash).
ANY_CHAR = WILDCARD_RESERVED_BASE,
/// Character representing any character string not containing '/' (slash). /// Character representing any character string not containing '/' (slash).
ANY_STRING, ANY_STRING = WILDCARD_RESERVED_BASE,
/// Character representing any character string. /// Character representing any character string.
ANY_STRING_RECURSIVE, ANY_STRING_RECURSIVE,
/// This is a special psuedo-char that is not used other than to mark the /// This is a special psuedo-char that is not used other than to mark the

View file

@ -117,16 +117,16 @@
# string unescape --style=var (string escape --style=var -- -) # string unescape --style=var (string escape --style=var -- -)
#################### ####################
# string match "?" a # string match "*" a
#################### ####################
# string match "a*b" axxb # string match "a*b" axxb
#################### ####################
# string match -i "a??B" Axxb # string match -i "a**B" Axxb
#################### ####################
# echo "ok?" | string match "*\?" # echo "ok?" | string match "*?"
#################### ####################
# string match -r "cat|dog|fish" "nice dog" # string match -r "cat|dog|fish" "nice dog"
@ -190,7 +190,7 @@ string invalidarg; and echo "unexpected exit 0"
# string match -r -v "[dcantg].*" dog can cat diz # string match -r -v "[dcantg].*" dog can cat diz
#################### ####################
# string match -v "???" dog can cat diz # string match -v "*" dog can cat diz
#################### ####################
# string match -rvn a bbb # string match -rvn a bbb

View file

@ -124,17 +124,17 @@ string unescape --style=var -- (string escape --style=var -- -)
# The following tests verify that we can correctly match strings. # The following tests verify that we can correctly match strings.
logmsg 'string match "?" a' logmsg 'string match "*" a'
string match "?" a string match "*" a
logmsg 'string match "a*b" axxb' logmsg 'string match "a*b" axxb'
string match "a*b" axxb string match "a*b" axxb
logmsg 'string match -i "a??B" Axxb' logmsg 'string match -i "a**B" Axxb'
string match -i "a??B" Axxb string match -i "a**B" Axxb
logmsg 'echo "ok?" | string match "*\?"' logmsg 'echo "ok?" | string match "*?"'
echo "ok?" | string match "*\?" echo "ok?" | string match "*?"
logmsg 'string match -r "cat|dog|fish" "nice dog"' logmsg 'string match -r "cat|dog|fish" "nice dog"'
string match -r "cat|dog|fish" "nice dog" string match -r "cat|dog|fish" "nice dog"
@ -199,8 +199,8 @@ string length; or echo "missing argument returns 1"
logmsg 'string match -r -v "[dcantg].*" dog can cat diz' logmsg 'string match -r -v "[dcantg].*" dog can cat diz'
string match -r -v "[dcantg].*" dog can cat diz; or echo "no regexp invert match" string match -r -v "[dcantg].*" dog can cat diz; or echo "no regexp invert match"
logmsg 'string match -v "???" dog can cat diz' logmsg 'string match -v "*" dog can cat diz'
string match -v "???" dog can cat diz; or echo "no glob invert match" string match -v "*" dog can cat diz; or echo "no glob invert match"
logmsg 'string match -rvn a bbb' logmsg 'string match -rvn a bbb'
string match -rvn a bbb; or echo "exit 1" string match -rvn a bbb; or echo "exit 1"

View file

@ -170,7 +170,7 @@ _a_b_c_
- -
#################### ####################
# string match "?" a # string match "*" a
a a
#################### ####################
@ -178,11 +178,11 @@ a
axxb axxb
#################### ####################
# string match -i "a??B" Axxb # string match -i "a**B" Axxb
Axxb Axxb
#################### ####################
# echo "ok?" | string match "*\?" # echo "ok?" | string match "*?"
ok? ok?
#################### ####################
@ -272,7 +272,7 @@ missing argument returns 1
no regexp invert match no regexp invert match
#################### ####################
# string match -v "???" dog can cat diz # string match -v "*" dog can cat diz
no glob invert match no glob invert match
#################### ####################

View file

@ -65,7 +65,7 @@ for i in Test for continue break and switch builtins problems;
switch $i switch $i
case Test case Test
printf "%s " $i printf "%s " $i
case "f??" case "for"
printf "%s " 3b printf "%s " 3b
case "c*" case "c*"
echo pass echo pass

View file

@ -20,7 +20,7 @@ end
switch $smurf switch $smurf
case cyan magenta yellow case cyan magenta yellow
echo Test 3 fail echo Test 3 fail
case "?????" case "*"
echo Test 3 pass echo Test 3 pass
end end