@@ -929,6 +929,33 @@ def precmd(self, statement):
929
929
"""
930
930
return statement
931
931
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
+
932
959
def onecmd_plus_hooks (self , line ):
933
960
"""Top-level function called by cmdloop() to handle parsing a line and running the command and all of its hooks.
934
961
@@ -1327,48 +1354,180 @@ def help_shell(self):
1327
1354
Usage: shell cmd"""
1328
1355
self .stdout .write ("{}\n " .format (help_str ))
1329
1356
1330
- @staticmethod
1331
- def path_complete (line ):
1357
+ def path_complete (self , text , line , begidx , endidx , dir_exe_only = False ):
1332
1358
"""Method called to complete an input line by local file system path completion.
1333
1359
1360
+ :param text: str - the string prefix we are attempting to match (all returned matches must begin with it)
1334
1361
: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
1335
1365
:return: List[str] - a list of possible tab completions
1336
1366
"""
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
1340
1403
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 )
1343
1409
1344
1410
# 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 )
1346
1421
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
1349
1425
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 ]
1353
1436
1354
1437
return completions
1355
1438
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
+
1356
1477
# noinspection PyUnusedLocal
1357
1478
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.
1359
1480
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)
1361
1482
: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
1364
1485
:return: List[str] - a list of possible tab completions
1365
1486
"""
1366
- return self .path_complete (line )
1367
1487
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 )
1372
1531
1373
1532
def do_py (self , arg ):
1374
1533
"""
0 commit comments