comparison frontends/src/jp/cmd_shell.py @ 2309:c7a72b75232b

jp (shell): shell command (REPL mode), first draft: This command launch jp in REPL mode, allowing do normal jp commands with some facilities. Command can be selected with "cmd" (e.g. "cmd blog"). An argument can be fixed with "use" (e.g. "use output fancy"). Command is launched with "do", or directly with its name if it doesn't conflict with a shell command. Arguments completion is still TODO (only shell commands are completed so far).
author Goffi <goffi@goffi.org>
date Thu, 06 Jul 2017 20:28:25 +0200
parents
children 5996063ecad7
comparison
equal deleted inserted replaced
2308:0b21d87c91cf 2309:c7a72b75232b
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # jp: a SàT command line tool
5 # Copyright (C) 2009-2016 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20
21 import base
22 import cmd
23 from sat.core.i18n import _
24 from sat.core import exceptions
25 from sat_frontends.jp.constants import Const as C
26 from sat.tools.common.ansi import ANSI as A
27 import shlex
28
29 __commands__ = ["Shell"]
30 INTRO = _(u"""Welcome to {app_name} shell, the Salut à Toi shell !
31
32 This enrironment helps you using several {app_name} commands with similar parameters.
33
34 To quit, just enter "quit" or press C-c.
35 Enter "help" or "?" to know what to do
36 """).format(app_name = C.APP_NAME)
37
38
39 class Shell(base.CommandBase, cmd.Cmd):
40
41 def __init__(self, host):
42 base.CommandBase.__init__(self, host, 'shell', help=_(u'launch jp in shell (REPL) mode'))
43 cmd.Cmd.__init__(self)
44
45 def parse_args(self, args):
46 """parse line arguments"""
47 return shlex.split(args, posix=True)
48
49 def get_cmd_choices(self, cmd=None, parser=None):
50 if parser is None:
51 parser = self._cur_parser
52 try:
53 choices = parser._subparsers._group_actions[0].choices
54 return choices[cmd] if cmd is not None else choices
55 except (KeyError, AttributeError):
56 raise exceptions.NotFound
57
58 def update_path(self):
59 self._cur_parser = self.host.parser
60 self.help = u''
61 for idx, path_elt in enumerate(self.path):
62 try:
63 self._cur_parser = self.get_cmd_choices(path_elt)
64 except exceptions.NotFound:
65 self.disp(_(u'bad command path'), error=True)
66 self.path=self.path[:idx]
67 break
68 else:
69 self.help = self._cur_parser
70
71 self.prompt = A.color(C.A_PROMPT_PATH, u'/'.join(self.path)) + A.color(C.A_PROMPT_SUF, u'> ')
72 try:
73 self.actions = self.get_cmd_choices().keys()
74 except exceptions.NotFound:
75 self.actions = []
76
77 def add_parser_options(self):
78 pass
79
80 def escape_arg(self, arg):
81 """format arg with quotes"""
82 return u'"' + arg.replace(u'"',u'\\"') + u'"'
83
84 def format_args(self, args):
85 """format argument to be printed with quotes if needed"""
86 for arg in args:
87 if " " in arg:
88 yield self.escape_arg(arg)
89 else:
90 yield arg
91
92 def get_use_args(self, args):
93 """format args for current parser according to self.use"""
94 parser = self._cur_parser
95
96 # we check not optional args to see if there
97 # is a corresonding parser
98 # else USE args would not work correctly (only for current parser)
99 cmd_args = []
100 for arg in args:
101 if arg.startswith(u'-'):
102 break
103 try:
104 parser = self.get_cmd_choices(arg, parser)
105 except exceptions.NotFound:
106 break
107 cmd_args.append(arg)
108
109 # we remove command args
110 # they'll be in returned list (use_args)
111 del args[:len(cmd_args)]
112
113 opt_args = []
114 pos_args = []
115 actions = {a.dest: a for a in parser._actions}
116 for arg, value in self.use.iteritems():
117 try:
118 action = actions[arg]
119 except KeyError:
120 if self.verbose:
121 self.disp(_(u'ignoring {name}={value}, not corresponding to any argument (in USE)').format(
122 name=arg,
123 value=self.escape_arg(value)))
124 else:
125 if self.verbose:
126 self.disp(_(u'arg {name}={value} set (in USE)').format(name=arg, value=self.escape_arg(value)))
127 if not action.option_strings:
128 pos_args.append(value)
129 else:
130 opt_args.append(action.option_strings[0])
131 opt_args.append(value)
132 return cmd_args + opt_args + pos_args
133
134 def default(self, args):
135 """called when no shell command is recognized
136
137 will launch the command with args on the line
138 (i.e. will launch do [args])
139 """
140 self.do_do(args)
141
142 def do_help(self, args):
143 """show help message"""
144 self.disp(A.color(C.A_HEADER, _(u'Shell commands:')), no_lf=True)
145 super(Shell, self).do_help(args)
146 if not args:
147 self.disp(A.color(C.A_HEADER, _(u'Action commands:')))
148 help_list = self._cur_parser.format_help().split('\n\n')
149 print('\n\n'.join(help_list[1 if self.path else 2:]))
150
151 def do_debug(self, args):
152 """launch internal debugger"""
153 try:
154 import ipdb as pdb
155 except ImportError:
156 import pdb
157 pdb.set_trace()
158
159 def do_verbose(self, args):
160 """show verbose mode, or (de)activate it"""
161 args = self.parse_args(args)
162 if args:
163 self.verbose = C.bool(args[0])
164 self.disp(_(u'verbose mode is {status}').format(
165 status = _(u'ENABLED') if self.verbose else _(u'DISABLED')))
166
167 def do_cmd(self, args):
168 """change command path"""
169 if args == '..':
170 self.path = self.path[:-1]
171 else:
172 if not args or args[0] == '/':
173 self.path = []
174 args = '/'.join(args.split())
175 for path_elt in args.split('/'):
176 path_elt = path_elt.strip()
177 if not path_elt:
178 continue
179 self.path.append(path_elt)
180 self.update_path()
181
182 def do_version(self, args):
183 """show current SàT/jp version"""
184 try:
185 self.host.run(['--version'])
186 except SystemExit:
187 pass
188
189 def do_do(self, args):
190 """lauch a command"""
191 # we don't want host to really exit
192 # we want to stay in the loop
193 self.host._no_exit = True
194 args = self.parse_args(args)
195 # args may be modified by use_args
196 # to remove subparsers from it
197 use_args = self.get_use_args(args)
198 cmd_args = self.path + use_args + args
199 if self.verbose:
200 self.disp(u"COMMAND => {args}".format(args=u' '.join(self.format_args(cmd_args))))
201 try:
202 self.host.run(cmd_args)
203 except SystemExit as e:
204 if e.code != 0:
205 self.disp(A.color(C.A_FAILURE, u'command failed with an error code of {err_no}'.format(err_no=e.code)), error=True)
206 except Exception as e:
207 self.disp(A.color(C.A_FAILURE, u'command failed with an exception: {msg}'.format(msg=e)), error=True)
208 finally:
209 self.host._no_exit = False
210
211 def do_use(self, args):
212 """fix an argument"""
213 args = self.parse_args(args)
214 if not args:
215 if not self.use:
216 self.disp(_(u'no argument in USE'))
217 else:
218 self.disp(_(u'arguments in USE:'))
219 for arg, value in self.use.iteritems():
220 self.disp(_(A.color(C.A_SUBHEADER, arg, A.RESET, u' = ', self.escape_arg(value))))
221 elif len(args) != 2:
222 self.disp(u'bad syntax, please use:\nuse [arg] [value]', error=True)
223 else:
224 self.use[args[0]] = u' '.join(args[1:])
225 if self.verbose:
226 self.disp('set {name} = {value}'.format(
227 name = args[0], value=self.escape_arg(args[1])))
228
229 def do_use_clear(self, args):
230 """unset one or many argument(s) in USE, or all of them if no arg is specified"""
231 args = self.parse_args(args)
232 if not args:
233 self.use.clear()
234 else:
235 for arg in args:
236 try:
237 del self.use[arg]
238 except KeyError:
239 self.disp(A.color(C.A_FAILURE, _(u'argument {name} not found').format(name=arg)), error=True)
240 else:
241 if self.verbose:
242 self.disp(_(u'argument {name} removed').format(name=arg))
243
244 def do_quit(self, args):
245 u"""quit the shell"""
246 self.disp(_(u'good bye!'))
247 self.host.quit()
248
249 def do_exit(self, args):
250 u"""alias for quit"""
251 self.do_quit(args)
252
253 def start(self):
254 self.path = []
255 self._cur_parser = self.host.parser
256 self.use = {}
257 self.verbose = False
258 self.update_path()
259 self.cmdloop(INTRO.encode('utf-8'))