@@ -929,6 +929,33 @@ def precmd(self, statement):
929929 """
930930 return statement
931931
932+ def parseline (self , line ):
933+ """Parse the line into a command name and a string containing the arguments.
934+
935+ Used for command tab completion. Returns a tuple containing (command, args, line).
936+ 'command' and 'args' may be None if the line couldn't be parsed.
937+
938+ :param line: str - line read by readline
939+ :return: (str, str, str) - tuple containing (command, args, line)
940+ """
941+ line = line .strip ()
942+
943+ if not line :
944+ # Deal with empty line or all whitespace line
945+ return None , None , line
946+
947+ # Expand command shortcuts to the full command name
948+ for (shortcut , expansion ) in self .shortcuts :
949+ if line .startswith (shortcut ):
950+ line = line .replace (shortcut , expansion + ' ' , 1 )
951+ break
952+
953+ i , n = 0 , len (line )
954+ while i < n and line [i ] in self .identchars :
955+ i = i + 1
956+ command , arg = line [:i ], line [i :].strip ()
957+ return command , arg , line
958+
932959 def onecmd_plus_hooks (self , line ):
933960 """Top-level function called by cmdloop() to handle parsing a line and running the command and all of its hooks.
934961
@@ -1327,48 +1354,180 @@ def help_shell(self):
13271354 Usage: shell cmd"""
13281355 self .stdout .write ("{}\n " .format (help_str ))
13291356
1330- @staticmethod
1331- def path_complete (line ):
1357+ def path_complete (self , text , line , begidx , endidx , dir_exe_only = False ):
13321358 """Method called to complete an input line by local file system path completion.
13331359
1360+ :param text: str - the string prefix we are attempting to match (all returned matches must begin with it)
13341361 :param line: str - the current input line with leading whitespace removed
1362+ :param begidx: int - the beginning indexe of the prefix text
1363+ :param endidx: int - the ending index of the prefix text
1364+ :param dir_exe_only: bool - only return directories and executables, not non-executable files
13351365 :return: List[str] - a list of possible tab completions
13361366 """
1337- path = line .split ()[- 1 ]
1338- if not path :
1339- path = '.'
1367+ # Deal with cases like load command and @ key when path completion is immediately after a shortcut
1368+ for (shortcut , expansion ) in self .shortcuts :
1369+ if line .startswith (shortcut ):
1370+ # If the next character after the shortcut isn't a space, then insert one and adjust indices
1371+ shortcut_len = len (shortcut )
1372+ if len (line ) == shortcut_len or line [shortcut_len ] != ' ' :
1373+ line = line .replace (shortcut , shortcut + ' ' , 1 )
1374+ begidx += 1
1375+ endidx += 1
1376+ break
1377+
1378+ # Determine if a trailing separator should be appended to directory completions
1379+ add_trailing_sep_if_dir = False
1380+ if endidx == len (line ) or (endidx < len (line ) and line [endidx ] != os .path .sep ):
1381+ add_trailing_sep_if_dir = True
1382+
1383+ add_sep_after_tilde = False
1384+ # If not path and no search text has been entered, then search in the CWD for *
1385+ if not text and line [begidx - 1 ] == ' ' and (begidx >= len (line ) or line [begidx ] == ' ' ):
1386+ search_str = os .path .join (os .getcwd (), '*' )
1387+ else :
1388+ # Parse out the path being searched
1389+ prev_space_index = line .rfind (' ' , 0 , begidx )
1390+ dirname = line [prev_space_index + 1 :begidx ]
1391+
1392+ # Purposely don't match any path containing wildcards - what we are doing is complicated enough!
1393+ wildcards = ['*' , '?' ]
1394+ for wildcard in wildcards :
1395+ if wildcard in dirname or wildcard in text :
1396+ return []
1397+
1398+ if not dirname :
1399+ dirname = os .getcwd ()
1400+ elif dirname == '~' :
1401+ # If tilde was used without separator, add a separator after the tilde in the completions
1402+ add_sep_after_tilde = True
13401403
1341- dirname , rest = os .path .split (path )
1342- real_dir = os .path .expanduser (dirname )
1404+ # Build the search string
1405+ search_str = os .path .join (dirname , text + '*' )
1406+
1407+ # Expand "~" to the real user directory
1408+ search_str = os .path .expanduser (search_str )
13431409
13441410 # Find all matching path completions
1345- path_completions = glob .glob (os .path .join (real_dir , rest ) + '*' )
1411+ path_completions = glob .glob (search_str )
1412+
1413+ # If we only want directories and executables, filter everything else out first
1414+ if dir_exe_only :
1415+ path_completions = [c for c in path_completions if os .path .isdir (c ) or os .access (c , os .X_OK )]
1416+
1417+ # Get the basename of the paths
1418+ completions = []
1419+ for c in path_completions :
1420+ basename = os .path .basename (c )
13461421
1347- # Strip off everything but the final part of the completion because that's the way readline works
1348- completions = [os .path .basename (c ) for c in path_completions ]
1422+ # Add a separator after directories if the next character isn't already a separator
1423+ if os .path .isdir (c ) and add_trailing_sep_if_dir :
1424+ basename += os .path .sep
13491425
1350- # If there is a single completion and it is a directory, add the final separator for convenience
1351- if len (completions ) == 1 and os .path .isdir (path_completions [0 ]):
1352- completions [0 ] += os .path .sep
1426+ completions .append (basename )
1427+
1428+ # If there is a single completion
1429+ if len (completions ) == 1 :
1430+ # If it is a file and we are at the end of the line, then add a space for convenience
1431+ if os .path .isfile (path_completions [0 ]) and endidx == len (line ):
1432+ completions [0 ] += ' '
1433+ # If tilde was expanded without a separator, prepend one
1434+ elif os .path .isdir (path_completions [0 ]) and add_sep_after_tilde :
1435+ completions [0 ] = os .path .sep + completions [0 ]
13531436
13541437 return completions
13551438
1439+ # Enable tab completion of paths for relevant commands
1440+ complete_edit = path_complete
1441+ complete_load = path_complete
1442+ complete_save = path_complete
1443+
1444+ @staticmethod
1445+ def _shell_command_complete (search_text ):
1446+ """Method called to complete an input line by environment PATH executable completion.
1447+
1448+ :param search_text: str - the search text used to find a shell command
1449+ :return: List[str] - a list of possible tab completions
1450+ """
1451+
1452+ # Purposely don't match any executable containing wildcards
1453+ wildcards = ['*' , '?' ]
1454+ for wildcard in wildcards :
1455+ if wildcard in search_text :
1456+ return []
1457+
1458+ # Get a list of every directory in the PATH environment variable and ignore symbolic links
1459+ paths = [p for p in os .getenv ('PATH' ).split (':' ) if not os .path .islink (p )]
1460+
1461+ # Find every executable file in the PATH that matches the pattern
1462+ exes = []
1463+ for path in paths :
1464+ full_path = os .path .join (path , search_text )
1465+ matches = [f for f in glob .glob (full_path + '*' ) if os .path .isfile (f ) and os .access (f , os .X_OK )]
1466+
1467+ for match in matches :
1468+ exes .append (os .path .basename (match ))
1469+
1470+ # If there is a single completion, then add a space at the end for convenience since
1471+ # this will be printed to the command line the user is typing
1472+ if len (exes ) == 1 :
1473+ exes [0 ] += ' '
1474+
1475+ return exes
1476+
13561477 # noinspection PyUnusedLocal
13571478 def complete_shell (self , text , line , begidx , endidx ):
1358- """Handles tab completion of local file system paths.
1479+ """Handles tab completion of executable commands and local file system paths.
13591480
1360- :param text: str - the string prefix we are attempting to match (all returned matches must begin with it)
1481+ :param text: str - the string prefix we are attempting to match (all returned matches must begin with it)
13611482 :param line: str - the current input line with leading whitespace removed
1362- :param begidx: str - the beginning indexe of the prefix text
1363- :param endidx: str - the ending index of the prefix text
1483+ :param begidx: int - the beginning index of the prefix text
1484+ :param endidx: int - the ending index of the prefix text
13641485 :return: List[str] - a list of possible tab completions
13651486 """
1366- return self .path_complete (line )
13671487
1368- # Enable tab completion of paths for other commands in an identical fashion
1369- complete_edit = complete_shell
1370- complete_load = complete_shell
1371- complete_save = complete_shell
1488+ # First we strip off the shell command or shortcut key
1489+ if line .startswith ('!' ):
1490+ stripped_line = line .lstrip ('!' )
1491+ initial_length = len ('!' )
1492+ else :
1493+ stripped_line = line [len ('shell' ):]
1494+ initial_length = len ('shell' )
1495+
1496+ line_parts = stripped_line .split ()
1497+
1498+ # Don't tab complete anything if user only typed shell or !
1499+ if not line_parts :
1500+ return []
1501+
1502+ # Find the start index of the first thing after the shell or !
1503+ cmd_start = line .find (line_parts [0 ], initial_length )
1504+ cmd_end = cmd_start + len (line_parts [0 ])
1505+
1506+ # Check if we are in the command token
1507+ if cmd_start <= begidx <= cmd_end :
1508+
1509+ # See if text is part of a path
1510+ possible_path = line [cmd_start :begidx ]
1511+
1512+ # There is nothing to search
1513+ if len (possible_path ) == 0 and not text :
1514+ return []
1515+
1516+ if os .path .sep not in possible_path :
1517+ # The text before the search text is not a directory path.
1518+ # It is OK to try shell command completion.
1519+ command_completions = self ._shell_command_complete (text )
1520+
1521+ if command_completions :
1522+ return command_completions
1523+
1524+ # If we have no results, try path completion
1525+ return self .path_complete (text , line , begidx , endidx , dir_exe_only = True )
1526+
1527+ # Past command token
1528+ else :
1529+ # Do path completion
1530+ return self .path_complete (text , line , begidx , endidx )
13721531
13731532 def do_py (self , arg ):
13741533 """
0 commit comments