|
82 | 82 | # Try to import readline, but allow failure for convenience in Windows unit testing
|
83 | 83 | # Note: If this actually fails, you should install readline on Linux or Mac or pyreadline on Windows
|
84 | 84 | try:
|
| 85 | + # noinspection PyUnresolvedReferences |
85 | 86 | import readline
|
86 | 87 | except ImportError:
|
87 | 88 | pass
|
@@ -479,6 +480,7 @@ class StubbornDict(dict):
|
479 | 480 |
|
480 | 481 | Create it with the stubbornDict(arg) factory function.
|
481 | 482 | """
|
| 483 | + # noinspection PyMethodOverriding |
482 | 484 | def update(self, arg):
|
483 | 485 | """Adds dictionary arg's key-values pairs in to dict
|
484 | 486 |
|
@@ -929,6 +931,33 @@ def precmd(self, statement):
|
929 | 931 | """
|
930 | 932 | return statement
|
931 | 933 |
|
| 934 | + def parseline(self, line): |
| 935 | + """Parse the line into a command name and a string containing the arguments. |
| 936 | +
|
| 937 | + Used for command tab completion. Returns a tuple containing (command, args, line). |
| 938 | + 'command' and 'args' may be None if the line couldn't be parsed. |
| 939 | +
|
| 940 | + :param line: str - line read by readline |
| 941 | + :return: (str, str, str) - tuple containing (command, args, line) |
| 942 | + """ |
| 943 | + line = line.strip() |
| 944 | + |
| 945 | + if not line: |
| 946 | + # Deal with empty line or all whitespace line |
| 947 | + return None, None, line |
| 948 | + |
| 949 | + # Expand command shortcuts to the full command name |
| 950 | + for (shortcut, expansion) in self.shortcuts: |
| 951 | + if line.startswith(shortcut): |
| 952 | + line = line.replace(shortcut, expansion + ' ', 1) |
| 953 | + break |
| 954 | + |
| 955 | + i, n = 0, len(line) |
| 956 | + while i < n and line[i] in self.identchars: |
| 957 | + i += 1 |
| 958 | + command, arg = line[:i], line[i:].strip() |
| 959 | + return command, arg, line |
| 960 | + |
932 | 961 | def onecmd_plus_hooks(self, line):
|
933 | 962 | """Top-level function called by cmdloop() to handle parsing a line and running the command and all of its hooks.
|
934 | 963 |
|
@@ -1327,48 +1356,183 @@ def help_shell(self):
|
1327 | 1356 | Usage: shell cmd"""
|
1328 | 1357 | self.stdout.write("{}\n".format(help_str))
|
1329 | 1358 |
|
1330 |
| - @staticmethod |
1331 |
| - def path_complete(line): |
| 1359 | + def path_complete(self, text, line, begidx, endidx, dir_exe_only=False, dir_only=False): |
1332 | 1360 | """Method called to complete an input line by local file system path completion.
|
1333 | 1361 |
|
| 1362 | + :param text: str - the string prefix we are attempting to match (all returned matches must begin with it) |
1334 | 1363 | :param line: str - the current input line with leading whitespace removed
|
| 1364 | + :param begidx: int - the beginning indexe of the prefix text |
| 1365 | + :param endidx: int - the ending index of the prefix text |
| 1366 | + :param dir_exe_only: bool - only return directories and executables, not non-executable files |
| 1367 | + :param dir_only: bool - only return directories |
1335 | 1368 | :return: List[str] - a list of possible tab completions
|
1336 | 1369 | """
|
1337 |
| - path = line.split()[-1] |
1338 |
| - if not path: |
1339 |
| - path = '.' |
| 1370 | + # Deal with cases like load command and @ key when path completion is immediately after a shortcut |
| 1371 | + for (shortcut, expansion) in self.shortcuts: |
| 1372 | + if line.startswith(shortcut): |
| 1373 | + # If the next character after the shortcut isn't a space, then insert one and adjust indices |
| 1374 | + shortcut_len = len(shortcut) |
| 1375 | + if len(line) == shortcut_len or line[shortcut_len] != ' ': |
| 1376 | + line = line.replace(shortcut, shortcut + ' ', 1) |
| 1377 | + begidx += 1 |
| 1378 | + endidx += 1 |
| 1379 | + break |
1340 | 1380 |
|
1341 |
| - dirname, rest = os.path.split(path) |
1342 |
| - real_dir = os.path.expanduser(dirname) |
| 1381 | + # Determine if a trailing separator should be appended to directory completions |
| 1382 | + add_trailing_sep_if_dir = False |
| 1383 | + if endidx == len(line) or (endidx < len(line) and line[endidx] != os.path.sep): |
| 1384 | + add_trailing_sep_if_dir = True |
1343 | 1385 |
|
1344 |
| - # Find all matching path completions |
1345 |
| - path_completions = glob.glob(os.path.join(real_dir, rest) + '*') |
| 1386 | + add_sep_after_tilde = False |
| 1387 | + # If no path and no search text has been entered, then search in the CWD for * |
| 1388 | + if not text and line[begidx - 1] == ' ' and (begidx >= len(line) or line[begidx] == ' '): |
| 1389 | + search_str = os.path.join(os.getcwd(), '*') |
| 1390 | + else: |
| 1391 | + # Parse out the path being searched |
| 1392 | + prev_space_index = line.rfind(' ', 0, begidx) |
| 1393 | + dirname = line[prev_space_index + 1:begidx] |
1346 | 1394 |
|
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] |
| 1395 | + # Purposely don't match any path containing wildcards - what we are doing is complicated enough! |
| 1396 | + wildcards = ['*', '?'] |
| 1397 | + for wildcard in wildcards: |
| 1398 | + if wildcard in dirname or wildcard in text: |
| 1399 | + return [] |
1349 | 1400 |
|
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 |
| 1401 | + if not dirname: |
| 1402 | + dirname = os.getcwd() |
| 1403 | + elif dirname == '~': |
| 1404 | + # If tilde was used without separator, add a separator after the tilde in the completions |
| 1405 | + add_sep_after_tilde = True |
| 1406 | + |
| 1407 | + # Build the search string |
| 1408 | + search_str = os.path.join(dirname, text + '*') |
| 1409 | + |
| 1410 | + # Expand "~" to the real user directory |
| 1411 | + search_str = os.path.expanduser(search_str) |
| 1412 | + |
| 1413 | + # Find all matching path completions |
| 1414 | + path_completions = glob.glob(search_str) |
| 1415 | + |
| 1416 | + # If we only want directories and executables, filter everything else out first |
| 1417 | + if dir_exe_only: |
| 1418 | + path_completions = [c for c in path_completions if os.path.isdir(c) or os.access(c, os.X_OK)] |
| 1419 | + elif dir_only: |
| 1420 | + path_completions = [c for c in path_completions if os.path.isdir(c)] |
| 1421 | + |
| 1422 | + # Get the basename of the paths |
| 1423 | + completions = [] |
| 1424 | + for c in path_completions: |
| 1425 | + basename = os.path.basename(c) |
| 1426 | + |
| 1427 | + # Add a separator after directories if the next character isn't already a separator |
| 1428 | + if os.path.isdir(c) and add_trailing_sep_if_dir: |
| 1429 | + basename += os.path.sep |
| 1430 | + |
| 1431 | + completions.append(basename) |
| 1432 | + |
| 1433 | + # If there is a single completion |
| 1434 | + if len(completions) == 1: |
| 1435 | + # If it is a file and we are at the end of the line, then add a space for convenience |
| 1436 | + if os.path.isfile(path_completions[0]) and endidx == len(line): |
| 1437 | + completions[0] += ' ' |
| 1438 | + # If tilde was expanded without a separator, prepend one |
| 1439 | + elif os.path.isdir(path_completions[0]) and add_sep_after_tilde: |
| 1440 | + completions[0] = os.path.sep + completions[0] |
1353 | 1441 |
|
1354 | 1442 | return completions
|
1355 | 1443 |
|
| 1444 | + # Enable tab completion of paths for relevant commands |
| 1445 | + complete_edit = path_complete |
| 1446 | + complete_load = path_complete |
| 1447 | + complete_save = path_complete |
| 1448 | + |
| 1449 | + @staticmethod |
| 1450 | + def _shell_command_complete(search_text): |
| 1451 | + """Method called to complete an input line by environment PATH executable completion. |
| 1452 | +
|
| 1453 | + :param search_text: str - the search text used to find a shell command |
| 1454 | + :return: List[str] - a list of possible tab completions |
| 1455 | + """ |
| 1456 | + |
| 1457 | + # Purposely don't match any executable containing wildcards |
| 1458 | + wildcards = ['*', '?'] |
| 1459 | + for wildcard in wildcards: |
| 1460 | + if wildcard in search_text: |
| 1461 | + return [] |
| 1462 | + |
| 1463 | + # Get a list of every directory in the PATH environment variable and ignore symbolic links |
| 1464 | + paths = [p for p in os.getenv('PATH').split(':') if not os.path.islink(p)] |
| 1465 | + |
| 1466 | + # Find every executable file in the PATH that matches the pattern |
| 1467 | + exes = [] |
| 1468 | + for path in paths: |
| 1469 | + full_path = os.path.join(path, search_text) |
| 1470 | + matches = [f for f in glob.glob(full_path + '*') if os.path.isfile(f) and os.access(f, os.X_OK)] |
| 1471 | + |
| 1472 | + for match in matches: |
| 1473 | + exes.append(os.path.basename(match)) |
| 1474 | + |
| 1475 | + # If there is a single completion, then add a space at the end for convenience since |
| 1476 | + # this will be printed to the command line the user is typing |
| 1477 | + if len(exes) == 1: |
| 1478 | + exes[0] += ' ' |
| 1479 | + |
| 1480 | + return exes |
| 1481 | + |
1356 | 1482 | # noinspection PyUnusedLocal
|
1357 | 1483 | def complete_shell(self, text, line, begidx, endidx):
|
1358 |
| - """Handles tab completion of local file system paths. |
| 1484 | + """Handles tab completion of executable commands and local file system paths. |
1359 | 1485 |
|
1360 | 1486 | :param text: str - the string prefix we are attempting to match (all returned matches must begin with it)
|
1361 | 1487 | :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 |
| 1488 | + :param begidx: int - the beginning index of the prefix text |
| 1489 | + :param endidx: int - the ending index of the prefix text |
1364 | 1490 | :return: List[str] - a list of possible tab completions
|
1365 | 1491 | """
|
1366 |
| - return self.path_complete(line) |
1367 | 1492 |
|
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 |
| 1493 | + # First we strip off the shell command or shortcut key |
| 1494 | + if line.startswith('!'): |
| 1495 | + stripped_line = line.lstrip('!') |
| 1496 | + initial_length = len('!') |
| 1497 | + else: |
| 1498 | + stripped_line = line[len('shell'):] |
| 1499 | + initial_length = len('shell') |
| 1500 | + |
| 1501 | + line_parts = stripped_line.split() |
| 1502 | + |
| 1503 | + # Don't tab complete anything if user only typed shell or ! |
| 1504 | + if not line_parts: |
| 1505 | + return [] |
| 1506 | + |
| 1507 | + # Find the start index of the first thing after the shell or ! |
| 1508 | + cmd_start = line.find(line_parts[0], initial_length) |
| 1509 | + cmd_end = cmd_start + len(line_parts[0]) |
| 1510 | + |
| 1511 | + # Check if we are in the command token |
| 1512 | + if cmd_start <= begidx <= cmd_end: |
| 1513 | + |
| 1514 | + # See if text is part of a path |
| 1515 | + possible_path = line[cmd_start:begidx] |
| 1516 | + |
| 1517 | + # There is nothing to search |
| 1518 | + if len(possible_path) == 0 and not text: |
| 1519 | + return [] |
| 1520 | + |
| 1521 | + if os.path.sep not in possible_path: |
| 1522 | + # The text before the search text is not a directory path. |
| 1523 | + # It is OK to try shell command completion. |
| 1524 | + command_completions = self._shell_command_complete(text) |
| 1525 | + |
| 1526 | + if command_completions: |
| 1527 | + return command_completions |
| 1528 | + |
| 1529 | + # If we have no results, try path completion |
| 1530 | + return self.path_complete(text, line, begidx, endidx, dir_exe_only=True) |
| 1531 | + |
| 1532 | + # Past command token |
| 1533 | + else: |
| 1534 | + # Do path completion |
| 1535 | + return self.path_complete(text, line, begidx, endidx) |
1372 | 1536 |
|
1373 | 1537 | def do_py(self, arg):
|
1374 | 1538 | """
|
|
0 commit comments