Skip to content

Commit 04c1f2e

Browse files
author
Todd Leonhardt
committed
Fixed tab completion of command and shell commands
We now more closely emulate how Bash does or does not append a space.
1 parent a9f7c77 commit 04c1f2e

File tree

2 files changed

+18
-12
lines changed

2 files changed

+18
-12
lines changed

CHANGES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ News
1313
* Added the ability to exclude commands from the help menu (**eof** included by default)
1414
* Redundant **list** command removed and features merged into **history** command
1515
* Added **pyscript** command which supports tab-completion and running Python scripts with arguments
16-
* Improved tab-completion of file system paths, particularly for paths containing a dash (**-**)
16+
* Improved tab-completion of file system paths, command names, and shell commands
17+
* Thanks to Kevin Van Brunt for all fo the help with debugging and testing this
1718
* Changed default value of USE_ARG_LIST to True - this affects the beavhior of all **@options** commands
1819
* **WARNING**: This breaks backwards compatibility, to restore backwards compatibility, add this to the
1920
**__init__()** method in your custom class derived from cmd2.Cmd:

cmd2.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
# Detect whether IPython is installed to determine if the built-in "ipy" command should be included
7575
ipython_available = True
7676
try:
77-
# noinspection PyUnresolvedReferences
77+
# noinspection PyUnresolvedReferences,PyPackageRequirements
7878
from IPython import embed
7979
except ImportError:
8080
ipython_available = False
@@ -712,6 +712,7 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, use_ipython=False
712712

713713
def _finalize_app_parameters(self):
714714
self.commentGrammars.ignore(pyparsing.quotedString).setParseAction(lambda x: '')
715+
# noinspection PyUnresolvedReferences
715716
self.shortcuts = sorted(self.shortcuts.items(), reverse=True)
716717

717718
def poutput(self, msg):
@@ -771,8 +772,8 @@ def completenames(self, text, line, begidx, endidx):
771772
# Call super class method. Need to do it this way for Python 2 and 3 compatibility
772773
cmd_completion = cmd.Cmd.completenames(self, text)
773774

774-
# If we are completing the initial command name and get exactly 1 result, add a space afterwards for convenience
775-
if begidx == 0 and len(cmd_completion) == 1:
775+
# If we are completing the initial command name and get exactly 1 result and are at end of line, add a space
776+
if begidx == 0 and len(cmd_completion) == 1 and endidx == len(line):
776777
cmd_completion[0] += ' '
777778

778779
return cmd_completion
@@ -1434,18 +1435,22 @@ def path_complete(self, text, line, begidx, endidx, dir_exe_only=False, dir_only
14341435
complete_load = path_complete
14351436
complete_save = path_complete
14361437

1438+
# noinspection PyUnusedLocal
14371439
@staticmethod
1438-
def _shell_command_complete(search_text):
1440+
def _shell_command_complete(text, line, begidx, endidx):
14391441
"""Method called to complete an input line by environment PATH executable completion.
14401442
1441-
:param search_text: str - the search text used to find a shell command
1443+
:param text: str - the string prefix we are attempting to match (all returned matches must begin with it)
1444+
:param line: str - the current input line with leading whitespace removed
1445+
:param begidx: int - the beginning index of the prefix text
1446+
:param endidx: int - the ending index of the prefix text
14421447
:return: List[str] - a list of possible tab completions
14431448
"""
14441449

14451450
# Purposely don't match any executable containing wildcards
14461451
wildcards = ['*', '?']
14471452
for wildcard in wildcards:
1448-
if wildcard in search_text:
1453+
if wildcard in text:
14491454
return []
14501455

14511456
# Get a list of every directory in the PATH environment variable and ignore symbolic links
@@ -1454,15 +1459,14 @@ def _shell_command_complete(search_text):
14541459
# Find every executable file in the PATH that matches the pattern
14551460
exes = []
14561461
for path in paths:
1457-
full_path = os.path.join(path, search_text)
1462+
full_path = os.path.join(path, text)
14581463
matches = [f for f in glob.glob(full_path + '*') if os.path.isfile(f) and os.access(f, os.X_OK)]
14591464

14601465
for match in matches:
14611466
exes.append(os.path.basename(match))
14621467

1463-
# If there is a single completion, then add a space at the end for convenience since
1464-
# this will be printed to the command line the user is typing
1465-
if len(exes) == 1:
1468+
# If there is a single completion and we are at end of the line, then add a space at the end for convenience
1469+
if len(exes) == 1 and endidx == len(line):
14661470
exes[0] += ' '
14671471

14681472
return exes
@@ -1509,7 +1513,7 @@ def complete_shell(self, text, line, begidx, endidx):
15091513
if os.path.sep not in possible_path and possible_path != '~':
15101514
# The text before the search text is not a directory path.
15111515
# It is OK to try shell command completion.
1512-
command_completions = self._shell_command_complete(text)
1516+
command_completions = self._shell_command_complete(text, line, begidx, endidx)
15131517

15141518
if command_completions:
15151519
return command_completions
@@ -1522,6 +1526,7 @@ def complete_shell(self, text, line, begidx, endidx):
15221526
# Do path completion
15231527
return self.path_complete(text, line, begidx, endidx)
15241528

1529+
# noinspection PyBroadException
15251530
def do_py(self, arg):
15261531
"""
15271532
py <command>: Executes a Python command.

0 commit comments

Comments
 (0)