Skip to content

Commit

Permalink
Fix autocompleter for files with spaces
Browse files Browse the repository at this point in the history
  • Loading branch information
igrek51 committed Jun 21, 2020
1 parent 0b5fa49 commit 10e024e
Show file tree
Hide file tree
Showing 8 changed files with 45 additions and 22 deletions.
11 changes: 9 additions & 2 deletions cliglue/autocomplete/autocomplete.py
Expand Up @@ -17,18 +17,25 @@ def bash_autocomplete(rules: List[CliRule], cmdline: str, word_idx: Optional[int

def find_matching_completions(cmdline, rules, word_idx: Optional[int]) -> List[str]:
extracted_cmdline = _extract_quotes(cmdline)
args: List[str] = extract_args(extracted_cmdline)
try:
args: List[str] = extract_args(extracted_cmdline)
except ValueError:
return []
current_word: str = get_current_word(args, word_idx)
available: List[str] = _find_available_completions(rules, args, current_word)
# convert '--param=value' proposals to 'value'
hyphen_param_matcher = re.compile(r'-(.+)=(.+)')
return [
hyphen_param_matcher.sub('\\2', c)
escape_spaces(hyphen_param_matcher.sub('\\2', c))
for c in available
if c.startswith(current_word)
]


def escape_spaces(name: str) -> str:
return name.replace(' ', '\\ ')


def extract_args(extracted_cmdline) -> List[str]:
args = shlex.split(extracted_cmdline)[1:]
# restore last whitespace
Expand Down
Expand Up @@ -28,8 +28,6 @@ def install_bash(app_name: str):
usr_bin_executable: str = f'/usr/bin/{app_name}'
if os.path.exists(usr_bin_executable) or os.path.islink(usr_bin_executable):
warn(f'file {usr_bin_executable} already exists - skipping.')
if not os.path.exists(usr_bin_executable):
warn(f'link {usr_bin_executable} is broken.')
else:
info(f'creating link: {usr_bin_executable} -> {app_path}')
shell(f'sudo ln -s {app_path} {usr_bin_executable}')
Expand All @@ -55,9 +53,10 @@ def install_autocomplete(app_name: Optional[str]):
shell(f"""cat << 'EOF' | sudo tee {completion_script_path}
#!/bin/bash
{function_name}() {{
COMPREPLY=( $({app_name} --autocomplete "${{COMP_LINE}}" ${{COMP_CWORD}}) )
IFS=$'\n'
COMPREPLY=($({app_name} --autocomplete "${{COMP_LINE}}" ${{COMP_CWORD}}))
}}
complete -F {function_name} {app_name}
complete -o filenames -F {function_name} {app_name}
EOF
""")
info(f'Autocompleter has been installed in {completion_script_path} for command "{app_name}". '
Expand Down
2 changes: 1 addition & 1 deletion cliglue/builder/builder.py
Expand Up @@ -2,7 +2,7 @@
from typing import Callable, List, Optional

from cliglue.autocomplete.autocomplete import bash_autocomplete
from cliglue.autocomplete.bash_install import install_bash, install_autocomplete
from cliglue.autocomplete.install import install_bash, install_autocomplete
from cliglue.help.help import print_version, print_help, print_usage
from cliglue.parser.error import CliSyntaxError, CliDefinitionError
from cliglue.parser.parser import Parser
Expand Down
4 changes: 1 addition & 3 deletions cliglue/completers/file.py
Expand Up @@ -8,9 +8,7 @@ def file_completer(current: Optional[str]):
for file in os.listdir(listdir):
filepath = f'{prefixdir}{file}'
if os.path.isdir(filepath):
# prevent from immediate resolving
names.append(f'{filepath}/')
names.append(f'{filepath}/ ')
names.append(f'{filepath}')
else:
names.append(filepath)
return sorted(names)
Expand Down
2 changes: 1 addition & 1 deletion cliglue/version.py
@@ -1 +1 @@
__version__ = "1.1.2"
__version__ = "1.1.3"
19 changes: 19 additions & 0 deletions tests/autocomplete/test_autocomplete.py
Expand Up @@ -216,6 +216,14 @@ def test_empty_choices():
assert mockio.stripped() == ''


def test_opened_quote():
with MockIO('--autocomplete', '"app \""') as mockio:
CliBuilder().has(
parameter('pos', choices=['val']),
).run()
assert mockio.stripped() == ''


def test_completing_word_in_the_middle():
with MockIO('--autocomplete', '"app ru in"', '1') as mockio:
CliBuilder().has(
Expand All @@ -234,3 +242,14 @@ def complete():
arguments('a', choices=complete),
).run()
assert mockio.stripped() == '42\n47'


def test_escaping_space_filenames():
def complete():
return ['file with spaces', 'file_without']

with MockIO('--autocomplete', '"app file"') as mockio:
CliBuilder(reraise_error=True).has(
arguments('a', choices=complete),
).run()
assert mockio.stripped() == 'file\\ with\\ spaces\nfile_without'
8 changes: 4 additions & 4 deletions tests/autocomplete/test_bash_install.py
Expand Up @@ -40,8 +40,8 @@ def test_autocomplete_install_explicit_name():
assert 'Autocompleter has been installed' in mockio.output()
assert os.path.exists(f'/etc/bash_completion.d/cliglue_{app_name}.sh')
completion_script = Path(f'/etc/bash_completion.d/cliglue_{app_name}.sh').read_text()
assert '''COMPREPLY=( $(cliglue_test_dupa123 --autocomplete "${COMP_LINE}" ${COMP_CWORD}) )''' in completion_script
assert '''complete -F _autocomplete_1437206436 cliglue_test_dupa123''' in completion_script
assert '''COMPREPLY=($(cliglue_test_dupa123 --autocomplete "${COMP_LINE}" ${COMP_CWORD}))''' in completion_script
assert '''complete -o filenames -F _autocomplete_1437206436 cliglue_test_dupa123''' in completion_script

shell(f'sudo rm -f /etc/bash_completion.d/cliglue_{app_name}.sh')
assert not os.path.exists(f'/etc/bash_completion.d/cliglue_{app_name}.sh')
Expand All @@ -55,8 +55,8 @@ def test_autocomplete_install_implicit_name():
assert 'Autocompleter has been installed' in mockio.output()
assert os.path.exists(f'/etc/bash_completion.d/cliglue_{app_name}.sh')
completion_script = Path(f'/etc/bash_completion.d/cliglue_{app_name}.sh').read_text()
assert '''COMPREPLY=( $(glue --autocomplete "${COMP_LINE}" ${COMP_CWORD}) )''' in completion_script
assert '''complete -F _autocomplete_70451630 glue''' in completion_script
assert '''COMPREPLY=($(glue --autocomplete "${COMP_LINE}" ${COMP_CWORD}))''' in completion_script
assert '''complete -o filenames -F _autocomplete_70451630 glue''' in completion_script

shell(f'sudo rm -f /etc/bash_completion.d/cliglue_{app_name}.sh')
assert not os.path.exists(f'/etc/bash_completion.d/cliglue_{app_name}.sh')
14 changes: 7 additions & 7 deletions tests/completers/test_file_completer.py
Expand Up @@ -12,10 +12,10 @@ def test_file_completer_empty():
).run()
proposals = mockio.stripped().splitlines()
assert '.gitignore' in proposals
assert 'tests/' in proposals
assert 'tests' in proposals

assert 'tests' not in proposals
assert 'tests/autocomplete/' not in proposals
assert 'tests/' not in proposals
assert 'tests/autocomplete' not in proposals
assert 'tests/__init__.py' not in proposals
assert '.' not in proposals
assert '..' not in proposals
Expand All @@ -35,7 +35,7 @@ def test_file_completer_dir1():
arguments('f', choices=file_completer),
).run()
proposals = set(mockio.stripped().splitlines())
assert proposals == {'tests/'}
assert proposals == {'tests'}


def test_file_completer_dir_content():
Expand All @@ -44,11 +44,11 @@ def test_file_completer_dir_content():
arguments('f', choices=file_completer),
).run()
proposals = mockio.stripped().splitlines()
assert 'tests/autocomplete/' in proposals
assert 'tests/autocomplete' in proposals
assert 'tests/__init__.py' in proposals

assert 'tests/' not in proposals
assert 'tests/autocomplete' not in proposals
assert 'tests/autocomplete/' not in proposals
assert '.gitignore' not in proposals
assert '.' not in proposals
assert '..' not in proposals
Expand All @@ -60,7 +60,7 @@ def test_file_completer_subdir():
arguments('f', choices=file_completer),
).run()
proposals = set(mockio.stripped().splitlines())
assert proposals == {'tests/autocomplete/'}
assert proposals == {'tests/autocomplete'}


def test_file_completer_subdir_content():
Expand Down

0 comments on commit 10e024e

Please sign in to comment.