Coverage for /home/agp/Documents/me/code/gutools/gutools/bash_completer.py : 0%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import os
2import re
3import glob
4import psutil
6from gutools.tools import _call
7from gutools.controllers.arguments import *
8from cement import App, Controller, ex
10# -------------------------------------------------------------------
11# helpers for expand argument values
12# -------------------------------------------------------------------
13def exp_file(args):
14 partial_path = args[-1]
16 if '*' in partial_path:
17 return args
19 partial_path += '*'
20 return [f for f in glob.iglob(partial_path)]
22def exp_pid(attr):
23 "Expand all matching running processes pid"
24 candidates = set([])
25 excluded = (os.getpid(), )
26 for pid in psutil.pids():
27 if pid in excluded:
28 continue
29 pid = str(pid)
30 if pid.startswith(attr):
31 candidates.add(pid)
32 return candidates
34remove_pattern = re.compile("[{}]".format(r''.join([r'\{}'.format(c) for c in '${}@#!-'])))
35def hasheable_cmdline(cmdline):
36 # join all the tokens ...
37 cmdline = ''.join(cmdline)
38 # and replace bash sensitive symbols
39 cmdline = remove_pattern.sub('', cmdline)
41 return cmdline
43def exp_cmdline(attr):
44 "Expand all matching running command line"
45 candidates = set([])
46 excluded = (os.getpid(), )
47 for pid in psutil.pids():
48 if pid in excluded:
49 continue
50 proc = psutil.Process(pid)
51 cmdline = proc.cmdline()
52 # create an alternative represetatoin of cmdline
53 # compatible with being a single string
54 alt_cmd = hasheable_cmdline(cmdline)
55 if attr in alt_cmd:# .startswith(attr):
56 candidates.add(alt_cmd)
57 return candidates
59def find_process_by_cmdline(alt_cmd):
60 candidates = set([])
61 for pid in psutil.pids():
62 proc = psutil.Process(pid)
63 cmdline = proc.cmdline()
64 # create an alternative represetatoin of cmdline
65 # compatible with being a single string
66 cmdline = hasheable_cmdline(cmdline)
67 if cmdline.startswith(alt_cmd):
68 candidates.add(proc)
70 return candidates
73# -------------------------------------------------------------------
74# CompleterController
75# -------------------------------------------------------------------
76# TODO: add more specific argument expansors
79class CompletableController(Controller):
81 default_argument_expander = {
82 # 'file': exp_file, # expand names from file system
83 'pid': exp_pid, # expand pid from running process in os
84 'cmdline': exp_cmdline, # expand cmdline from running process in os
85 }
86 "'static' functions that would be called for completion by default"
88 def __init__(self, *args, **kw):
89 super().__init__(*args, **kw)
90 self._command_argument_expander = dict()
91 "per commnad functions that override default_argument_expander"
93 def _add_expander(self, command, attr, func):
94 exp = self._command_argument_expander.setdefault(command, {})
95 exp[attr] = func
97 def guess_values(self, command, info, args):
98 """Try to guess argument values based on argument definition."""
99 # argument = info[0][-1].split('-')[-1]
100 argument = info[1].get('dest')
102 func = self._command_argument_expander.get(command,{}).get(argument)
103 if not func:
104 func = self.default_argument_expander.get(argument, None)
106 if func:
107 return _call(func, self=self, attr=args[-1])
109 def _get_completion_prefix(self, info, args):
110 # get the last token used by bash completion
111 # get the prefix that must be removed from responses
112 # to match what bash expect for completion
113 last = args[-1]
114 if last in ('=', ':'):
115 incomplete, last = list(args), ''
116 else:
117 incomplete = list(args[:-1])
119 prefix = list()
120 attrs = info[0]
121 while incomplete:
122 token = incomplete.pop()
123 if token in attrs:
124 break
125 prefix.append(token)
126 prefix.reverse()
127 prefix = ''.join(prefix)
128 return prefix, f'{prefix}{last}'
130 def _get_current_command(self, args):
131 """Get the current command in cmdline"""
132 exposed = self._get_exposed_commands()
133 incompleted = list(args)
134 while incompleted:
135 token = incompleted.pop()
136 if token in exposed:
137 return token
138 return ''
140def fix_command_line(cmdline):
141 # remove wrapped (")
142 cmd = cmdline.strip('"')
143 # # join BASH splitting in uri and assignations
144 # cmd = cmd.replace(' : ', ':')
145 # cmd = cmd.replace(' = ', '=')
147 return cmd
149class CompleterController(Controller):
150 """Bash completed controller for cement.
151 To use just include this code in your main cement App
153 from bash_completer import CompleterController
155 class MyApp(App):
157 class Meta:
158 label = 'atlas'
160 handlers = [
161 Base,
162 CompleterController,
163 ...,
164 ]
166 and in a your main application folder
167 create and execute the script
169 # install_bash_completer.rc
170 # execute source install_bash_completer.rc
171 _myapp_complete() {
172 # set your app name
173 local myapp=$(basename "$PWD")
175 COMPREPLY=()
176 local words=( "${COMP_WORDS[@]}" )
177 local word="${COMP_WORDS[COMP_CWORD]}"
178 words=("${words[@]:1}")
179 local completions="$($myapp completer --cmplt=\""${words[*]}"\")"
180 COMPREPLY=($(compgen -W "$completions" -- "$word"))
181 }
183 complete -F _myapp_complete atlas
184 """
185 class Meta:
186 label = 'completer'
187 stacked_on = 'base'
188 stacked_type = 'nested'
189 description = 'auto-completer: hidden command'
190 arguments = [
191 (['--cmplt'], dict(help='command list so far')),
192 (['--cword'], dict(help='word that is being completed', type=int)),
193 (['--cpos'], dict(help='cursor position in the line', type=int)),
195 ]
196 hide = False
198 def _default(self):
199 """
200 Returns completion candidates for bash complete command.
201 It would complete:
202 - nested controllers
203 - controller commands
204 - controller arguments names
205 - controller arguments values based on argument definition
206 """
208 self._build_controller_tree()
209 cmdline = fix_command_line(self.app.pargs.cmplt)
210 cword = self.app.pargs.cword
211 cpos = self.app.pargs.cpos
213 # we use cpos to be able to complete in the middle of a sentence
215 # Get all cmdline tokens
216 tokens = [cmd for cmd in cmdline.split(' ')]
217 # Get tokens until current cursor position
218 # capture trailing space as well
219 under_construction = [cmd for cmd in cmdline[:cpos].split(' ')]
220 under_construction[0] = 'base' # remove program name :)
221 # notmatch = [cmd for cmd in cmdline[cpos:].split(' ')][:-1]
222 last = tokens[cword]
223 # if not word:
224 # under_construction = under_construction[:-1]
226 used_arguments = list()
227 # get already completed controllers
228 controller_names = list()
229 arguments = list()
230 # get all used controllers and the final part that is building
231 # under last controller scope
232 for i in range(len(under_construction) - 1, -1, -1):
233 name = under_construction[i]
234 fqname = self.root.get(name)
235 if fqname and set(fqname.split('.')).issubset(under_construction):
236 controller_names.append(name)
237 if not arguments:
238 arguments = under_construction[i+1:]
240 controller_names = controller_names or ['base']
241 controllers = [self.parent[name][1]() for name in controller_names]
242 candidates = list()
243 if len(arguments) <= 1:
244 for c in controllers:
245 # just show the exposed commands that match with pattern
246 candidates.extend([cmd for cmd in c._get_exposed_commands() if \
247 cmd.startswith(last)])
248 # and nested controllers
249 parent = c.Meta.label
250 for name, (p, _) in self.parent.items():
251 if parent == p:
252 if name.startswith(last):
253 candidates.append(name)
255 break # only do with last controller near cursor
257 elif len(arguments) > 1:
258 # is a subcommand trying to complete options
259 command = arguments[0]
260 # last = arguments[-1]
261 # candidates.clear()
262 used_attr = set()
263 for c in controllers:
264 for meta in [cmd['arguments'] for cmd in \
265 c._collect_commands() \
266 if cmd['exposed'] and cmd['label'] == command]:
268 # check for param value completion
269 # of param name completion
270 # if there is a matched attr, then complete param
271 # else show matching param names
272 incomplete = set()
274 # find the closer to cursor parameter
275 best_idx, best_info = 0, None
276 for info in meta:
277 attrs, specs = info
278 if not specs.get('dest'): # is a key, not a positional args
279 continue
280 # check if flag as been already used in cmdline
281 if set(attrs).intersection(arguments):
282 used_attr.update(attrs)
284 for attr in attrs:
285 if attr.startswith(last):
286 incomplete.add(attr)
287 try:
288 idx = arguments.index(attr, best_idx)
289 best_idx, best_info = idx, info
290 except ValueError:
291 pass
292 # best points to the closer to end attr that is formed or 0
293 if last.startswith('-') or not best_info:
294 # try to complete attr names, excluding already used
295 incomplete.difference_update(used_attr)
296 candidates.extend(incomplete)
297 else:
298 # try to complete attr values
299 candidates = c.guess_values(command, best_info, under_construction) or []
301 # TODO: actually we can return here ...
302 break # just use the 1st active controller
303 else:
304 # there is nothing to do here
305 pass
307 # candidates = ['1']
308 # print(*candidates)
309 foo = 1
310 for token in candidates:
311 print(token, flush=True)
314 def _build_controller_tree(self):
315 """Build the controller tree"""
316 self.root = dict() # not used by now
317 self.reverse = dict() # not used by now
318 self.controller = dict()
319 self.parent = dict()
321 def iterator():
322 for c in self.app.handler.list('controller'):
323 m = c.Meta
324 # child = '' if m.label == 'base' else m.label
325 child = m.label
326 # if getattr(m, 'stacked_type', None) == 'nested':
327 # parent = getattr(m, 'stacked_on', None)
328 # else:
329 # parent = None
330 parent = getattr(m, 'stacked_on', None)
331 yield parent, child, c
333 for parent, child, c in iterator():
334 self.parent[child] = parent, c
336 for child, (parent, c) in self.parent.items():
337 path = [child, ]
338 while parent:
339 path.append(parent)
340 parent, _ = self.parent[parent]
342 # path.remove('base')
343 path = '.'.join(reversed(path))
344 self.root[child] = path
345 self.controller[path] = c
346 self.reverse[path] = child
348 foo = 1