Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/invoke/completion/complete.py: 20%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

65 statements  

1""" 

2Command-line completion mechanisms, executed by the core ``--complete`` flag. 

3""" 

4 

5from typing import List 

6import glob 

7import os 

8import re 

9import shlex 

10from typing import TYPE_CHECKING 

11 

12from ..exceptions import Exit, ParseError 

13from ..util import debug, task_name_sort_key 

14 

15if TYPE_CHECKING: 

16 from ..collection import Collection 

17 from ..parser import Parser, ParseResult, ParserContext 

18 

19 

20def complete( 

21 names: List[str], 

22 core: "ParseResult", 

23 initial_context: "ParserContext", 

24 collection: "Collection", 

25 parser: "Parser", 

26) -> Exit: 

27 # Strip out program name (scripts give us full command line) 

28 # TODO: this may not handle path/to/script though? 

29 invocation = re.sub(r"^({}) ".format("|".join(names)), "", core.remainder) 

30 debug("Completing for invocation: {!r}".format(invocation)) 

31 # Tokenize (shlex will have to do) 

32 tokens = shlex.split(invocation) 

33 # Handle flags (partial or otherwise) 

34 if tokens and tokens[-1].startswith("-"): 

35 tail = tokens[-1] 

36 debug("Invocation's tail {!r} is flag-like".format(tail)) 

37 # Gently parse invocation to obtain 'current' context. 

38 # Use last seen context in case of failure (required for 

39 # otherwise-invalid partial invocations being completed). 

40 

41 contexts: List[ParserContext] 

42 try: 

43 debug("Seeking context name in tokens: {!r}".format(tokens)) 

44 contexts = parser.parse_argv(tokens) 

45 except ParseError as e: 

46 msg = "Got parser error ({!r}), grabbing its last-seen context {!r}" # noqa 

47 debug(msg.format(e, e.context)) 

48 contexts = [e.context] if e.context is not None else [] 

49 # Fall back to core context if no context seen. 

50 debug("Parsed invocation, contexts: {!r}".format(contexts)) 

51 if not contexts or not contexts[-1]: 

52 context = initial_context 

53 else: 

54 context = contexts[-1] 

55 debug("Selected context: {!r}".format(context)) 

56 # Unknown flags (could be e.g. only partially typed out; could be 

57 # wholly invalid; doesn't matter) complete with flags. 

58 debug("Looking for {!r} in {!r}".format(tail, context.flags)) 

59 if tail not in context.flags: 

60 debug("Not found, completing with flag names") 

61 # Long flags - partial or just the dashes - complete w/ long flags 

62 if tail.startswith("--"): 

63 for name in filter( 

64 lambda x: x.startswith("--"), context.flag_names() 

65 ): 

66 print(name) 

67 # Just a dash, completes with all flags 

68 elif tail == "-": 

69 for name in context.flag_names(): 

70 print(name) 

71 # Otherwise, it's something entirely invalid (a shortflag not 

72 # recognized, or a java style flag like -foo) so return nothing 

73 # (the shell will still try completing with files, but that doesn't 

74 # hurt really.) 

75 else: 

76 pass 

77 # Known flags complete w/ nothing or tasks, depending 

78 else: 

79 # Flags expecting values: do nothing, to let default (usually 

80 # file) shell completion occur (which we actively want in this 

81 # case.) 

82 if context.flags[tail].takes_value: 

83 debug("Found, and it takes a value, so no completion") 

84 pass 

85 # Not taking values (eg bools): print task names 

86 else: 

87 debug("Found, takes no value, printing task names") 

88 print_task_names(collection) 

89 # If not a flag, is either task name or a flag value, so just complete 

90 # task names. 

91 else: 

92 debug("Last token isn't flag-like, just printing task names") 

93 print_task_names(collection) 

94 raise Exit 

95 

96 

97def print_task_names(collection: "Collection") -> None: 

98 for name in sorted(collection.task_names, key=task_name_sort_key): 

99 print(name) 

100 # Just stick aliases after the thing they're aliased to. Sorting isn't 

101 # so important that it's worth bending over backwards here. 

102 for alias in collection.task_names[name]: 

103 print(alias) 

104 

105 

106def print_completion_script(shell: str, names: List[str]) -> None: 

107 # Grab all .completion files in invoke/completion/. (These used to have no 

108 # suffix, but surprise, that's super fragile. 

109 completions = { 

110 os.path.splitext(os.path.basename(x))[0]: x 

111 for x in glob.glob( 

112 os.path.join( 

113 os.path.dirname(os.path.realpath(__file__)), "*.completion" 

114 ) 

115 ) 

116 } 

117 try: 

118 path = completions[shell] 

119 except KeyError: 

120 err = 'Completion for shell "{}" not supported (options are: {}).' 

121 raise ParseError(err.format(shell, ", ".join(sorted(completions)))) 

122 debug("Printing completion script from {}".format(path)) 

123 # Choose one arbitrary program name for script's own internal invocation 

124 # (also used to construct completion function names when necessary) 

125 binary = names[0] 

126 with open(path, "r") as script: 

127 print( 

128 script.read().format(binary=binary, spaced_names=" ".join(names)) 

129 )