6 # - readline: http://bip.weizmann.ac.il/course/python/PyMOTW/PyMOTW/docs/readline/index.html
8 # - more commands (int, ip, ether, date, file, ...)
9 # - how to specify help for commands?
10 # - gerer les guillemets, les retours chariot, les espaces
11 # - to_str() -> repr() ?
12 # - ambiguous match as an exception?
13 # - use logging module
15 # - should display command help string if no help for token: how to do it
20 show interface <ethif>|all [detailed]
25 "interface": "interface"
28 show interface eth0 detailed
30 "interface": "interface",
32 "detailed": "detailed" }
36 filter [in <in-iface>]/[out <out-iface>]/[proto ipv4]/[proto ipv6] drop|pass
41 Les tokens nommés sont remplis dans un dictionnaire
42 Impossible d'avoir 2 fois le même nom, a verifier dans le createur de commande
43 -> faux. exemple au dessus.
45 Sinon comment gerer le ambiguous match?
52 Pour chaque completion possible (meme vide ?), il faut retourner:
53 - la chaine du token complet
54 - l'aide du token (ou l'objet)
55 - la commande ? (ou alors ça c'est géré dans la partie rdline)
61 legacy = blabla VERSION{1,2}
62 new = blabla wpa1/wpa2
64 legacy = blublu IFACE{1,*}
65 new = blublu <interfaces-list>
68 pour la définition d'un token, on pourrait avoir une fonction
69 display() qui permettrait d'afficher (<iface1> [<iface2> ...])
70 a la place de <interfaces-list>
81 _PYCOL_PKG_DIR = os.path.dirname(__file__)
84 def debug(*args, **kvargs):
86 print(*args, **kvargs)
88 class PycolMatch(dict):
92 return "<Match %s>"%(dict.__repr__(self))
93 def merge(self, match_res):
95 self[k] = match_res[k]
97 # XXX describe that it's a list of tuples
98 class PycolCompleteList(list):
99 def __init__(self, l = []):
100 if not isinstance(l, list):
102 list.__init__(self, l)
104 return "<CompleteList %s>"%(list.__repr__(self))
105 def merge(self, comp_res):
110 # XXX own(): keep the list but set token ref
112 class PycolComplete(object):
113 def __init__(self, token, cmd, terminal = True):
116 self.terminal = terminal
118 class PycolContext(list):
119 def __init__(self, l = []):
120 list.__init__(self, l)
121 def complete(self, tokens):
122 res = PycolCompleteList()
124 res2 = cmd.complete(tokens)
129 def __init__(self, key = None, cb = None, help_str = None):
131 assert(key == None or type(key) is str), "key is not a string: %s"%(key)
133 self.help_str = help_str
134 assert(help_str == None or type(help_str) is str), \
135 "help_str is not a string: %s"%(help_str)
136 self.default_help_str = "no help"
138 def match(self, tokens):
139 """return a match object
140 len(tokens) always >= 1
141 the last token is the one to complete (may be empty)
142 the first tokens are the ones to match
146 def complete(self, tokens):
147 # XXX return None when no completion
149 m = self.match(tokens)
150 if m != None or (len(tokens) == 1 and tokens[0] == ""):
151 return PycolCompleteList(PycolComplete(tokens[0], self))
152 return PycolCompleteList()
160 def test_match(self, in_buf):
161 tokens = shlex.split(in_buf, comments = True)
162 match_result = self.match(tokens)
163 debug("test match: %s %s"%(tokens, match_result))
164 if match_result == None:
169 # quick test for assert
170 def test_complete(self, in_buf):
171 complete_result = PycolCompleteList()
172 tokens = shlex.split(in_buf, comments = False)
173 # whitespace after the first token, it means we want to complete
175 debug("test_complete: %s", in_buf)
176 if len(tokens) == 0 or not in_buf.endswith(tokens[-1]):
178 complete_result = self.complete(tokens)
179 debug("test complete: %s %s"%(tokens, complete_result))
180 return list(map(lambda x:x.token, complete_result))
185 # XXX say that it can return a multiple lines string
187 if self.help_str == None:
188 return self.default_help_str
192 class CmdBuilder(CliCmd):
193 def __init__(self, expr, token_desc, key = None, cb = None, help_str = None):
194 super().__init__(key = key, cb = cb, help_str = help_str)
195 assert(isinstance(expr,str)), "expr must be a string"
196 assert(type(token_desc) is dict), "token_desc must be a dict"
197 self.token_desc = token_desc
198 parser = expparser.PycolExprParser()
199 tokens = parser.tokenize(expr)
200 expr = parser.parse(tokens)
202 self.token_desc_list = []
203 self.cmd = self.__expr2clicmd(expr, token_desc)
205 def match(self, tokens):
206 m = self.cmd.match(tokens)
207 if m != None and self.key != None:
208 m[self.key] = " ".join(tokens)
211 def complete(self, tokens):
212 return self.cmd.complete(tokens)
215 return self.cmd.to_expr()
218 return "<CmdBuilder(%s)>"%(self.cmd)
220 def __expr2clicmd(self, expr, token_desc):
222 for c in expr.children:
223 l.append(self.__expr2clicmd(c, token_desc))
226 assert(varname in token_desc)
227 cmd = token_desc[varname]
229 # keep the order of the variables for help displaying
230 self.token_desc_list.append(varname)
236 cmd = OptionalCliCmd(l[0])
238 cmd = BypassCliCmd(l[0])
239 #elif expr.op == " ": #XXX
243 if self.help_str == None:
244 help_str = self.default_help_str
246 help_str = self.help_str
247 for t in self.token_desc_list:
248 help_str += "\n %s: %s"%(t, self.token_desc[t].get_help())
252 return self.cmd.to_expr()
254 class TextCliCmd(CliCmd):
255 def __init__(self, text, key = None, cb = None, help_str = None):
256 super().__init__(key = key, cb = cb, help_str = help_str)
257 assert(not " " in text) # XXX find better?
259 debug("TextCliCmd(%s)"%text)
261 def match(self, tokens):
262 if tokens == [self.text]:
265 res[self.key] = self.text
269 def complete(self, tokens):
270 # more than one token as input, the string does not match
272 return PycolCompleteList()
273 # the beginning of the string does not match
274 if len(tokens) == 1 and not self.text.startswith(tokens[0]):
275 return PycolCompleteList()
277 return PycolCompleteList(PycolComplete(self.text, self))
283 return "<TextCliCmd(%s)>"%(self.text)
286 text_cmd = TextCliCmd("toto")
288 assert(text_cmd.test_match("") == False)
289 assert(text_cmd.test_match("tot") == False)
290 assert(text_cmd.test_match("toto toto") == False)
291 assert(text_cmd.test_match(" toto") == True)
292 assert(text_cmd.test_match("toto ") == True)
293 assert(text_cmd.test_match(" toto \n\n") == True)
294 assert(text_cmd.test_match("toto # coin") == True)
295 assert(text_cmd.test_match("toto") == True)
297 assert(text_cmd.test_complete("") == ["toto"])
298 assert(text_cmd.test_complete("to") == ["toto"])
299 assert(text_cmd.test_complete("tot") == ["toto"])
300 assert(text_cmd.test_complete(" tot") == ["toto"])
301 assert(text_cmd.test_complete("toto") == ["toto"])
302 assert(text_cmd.test_complete("d") == [])
303 assert(text_cmd.test_complete("toto ") == [])
304 assert(text_cmd.test_complete("toto#") == [])
306 class SeqCliCmd(CliCmd):
307 def __init__(self, cmdlist, key = None):
308 self.cmdlist = cmdlist
310 debug("SeqCliCmd(%s)"%cmdlist)
312 def match(self, tokens):
313 # only one command, try to match all tokens
314 if len(self.cmdlist) == 1:
315 m = self.cmdlist[0].match(tokens)
316 if m != None and self.key != None:
317 m[self.key] = " ".join(tokens)
319 # several commands, try to match the first command with 1 to N tokens,
320 # and do a recursive call for the following
323 for i in range(len(tokens)+1):
324 m = self.cmdlist[0].match(tokens[:i])
327 m2 = SeqCliCmd(self.cmdlist[1:]).match(tokens[i:])
335 ret[self.key] = " ".join(tokens[:i])
341 def complete(self, tokens):
342 debug("----------complete seq <%s>"%tokens)
344 match_tokens = tokens[:-1]
345 complete_token = tokens[-1]
347 # there is no match_token (only whitespaces), try to complete the first
349 if len(match_tokens) == 0:
350 return self.cmdlist[0].complete(tokens)
352 debug("match_tokens=<%s> complete_token=<%s>"%(match_tokens, complete_token))
353 res = PycolCompleteList()
355 # try to match match_tokens with 1 to N commands
356 for i in range(1, len(self.cmdlist)):
357 # if it does not match, continue
358 if SeqCliCmd(self.cmdlist[0:i]).match(match_tokens) == None:
361 # if it matches, try to complete the last token
362 res2 = self.cmdlist[i].complete([complete_token])
368 return " ".join([c.to_expr() for c in self.cmdlist])
371 return "<SeqCliCmd(%s)>"%(str(self.cmdlist))
373 seq_cmd = SeqCliCmd([TextCliCmd("toto"), TextCliCmd("titi"), TextCliCmd("tutu")])
374 assert(seq_cmd.test_match("") == False)
375 assert(seq_cmd.test_match("toto") == False)
376 assert(seq_cmd.test_match("titi") == False)
377 assert(seq_cmd.test_match("toto d") == False)
378 assert(seq_cmd.test_match("toto # titi tutu") == False)
379 assert(seq_cmd.test_match("toto titi tutu") == True)
380 assert(seq_cmd.test_match(" toto titi tutu") == True)
381 assert(seq_cmd.test_match("toto titi tutu #") == True)
382 assert(seq_cmd.test_match("toto titi tutu") == True)
384 assert(seq_cmd.test_complete("") == ["toto"])
385 assert(seq_cmd.test_complete("toto ") == ["titi"])
386 assert(seq_cmd.test_complete("toto t") == ["titi"])
387 assert(seq_cmd.test_complete("toto") == ["toto"])
388 assert(seq_cmd.test_complete("titi") == [])
389 assert(seq_cmd.test_complete("toto d") == [])
390 assert(seq_cmd.test_complete("toto # titi tutu") == [])
391 assert(seq_cmd.test_complete("toto titi tut") == ["tutu"])
392 assert(seq_cmd.test_complete(" toto titi tut") == ["tutu"])
393 assert(seq_cmd.test_complete("toto titi tutu #") == [])
394 assert(seq_cmd.test_complete("toto titi tutu") == ["tutu"])
397 class OrCliCmd(CliCmd):
398 def __init__(self, cmdlist, key = None):
399 self.cmdlist = cmdlist
401 debug("OrCliCmd(%s)"%cmdlist)
403 def match(self, tokens):
404 # try to match all commands
405 for cmd in self.cmdlist:
406 ret = cmd.match(tokens)
410 ret[self.key] = " ".join(tokens)
414 def complete(self, tokens):
415 debug("----------complete or <%s>"%tokens)
417 res = PycolCompleteList()
418 for cmd in self.cmdlist:
419 res2 = cmd.complete(tokens)
425 return "|".join([c.to_expr() for c in self.cmdlist])
428 return "<OrCliCmd(%s)>"%(str(self.cmdlist))
430 or_cmd = OrCliCmd([TextCliCmd("toto"), TextCliCmd("titi"), TextCliCmd("tutu")])
431 assert(or_cmd.test_match("") == False)
432 assert(or_cmd.test_match("toto") == True)
433 assert(or_cmd.test_match("titi") == True)
434 assert(or_cmd.test_match("tutu") == True)
435 assert(or_cmd.test_match(" toto # titi tutu") == True)
436 assert(or_cmd.test_match("toto titi tutu") == False)
437 assert(or_cmd.test_match(" toto t") == False)
438 assert(or_cmd.test_match("toto d#") == False)
440 assert(or_cmd.test_complete("") == ["toto", "titi", "tutu"])
441 assert(or_cmd.test_complete("t") == ["toto", "titi", "tutu"])
442 assert(or_cmd.test_complete("to") == ["toto"])
443 assert(or_cmd.test_complete(" tot") == ["toto"])
444 assert(or_cmd.test_complete(" titi") == ["titi"])
445 assert(or_cmd.test_complete(" titi to") == [])
446 assert(or_cmd.test_complete("titid") == [])
447 assert(or_cmd.test_complete(" ti#") == [])
451 class OptionalCliCmd(CliCmd):
452 def __init__(self, cmd, key = None):
456 def match(self, tokens):
457 # match an empty buffer
463 # else, try to match sub command
464 ret = self.cmd.match(tokens)
465 if ret != None and self.key != None:
466 ret[self.key] = " ".join(tokens)
469 def complete(self, tokens):
470 debug("----------complete optional <%s>"%tokens)
471 return self.cmd.complete(tokens)
474 return "[" + self.cmd.to_expr() + "]"
477 return "<OptionalCliCmd(%s)>"%(self.cmd)
479 opt_cmd = OptionalCliCmd(TextCliCmd("toto"))
480 assert(opt_cmd.test_match("") == True)
481 assert(opt_cmd.test_match("toto") == True)
482 assert(opt_cmd.test_match(" toto ") == True)
483 assert(opt_cmd.test_match(" toto # tutu") == True)
484 assert(opt_cmd.test_match("titi") == False)
485 assert(opt_cmd.test_match("toto titi") == False)
486 assert(opt_cmd.test_match(" toto t") == False)
487 assert(opt_cmd.test_match("toto d#") == False)
489 assert(opt_cmd.test_complete("") == ["toto"])
490 assert(opt_cmd.test_complete("t") == ["toto"])
491 assert(opt_cmd.test_complete("to") == ["toto"])
492 assert(opt_cmd.test_complete(" tot") == ["toto"])
493 assert(opt_cmd.test_complete(" ") == ["toto"])
494 assert(opt_cmd.test_complete(" titi to") == [])
495 assert(opt_cmd.test_complete("titid") == [])
496 assert(opt_cmd.test_complete(" ti#") == [])
498 class BypassCliCmd(CliCmd):
499 def __init__(self, cmd, key = None):
503 def match(self, tokens):
504 m = self.cmd.match(tokens)
505 if m != None and self.key != None:
506 m[self.key] = " ".join(tokens)
509 def complete(self, tokens):
510 return self.cmd.complete(tokens)
513 return "(" + self.cmd.to_expr() + ")"
516 return "<BypassCliCmd(%s)>"%(self.cmd)
519 class AnyTextCliCmd(CliCmd):
520 def __init__(self, key = None, help_str = None):
521 debug("AnyTextCliCmd()")
523 # XXX factorize all help and key in mother class?
525 self.help_str = help_str
527 self.help_str = "Anytext token"
529 def match(self, tokens):
530 debug("----------match anytext <%s>"%tokens)
535 res[self.key] = tokens[0]
538 def complete(self, tokens):
539 debug("----------complete anytext <%s>"%tokens)
542 return PycolCompleteList()
543 # match, but cannot complete
544 return PycolCompleteList(PycolComplete(tokens[0], self))
547 return "<anytext>" # XXX
550 return "<AnyTextCliCmd()>"
556 anytext_cmd = AnyTextCliCmd()
558 assert(anytext_cmd.test_match("") == False)
559 assert(anytext_cmd.test_match("toto toto") == False)
560 assert(anytext_cmd.test_match(" toto") == True)
561 assert(anytext_cmd.test_match("toto ") == True)
562 assert(anytext_cmd.test_match(" toto \n\n") == True)
563 assert(anytext_cmd.test_match("toto # coin") == True)
564 assert(anytext_cmd.test_match("toto") == True)
566 assert(anytext_cmd.test_complete("") == [""])
567 assert(anytext_cmd.test_complete("to") == ["to"])
568 assert(anytext_cmd.test_complete("tot") == ["tot"])
569 assert(anytext_cmd.test_complete(" tot") == ["tot"])
570 assert(anytext_cmd.test_complete("toto") == ["toto"])
571 #assert(anytext_cmd.test_complete("toto#") == []) #XXX later
576 class IntCliCmd(CliCmd):
577 def __init__(self, val_min = None, val_max = None, key = None,
581 # XXX factorize all help and key in mother class?
583 self.help_str = help_str
585 self.help_str = "Int token"
586 # XXX val_min val_max
588 def match(self, tokens):
589 debug("----------match int <%s>"%tokens)
605 return "<IntCliCmd()>"
611 int_cmd = IntCliCmd()
613 assert(int_cmd.test_match("") == False)
614 assert(int_cmd.test_match("toto") == False)
615 assert(int_cmd.test_match(" 13") == True)
616 assert(int_cmd.test_match("-12342 ") == True)
617 assert(int_cmd.test_match(" 33 \n\n") == True)
618 assert(int_cmd.test_match("1 # coin") == True)
619 assert(int_cmd.test_match("0") == True)
621 assert(int_cmd.test_complete("") == [""])
622 assert(int_cmd.test_complete("1") == ["1"])
623 assert(int_cmd.test_complete("12") == ["12"])
624 assert(int_cmd.test_complete(" ee") == [])
627 class RegexpCliCmd(CliCmd):
628 def __init__(self, regexp, cmd = None, key = None,
629 store_re_match = False, help_str = None):
630 debug("RegexpCliCmd()")
634 self.store_re_match = store_re_match
635 # XXX factorize all help and key in mother class?
637 self.help_str = help_str
639 self.help_str = "Regexp token"
641 def match(self, tokens):
642 debug("----------match regexp <%s>"%tokens)
645 m = re.fullmatch(self.regexp, tokens[0])
649 res = self.cmd.match(tokens)
655 if self.store_re_match:
658 res[self.key] = tokens[0]
661 def complete(self, tokens):
663 return CliCmd.complete(self, tokens)
664 res = PycolCompleteList()
665 completions = self.cmd.complete(tokens)
666 for c in completions:
667 if self.match([c[0]]):
668 res.append(PycolComplete(c[0], self))
672 return "<RegexpCliCmd()>"
678 regexp_cmd = RegexpCliCmd("x[123]")
680 assert(regexp_cmd.test_match("") == False)
681 assert(regexp_cmd.test_match("toto") == False)
682 assert(regexp_cmd.test_match(" x1") == True)
683 assert(regexp_cmd.test_match("x3 ") == True)
684 assert(regexp_cmd.test_match(" x2\n\n") == True)
685 assert(regexp_cmd.test_match("x1 # coin") == True)
687 assert(regexp_cmd.test_complete("") == [""]) # XXX handle this case at upper level?
688 assert(regexp_cmd.test_complete("x1") == ["x1"])
689 assert(regexp_cmd.test_complete(" x2") == ["x2"])
690 assert(regexp_cmd.test_complete(" ee") == [])
694 class IPv4CliCmd(CliCmd):
695 def __init__(self, cmd = None, key = None, help_str = None):
696 debug("IPv4CliCmd()")
699 # XXX factorize all help and key in mother class?
701 self.help_str = help_str
703 self.help_str = "IPv4 token"
705 def match(self, tokens):
706 debug("----------match IPv4 <%s>"%tokens)
710 val = socket.inet_aton(tokens[0])
713 if len(tokens[0].split(".")) != 4:
717 res[self.key] = tokens[0]
720 def complete(self, tokens):
722 return CliCmd.complete(self, tokens)
723 res = PycolCompleteList()
724 completions = self.cmd.complete(tokens)
725 for c in completions:
726 if self.match([c[0]]):
727 res.append(PycolComplete(c[0], self))
731 return "<IPv4>" # XXX
734 return "<IPv4CliCmd()>"
740 ipv4_cmd = IPv4CliCmd()
742 assert(ipv4_cmd.test_match("") == False)
743 assert(ipv4_cmd.test_match("toto") == False)
744 assert(ipv4_cmd.test_match(" 13.3.1.3") == True)
745 assert(ipv4_cmd.test_match("255.255.255.255 ") == True)
746 assert(ipv4_cmd.test_match(" 0.0.0.0 \n\n") == True)
747 assert(ipv4_cmd.test_match("1.2.3.4 # coin") == True)
748 assert(ipv4_cmd.test_match("300.2.2.2") == False)
750 assert(ipv4_cmd.test_complete("") == [""])
751 assert(ipv4_cmd.test_complete("1.2.3.4") == ["1.2.3.4"])
752 assert(ipv4_cmd.test_complete(" ee") == [])
757 class FileCliCmd(CliCmd):
758 def __init__(self, key = None, help_str = None):
759 debug("FileCliCmd()")
761 # XXX factorize all help and key in mother class?
763 self.help_str = help_str
765 self.help_str = "File token"
767 def match(self, tokens):
768 debug("----------match File <%s>"%tokens)
771 if not os.path.exists(tokens[0]):
775 res[self.key] = tokens[0]
778 def complete(self, tokens):
779 debug("----------complete File <%s>"%tokens)
780 res = PycolCompleteList()
783 dirname = os.path.dirname(tokens[0])
784 basename = os.path.basename(tokens[0])
785 if dirname != "" and not os.path.exists(dirname):
787 debug("dirname=%s basename=%s"%(dirname, basename))
791 ls = os.listdir(dirname)
792 debug("ls=%s"%(str(ls)))
794 path = os.path.join(dirname, f)
796 if path.startswith(tokens[0]):
797 if os.path.isdir(path):
799 res.append(PycolComplete(path, self, terminal = False))
801 res.append(PycolComplete(path, self, terminal = True))
807 return "<File>" # XXX
810 return "<FileCliCmd()>"
816 file_cmd = FileCliCmd()
818 assert(file_cmd.test_match("") == False)
819 assert(file_cmd.test_match("DEDEDzx") == False)
820 assert(file_cmd.test_match("/tmp") == True)
821 assert(file_cmd.test_match("/etc/passwd ") == True)
823 assert(file_cmd.test_complete("/tm") == ["/tmp/"])
824 assert(file_cmd.test_complete(" eededezezzzc") == [])
829 class ChoiceCliCmd(CliCmd):
830 def __init__(self, choice, key = None, cb = None, help_str = None):
831 super().__init__(key = key, cb = cb, help_str = help_str)
832 if isinstance(choice, str):
833 self.get_list_func = lambda:[choice]
834 elif isinstance(choice, list):
835 self.get_list_func = lambda:choice
836 else: # XXX isinstance(func)
837 self.get_list_func = choice
838 debug("ChoiceCliCmd(%s)"%self.get_list_func)
840 def match(self, tokens):
841 # more than one token as input, does not match
844 l = self.get_list_func()
853 def complete(self, tokens):
854 # more than one token as input, does not match
856 return PycolCompleteList()
857 l = self.get_list_func()
858 complete = PycolCompleteList()
860 # the beginning of the string does not match
861 if len(tokens) == 1 and not e.startswith(tokens[0]):
863 complete.append(PycolComplete(e, self))
867 return "<ChoiceCliCmd(%s)>"%(self.get_list_func)
870 choice_cmd = ChoiceCliCmd(["toto", "titi"])
872 assert(choice_cmd.test_match("") == False)
873 assert(choice_cmd.test_match("tot") == False)
874 assert(choice_cmd.test_match("toto toto") == False)
875 assert(choice_cmd.test_match(" toto") == True)
876 assert(choice_cmd.test_match("titi ") == True)
877 assert(choice_cmd.test_match(" toto \n\n") == True)
878 assert(choice_cmd.test_match("toto # coin") == True)
879 assert(choice_cmd.test_match("toto") == True)
881 assert(choice_cmd.test_complete("") == ["toto", "titi"])
882 assert(choice_cmd.test_complete("to") == ["toto"])
883 assert(choice_cmd.test_complete("tot") == ["toto"])
884 assert(choice_cmd.test_complete(" tot") == ["toto"])
885 assert(choice_cmd.test_complete("toto") == ["toto"])
886 assert(choice_cmd.test_complete("d") == [])
887 assert(choice_cmd.test_complete("toto ") == [])
888 assert(choice_cmd.test_complete("toto#") == [])
891 if __name__ == '__main__':
894 "toto a|b [coin] [bar]",
896 "toto": TextCliCmd("toto", help_str = "help for toto"),
897 "a": TextCliCmd("a", help_str = "help for a"),
898 "b": TextCliCmd("b", help_str = "help for b"),
899 "coin": TextCliCmd("coin", help_str = "help for coin"),
900 "bar": TextCliCmd("bar", help_str = "help for bar"),
904 assert(cmd.test_match("toto a") == True)
905 assert(cmd.test_match("toto b coin") == True)
906 assert(cmd.test_match("toto") == False)