Salome HOME
Merge branch 'master' into V9_dev
[tools/configuration.git] / config / scfg
1 #!/usr/bin/env python
2 #  -*- coding: iso-8859-1 -*-
3
4 ###
5 # Command line tool to manage SALOME configuration.
6 # Usage: type "scfg help" to learn how to use tool.
7 ###
8
9 import sys
10 import os
11 from sconfig.salome_config import CfgTool, defaultConfFile
12
13 ###
14 # TODO
15 # 1. Improve parsing of command line: each command should be defined as:
16 #    - command:    name of the command
17 #    - help:       help string as appears in the short description of a command in a list
18 #                  of commands and as a first line of command's help
19 #    - options:    (dict) see below
20 #    - parameters: (list or dict) see below
21 #    Each option should be defined with:
22 #    - title:      one letter signature
23 #    - help:       help string
24 #    - parameter:  name of parameter if option requires a parameter
25 #    Each parameter should be defined with:
26 #    - title:      as appears in help information
27 #    - optional:   flag (boolean) showing if this parameter is optional
28 #    - help:       help description
29 #    
30 #    Parser of command line should then automatically check validity of the command,
31 #    its parameters and options.
32 #  
33 # 2. Usage and help information should be automaticall generated and formatted
34 #    from command description as described above.
35
36 # 3. Protect from adding same named option to a command.
37
38 # 4. Improve formatting help for command's parameters (see 1) - namely, wrapping
39 #    into a block.
40 ###
41
42 ##
43 # function: get_tool_name
44
45 # gets a name of this command line tool (string)
46 ##
47
48 def get_tool_name():
49     return os.path.basename(sys.argv[0])
50
51 ##
52 # function: verbose
53
54 # returns True when in debug mode (env var VERBOSE is set to 1)
55 ##
56
57 def verbose():
58     return bool(os.getenv("VERBOSE", False))
59
60 ##
61 # function: debug
62
63 # prints debug information (for debug purposes)
64 #
65 # Parameters:
66 #   args: output data to be printed to the screen (any printable objects)
67 ##
68
69 def debug(*args):
70     if verbose():
71         print("[DEBUG]", end=' ')
72         for a in args: print(a, end=' ')
73         print()
74     pass
75
76 ##
77 # function: error_exit
78
79 # prints error information and exits with non-zero status
80
81 # Parameters:
82 #   msg: error message (string)
83 ##
84
85 def error_exit(msg):
86     print("%s: %s" % (get_tool_name(), msg))
87     sys.exit(1)
88     pass
89
90 ##
91 # function: get_commands
92
93 # get supported commands
94
95 # return value is a dictionary:
96 # { cmd : ( help, opt_map ) }
97 # here:
98 # - cmd is a command (string)
99 # - help is a command's general help information on a command (string)
100 # - opt_map is a disctionary of allowed command's options:
101 #   { opt: opt_help }, where 
102 #   - opt is option ("a:" if option requires a parameter or just "a" elsewhere, where
103 #                    "a" is any letter)
104 #   - opt_help is a help information on the option (string).
105
106 # See also: add_command
107 ##
108
109 def get_commands():
110     if not globals().get("__commands__"): globals()["__commands__"] = {}
111     return globals().get("__commands__")
112
113 ##
114 # function: add_command
115
116 # adds command to the tool
117 #
118 # Parameters:
119 #   cmd: command (string)
120 #   help_string: help information on the command (string)
121 #   options: options supported by the command (map {opt: opt_help}, see get_commands)
122 ##
123
124 def add_command(cmd, help_string, options):
125     if cmd in get_commands():
126         debug("command %s already exists" % cmd)
127     else:
128         get_commands()[ cmd ] = ( help_string, options )
129     pass
130
131 ##
132 # function: get_opts
133
134 # gets supported options for the command
135
136 # Parameters:
137 #   cmd: command (string)
138
139 # return value is map {opt: opt_help} (see get_commands)
140 ##
141
142 def get_opts(cmd):
143     if cmd is None: cmd = ""
144     opts = {}
145     try:
146         opts = get_commands()[cmd][1]
147     except:
148         pass
149     return opts
150
151 ##
152 # function: get_help
153
154 # gets help string for the command
155
156 # Parameters:
157 #   cmd: command (string)
158
159 # return value is unformatted string
160 ##
161
162 def get_help(cmd):
163     if cmd is None: cmd = ""
164     help_string = ""
165     try:
166         help_string = get_commands()[cmd][0]
167     except:
168         pass
169     return help_string
170
171 ##
172 # function: format_string
173
174 # formats string into block according to the given block width
175
176 # Parameters:
177 #   s: string data
178 #   width: requested width of resulting string block
179
180 # return formatted string block
181 ##
182
183 def format_string(s, width):
184     blocks = s.split("\n")
185     result = []
186     for block in blocks:
187         block = block.replace("\t", " [[TAB]] ")
188         words = block.split()
189         current_string = ""
190         for word in words:
191             word = word if word != "[[TAB]]" else " "*2
192             if not current_string:
193                 current_string = word
194             else:
195                 if len(current_string + " " + word) <= width:
196                     current_string += " " + word
197                 else:
198                     result.append(current_string)
199                     current_string = word
200                     pass
201                 pass
202             pass
203         result.append(current_string)
204         pass
205     return "\n".join(result)
206
207 ##
208 # function: format_commands
209
210 # formats short help on all supported commands
211
212 # return string that contains formatted help on commands
213 ##
214
215 def format_commands():
216     commands = get_commands()
217     # filter out tool itself
218     commands = sorted([a for a in commands if a != ""])
219     # sort commands alphabetically
220     # get max command's length
221     max_length = max([ len(i) for i in commands])
222     # generate formatting string
223     prefix = "  "
224     separator = "    "
225     fmt_string = prefix + "%" + "-%ds" % max_length + separator
226     def _format(_c):
227         _h = get_help(_c).split("\n")[0]
228         _h = format_string(_h, 80-(max_length+len(prefix)+len(separator)))
229         _h = _h.split("\n")
230         _t = prefix + " " * max_length + separator
231         _r = []
232         _r.append(_h[0])
233         for _i in _h[1:]: _r.append(_t + _i)
234         return "\n".join(_r)
235     return "\n".join([ fmt_string % i + _format(i) for i in commands])
236
237 ##
238 # function: format_list_options
239
240 # formats short help on commands's options to be inserted into short help info on a command
241
242 # Parameters:
243 #   cmd: command (string)
244
245 # return string that contains formatted help
246 ##
247
248 def format_list_options(cmd):
249     opts = get_opts(cmd)
250     return "".join([" [%s]" % i for i in opts])
251
252 ##
253 # function: format_options
254
255 # formats help on commands's options to be inserted after short help info on a command
256
257 # Parameters:
258 #   cmd: command (string)
259
260 # return string that contains formatted help
261 ##
262
263 def format_options(cmd):
264     opts = get_opts(cmd)
265     opts_names = list(opts.keys())
266     if not opts: return ""
267     # get max command's length
268     max_length = max([ len(i) for i in opts_names])
269     # generate formatting string
270     prefix = "  "
271     separator = "    "
272     fmt_string = prefix + "%" + "-%ds" % max_length + separator
273     def _format(_o):
274         _h = opts[_o]
275         _h = format_string(_h, 80-(max_length+len(prefix)+len(separator)))
276         _h = _h.split("\n")
277         _t = prefix + " " * max_length + separator
278         _r = []
279         _r.append(_h[0])
280         for _i in _h[1:]: _r.append(_t + _i)
281         return "\n".join(_r)
282     return "\n" + "\n".join([ fmt_string % i + _format(i) for i in opts_names]) + "\n"
283
284 ##
285 # function: usage
286
287 # prints help and exits with zero status
288 #
289 # Parameters:
290 #   cmd: command (string)
291 ##
292
293 def usage(cmd=None):
294     if cmd is None: cmd = ""
295     if cmd not in get_commands(): error_exit("unknown command: %s" % cmd)
296     fmt = {}
297     fmt[ "prg" ] = get_tool_name()
298     fmt[ "cmd" ] = get_tool_name() if not cmd else cmd
299     fmt[ "cmd_list" ] = format_commands()
300     fmt[ "opts" ] = format_list_options(cmd)
301     fmt[ "opt_list" ] = format_options(cmd)
302     help_string = get_help(cmd)
303     help_string = help_string.replace("\t", "  ")
304     print(help_string.format(**fmt))
305     sys.exit(0)
306     pass
307
308 ##
309 # function: parse_cmd_line
310
311 # parses command line; prints error and exits if unsupported command
312 # or option is specified
313
314 # Parameters:
315 #   args: arguments being parsed (list of strings)
316 ##
317
318 def parse_cmd_line(args):
319     tool_opts, cmd, cmd_opts, tgt, params = {}, None, {}, None, []
320     args.reverse()
321     commands = get_commands()
322     while args:
323         a = args.pop()
324         if a == "-":
325             # empty option
326             error_exit("empty option is not allowed")
327         if a.startswith("-"):
328             allowed_opts = list(get_opts(cmd).keys())
329             allowed_opts = dict([(i[1], len(i)>2) for i in allowed_opts])
330             processed_opts = tool_opts if cmd is None else cmd_opts
331             cmd_descr = "" if cmd is None else " for command '%s'" % cmd
332             opts = a[1:]
333             while opts:
334                 o = opts[0]
335                 opts = opts[1:]
336                 if o == "-":
337                     # "--" format is not supported
338                     error_exit("invalid format of option")
339                 elif o in allowed_opts:
340                     if allowed_opts[o]:
341                         # supported option; requires parameter
342                         if opts:
343                             processed_opts[ o ] = opts
344                             opts = []
345                         elif args and not args[-1].startswith("-") and not args[-1] in commands:
346                             processed_opts[ o ] = args.pop()
347                         else:
348                             error_exit("option %s%s requires argument" % (o, cmd_descr))
349                     else:
350                         # supported option; does not require parameter
351                         processed_opts[ o ] = None
352                         pass
353                     pass
354                 else:
355                     # unsupported option
356                     error_exit("unknown option: -%s%s" % (o, cmd_descr))
357                 pass # while opts
358             pass
359         elif not cmd:
360             cmd = a
361             if cmd not in commands:
362                 # unsupported command
363                 error_exit("unknown command: %s" % cmd)
364             pass
365         elif not tgt:
366             tgt = a
367             pass
368         else:
369             params += [ a ]
370     return tool_opts, cmd, cmd_opts, tgt, params
371
372 ##
373 # function: main
374
375 # main entry point of the tool
376 ##
377
378 def main():
379     # set-up commands
380
381     # - tool itself
382     help_string  = "Command line tool to manage SALOME configuration.\n\n"
383     help_string += "Usage: {prg}{opts} COMMAND [ARGS]\n"
384     help_string += "{opt_list}\n"
385     help_string += "Supported commands:\n"
386     help_string += "{cmd_list}\n\n"
387     help_string += "See '{prg} help COMMAND' for more information on a specific command."
388     options = {}
389     options [ "-s FILE" ] = "Path to the configuration file; default: %s." % defaultConfFile()
390     add_command("",
391                 help_string,
392                 options)
393
394     # - help command
395     help_string  = "Display help information about this tool.\n\n"
396     help_string += "Usage: {prg} {cmd}{opts}\n"
397     help_string += "{opt_list}"
398     add_command("help",
399                 help_string,
400                 {})
401
402     # - set command
403     help_string  = "Create or modify configuration object.\n\n"
404     help_string += "Usage: {prg} {cmd}{opts} TARGET [PARAM VALUE ...]\n\n"
405     help_string += "\tTARGET: configuration object being created / modified\n"
406     help_string += "\tPARAM: parameter of the target object\n"
407     help_string += "\tVALUE: value to be assigned to the attribute of the target object\n"
408     help_string += "{opt_list}"
409     add_command("set",
410                 help_string,
411                 {})
412
413     # - get command
414     help_string  = "Get parameter of configuration object.\n\n"
415     help_string += "Usage: {prg} {cmd}{opts} TARGET PARAM\n\n"
416     help_string += "\tTARGET: configuration object being inspected\n"
417     help_string += "\tPARAM: parameter of the target object\n"
418     help_string += "{opt_list}"
419     add_command("get",
420                 help_string,
421                 {})
422
423     # - remove command
424     help_string  = "Remove configuration object or its attribute(s).\n\n"
425     help_string += "Usage: {prg} {cmd}{opts} TARGET [PARAM ...]\n\n"
426     help_string += "\tTARGET: configuration object\n"
427     help_string += "\tPARAM: attribute of the target object\n"
428     help_string += "{opt_list}"
429     add_command("remove",
430                 help_string,
431                 {})
432
433     # - dump command
434     help_string  = "Dump configuration.\n\n"
435     help_string += "Usage: {prg} {cmd}{opts} [TARGET]\n\n"
436     help_string += "\tTARGET: optional configuration object (if not specified,\n"
437     help_string += "\t        all configuration is dumped).\n"
438     help_string += "{opt_list}"
439     add_command("dump",
440                 help_string,
441                 {})
442
443     # - verify command
444     help_string  = "Verify configuration.\n\n"
445     help_string += "Usage: {prg} {cmd}{opts} [TARGET]\n\n"
446     help_string += "\tTARGET: optional configuration object (if not specified,\n"
447     help_string += "\t        all configuration is verified).\n"
448     help_string += "{opt_list}"
449     add_command("verify",
450                 help_string,
451                 {})
452
453     # - clean command
454     help_string  = "Clean configuration.\n\n"
455     help_string += "Usage: {prg} {cmd}{opts}\n"
456     help_string += "{opt_list}"
457     add_command("clean",
458                 help_string,
459                 {})
460
461     # parse command line
462
463     opts, cmd, cmd_opts, tgt, params = parse_cmd_line(sys.argv[1:])
464     debug("parse command line: options =", opts, "; command =", cmd, "; command options =", cmd_opts, "; target =", tgt, "; arguments =", params)
465
466     # process command
467
468     if not cmd or cmd == "help":
469         # show help and exit
470         usage(tgt)
471         pass
472
473     try:
474         # check if custom source file is specified
475         
476         src_file = opts["s"] if "s" in opts else None
477         debug("source file:", src_file)
478         
479         # create config tool
480
481         cfg_tool = CfgTool(src_file)
482         debug("cfg tool:", cfg_tool)
483
484         # TODO: we should not check commands in a switch-like block below;
485         # instead processing should be done automatically basing on the
486         # predefined set of commands (TODO)
487
488         if cmd == "get":
489             if not tgt:
490                 error_exit("%s: target is not specified!" % cmd)
491             if len(params) < 1:
492                 error_exit("%s: parameter of target %s is not specified!" % (cmd, tgt))
493             # only one parameter can be read!
494             if len(params) > 1:
495                 error_exit("%s: can't read more than one parameter at once!" % cmd)
496             def _toString(v):
497                 if v is None: return ""
498                 elif isinstance(v, list): return " ".join(v)
499                 else: return v
500             print(_toString(cfg_tool.get(tgt, params[0])))
501             pass
502         elif cmd == "set":
503             if not tgt:
504                 error_exit("%s: target is not specified!" % cmd)
505             # empty parameters list is allowed!
506             cfg_tool.set(tgt, *params)
507             pass
508         elif cmd == "remove":
509             if not tgt:
510                 error_exit("%s: target is not specified!" % cmd)
511             # empty parameters list is allowed!
512             cfg_tool.remove(tgt, *params)
513             pass
514         elif cmd == "dump":
515             if len(params) > 0:
516                 error_exit("%s: command does not support parameters!" % cmd)
517             # empty target is allowed!
518             cfg_tool.dump(tgt)
519             pass
520         elif cmd == "verify":
521             if len(params) > 0:
522                 error_exit("%s: command does not support parameters!" % cmd)
523             # empty target is allowed!
524             errors = []
525             if cfg_tool.verify(tgt, errors):
526                 msg = " (no products found)" if not cfg_tool.get("cfg", "products") else ""
527                 print("Configuration is valid%s." % msg)
528             else:
529                 raise Exception("Configuration is invalid:\n"+ "\n".join(errors))
530             pass
531         elif cmd == "clean":
532             if tgt:
533                 error_exit("%s: command does not support target!" % cmd)
534             if len(params) > 0:
535                 error_exit("%s: command does not support parameters!" % cmd)
536             # empty target is allowed!
537             cfg_tool.clean()
538             pass
539         else:
540             # unknown command: normally we should not go here
541             error_exit("unknown command: %s" % cmd)
542             pass
543         pass
544     except Exception as e:
545         error_exit(e)
546     pass
547     
548 if __name__ == "__main__":
549     main()