Fixing bash completion bug when prefix started with '>', '<', or ':' (#4826)

* Fixing bash completion bug when prefix started with '>', '<', or ':'

* Fixing code formatting

* More code formatting fixes

* Fixing test

* Refactor to fix bug by improving completion parser

* Supporting '<' and '>>' in completion parser

* Support for all IO redirect tokens
This commit is contained in:
jbw3 2022-08-06 04:07:09 -05:00 committed by GitHub
parent aa03bc9013
commit f2ca59a291
Failed to generate hash of commit
5 changed files with 109 additions and 2 deletions

View file

@ -0,0 +1,24 @@
**Added:**
* <news item>
**Changed:**
* <news item>
**Deprecated:**
* <news item>
**Removed:**
* <news item>
**Fixed:**
* Fixed a bash completion bug when prefixing a file path with '<' or '>' (for redirecting stdin/stdout/stderr)
* Fixed a bash completion bug when completing a git branch name when deleting a remote branch (e.g. `git push origin :dev-branch`)
**Security:**
* <news item>

View file

@ -232,3 +232,38 @@ def test_equal_sign_arg(command_context, completions, lprefix, exp_append_space)
isinstance(comp, RichCompletion) and comp.append_space == exp_append_space
for comp in bash_completions
)
@pytest.fixture
def bash_completer(fake_process):
fake_process.register_subprocess(
command=["bash", fake_process.any()],
# completion for "git push origin :dev-b"
stdout=b"""\
complete -o bashdefault -o default -o nospace -F __git_wrap__git_main git
dev-branch
""",
)
return fake_process
# git push origin :dev-b<TAB> -> git push origin :dev-branch
def test_git_delete_remote_branch(bash_completer):
command_context = CommandContext(
args=(
CommandArg("git"),
CommandArg("push"),
CommandArg("origin"),
),
arg_index=3,
prefix=":dev-b",
)
bash_completions, bash_lprefix = complete_from_bash(
CompletionContext(command_context)
)
assert bash_completions == {"dev-branch"} and bash_lprefix == 5
assert all(
isinstance(comp, RichCompletion) and comp.append_space is False
for comp in bash_completions
)

View file

@ -127,6 +127,38 @@ COMMAND_EXAMPLES = (
suffix="b",
),
),
(
f"command >/dev/nul{X}",
CommandContext(
args=(CommandArg("command"), CommandArg(">", is_io_redir=True)),
arg_index=2,
prefix="/dev/nul",
),
),
(
f"command 2>/dev/nul{X}",
CommandContext(
args=(CommandArg("command"), CommandArg("2>", is_io_redir=True)),
arg_index=2,
prefix="/dev/nul",
),
),
(
f"command >>/dev/nul{X}",
CommandContext(
args=(CommandArg("command"), CommandArg(">>", is_io_redir=True)),
arg_index=2,
prefix="/dev/nul",
),
),
(
f"command </dev/nul{X}",
CommandContext(
args=(CommandArg("command"), CommandArg("<", is_io_redir=True)),
arg_index=2,
prefix="/dev/nul",
),
),
)
EMPTY_COMMAND_EXAMPLES = (

View file

@ -432,6 +432,10 @@ def bash_completions(
# to be incorrectly calculated, so it needs to be fixed here
if "=" in prefix and "=" not in commprefix:
strip_len = prefix.index("=") + 1
# Fix case where remote git branch is being deleted
# (e.g. 'git push origin :dev-branch')
elif ":" in prefix and ":" not in commprefix:
strip_len = prefix.index(":") + 1
return out, max(len(prefix) - strip_len, 0)

View file

@ -35,6 +35,8 @@ class CommandArg(NamedTuple):
"""The arg's opening quote (if it exists)"""
closing_quote: str = ""
"""The arg's closing quote (if it exists)"""
is_io_redir: bool = False
"""Whether the arg is IO redirection"""
@property
def raw_value(self):
@ -328,6 +330,13 @@ class CompletionContextParser:
"OR",
}
used_tokens |= multi_tokens
io_redir_tokens = {
"LT",
"GT",
"RSHIFT",
"IOREDIRECT",
}
used_tokens |= io_redir_tokens
artificial_tokens = {"ANY"}
ignored_tokens = {"INDENT", "DEDENT", "WS"}
@ -701,7 +710,8 @@ class CompletionContextParser:
arg = CompletionContextParser.try_parse_string_literal(raw_arg)
if arg is None:
arg = CommandArg(raw_arg)
is_io_redir = p.slice[1].type in self.io_redir_tokens
arg = CommandArg(raw_arg, is_io_redir=is_io_redir)
p[0] = Spanned(arg, span, cursor_context=relative_cursor)
@ -727,7 +737,9 @@ class CompletionContextParser:
joined_raw = f"{last_arg.value.raw_value}{in_between}{new_arg.value.raw_value}"
string_literal = self.try_parse_string_literal(joined_raw)
if string_literal is not None or not in_between:
is_redir = new_arg.value.is_io_redir or last_arg.value.is_io_redir
if string_literal is not None or (not in_between and not is_redir):
if string_literal is not None:
# we're appending to a partial string, e.g. `"a b`
arg = string_literal