Hide keyboard shortcuts

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 

5 

6from gutools.tools import _call 

7from gutools.controllers.arguments import * 

8from cement import App, Controller, ex 

9 

10# ------------------------------------------------------------------- 

11# helpers for expand argument values 

12# ------------------------------------------------------------------- 

13def exp_file(args): 

14 partial_path = args[-1] 

15 

16 if '*' in partial_path: 

17 return args 

18 

19 partial_path += '*' 

20 return [f for f in glob.iglob(partial_path)] 

21 

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 

33 

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) 

40 

41 return cmdline 

42 

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 

58 

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) 

69 

70 return candidates 

71 

72 

73# ------------------------------------------------------------------- 

74# CompleterController 

75# ------------------------------------------------------------------- 

76# TODO: add more specific argument expansors 

77 

78 

79class CompletableController(Controller): 

80 

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" 

87 

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" 

92 

93 def _add_expander(self, command, attr, func): 

94 exp = self._command_argument_expander.setdefault(command, {}) 

95 exp[attr] = func 

96 

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') 

101 

102 func = self._command_argument_expander.get(command,{}).get(argument) 

103 if not func: 

104 func = self.default_argument_expander.get(argument, None) 

105 

106 if func: 

107 return _call(func, self=self, attr=args[-1]) 

108 

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]) 

118 

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}' 

129 

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 '' 

139 

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(' = ', '=') 

146 

147 return cmd 

148 

149class CompleterController(Controller): 

150 """Bash completed controller for cement. 

151 To use just include this code in your main cement App 

152 

153 from bash_completer import CompleterController 

154 

155 class MyApp(App): 

156 

157 class Meta: 

158 label = 'atlas' 

159 

160 handlers = [ 

161 Base, 

162 CompleterController, 

163 ..., 

164 ] 

165 

166 and in a your main application folder 

167 create and execute the script 

168 

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") 

174 

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 } 

182 

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)), 

194 

195 ] 

196 hide = False 

197 

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 """ 

207 

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 

212 

213 # we use cpos to be able to complete in the middle of a sentence 

214 

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] 

225 

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:] 

239 

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) 

254 

255 break # only do with last controller near cursor 

256 

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]: 

267 

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() 

273 

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) 

283 

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 [] 

300 

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 

306 

307 # candidates = ['1'] 

308 # print(*candidates) 

309 foo = 1 

310 for token in candidates: 

311 print(token, flush=True) 

312 

313 

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() 

320 

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 

332 

333 for parent, child, c in iterator(): 

334 self.parent[child] = parent, c 

335 

336 for child, (parent, c) in self.parent.items(): 

337 path = [child, ] 

338 while parent: 

339 path.append(parent) 

340 parent, _ = self.parent[parent] 

341 

342 # path.remove('base') 

343 path = '.'.join(reversed(path)) 

344 self.root[child] = path 

345 self.controller[path] = c 

346 self.reverse[path] = child 

347 

348 foo = 1