Salome HOME
870e0451b3ac492edbb67874fc8b4a0341e9b8f5
[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]",
72         for a in args: print a,
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 = filter(lambda a: a != "", commands)
219     # sort commands alphabetically
220     commands.sort()
221     # get max command's length
222     max_length = max([ len(i) for i in commands])
223     # generate formatting string
224     prefix = "  "
225     separator = "    "
226     fmt_string = prefix + "%" + "-%ds" % max_length + separator
227     def _format(_c):
228         _h = get_help(_c).split("\n")[0]
229         _h = format_string(_h, 80-(max_length+len(prefix)+len(separator)))
230         _h = _h.split("\n")
231         _t = prefix + " " * max_length + separator
232         _r = []
233         _r.append(_h[0])
234         for _i in _h[1:]: _r.append(_t + _i)
235         return "\n".join(_r)
236     return "\n".join([ fmt_string % i + _format(i) for i in commands])
237
238 ##
239 # function: format_list_options
240
241 # formats short help on commands's options to be inserted into short help info on a command
242
243 # Parameters:
244 #   cmd: command (string)
245
246 # return string that contains formatted help
247 ##
248
249 def format_list_options(cmd):
250     opts = get_opts(cmd)
251     return "".join([" [%s]" % i for i in opts.keys()])
252
253 ##
254 # function: format_options
255
256 # formats help on commands's options to be inserted after short help info on a command
257
258 # Parameters:
259 #   cmd: command (string)
260
261 # return string that contains formatted help
262 ##
263
264 def format_options(cmd):
265     opts = get_opts(cmd)
266     opts_names = opts.keys()
267     if not opts: return ""
268     # get max command's length
269     max_length = max([ len(i) for i in opts_names])
270     # generate formatting string
271     prefix = "  "
272     separator = "    "
273     fmt_string = prefix + "%" + "-%ds" % max_length + separator
274     def _format(_o):
275         _h = opts[_o]
276         _h = format_string(_h, 80-(max_length+len(prefix)+len(separator)))
277         _h = _h.split("\n")
278         _t = prefix + " " * max_length + separator
279         _r = []
280         _r.append(_h[0])
281         for _i in _h[1:]: _r.append(_t + _i)
282         return "\n".join(_r)
283     return "\n" + "\n".join([ fmt_string % i + _format(i) for i in opts_names]) + "\n"
284
285 ##
286 # function: usage
287
288 # prints help and exits with zero status
289 #
290 # Parameters:
291 #   cmd: command (string)
292 ##
293
294 def usage(cmd=None):
295     if cmd is None: cmd = ""
296     if cmd not in get_commands(): error_exit("unknown command: %s" % cmd)
297     fmt = {}
298     fmt[ "prg" ] = get_tool_name()
299     fmt[ "cmd" ] = get_tool_name() if not cmd else cmd
300     fmt[ "cmd_list" ] = format_commands()
301     fmt[ "opts" ] = format_list_options(cmd)
302     fmt[ "opt_list" ] = format_options(cmd)
303     help_string = get_help(cmd)
304     help_string = help_string.replace("\t", "  ")
305     print help_string.format(**fmt)
306     sys.exit(0)
307     pass
308
309 ##
310 # function: parse_cmd_line
311
312 # parses command line; prints error and exits if unsupported command
313 # or option is specified
314
315 # Parameters:
316 #   args: arguments being parsed (list of strings)
317 ##
318
319 def parse_cmd_line(args):
320     tool_opts, cmd, cmd_opts, tgt, params = {}, None, {}, None, []
321     args.reverse()
322     commands = get_commands()
323     while args:
324         a = args.pop()
325         if a == "-":
326             # empty option
327             error_exit("empty option is not allowed")
328         if a.startswith("-"):
329             allowed_opts = get_opts(cmd).keys()
330             allowed_opts = dict([(i[1], len(i)>2) for i in allowed_opts])
331             processed_opts = tool_opts if cmd is None else cmd_opts
332             cmd_descr = "" if cmd is None else " for command '%s'" % cmd
333             opts = a[1:]
334             while opts:
335                 o = opts[0]
336                 opts = opts[1:]
337                 if o == "-":
338                     # "--" format is not supported
339                     error_exit("invalid format of option")
340                 elif o in allowed_opts.keys():
341                     if allowed_opts[o]:
342                         # supported option; requires parameter
343                         if opts:
344                             processed_opts[ o ] = opts
345                             opts = []
346                         elif args and not args[-1].startswith("-") and not args[-1] in commands:
347                             processed_opts[ o ] = args.pop()
348                         else:
349                             error_exit("option %s%s requires argument" % (o, cmd_descr))
350                     else:
351                         # supported option; does not require parameter
352                         processed_opts[ o ] = None
353                         pass
354                     pass
355                 else:
356                     # unsupported option
357                     error_exit("unknown option: -%s%s" % (o, cmd_descr))
358                 pass # while opts
359             pass
360         elif not cmd:
361             cmd = a
362             if cmd not in commands:
363                 # unsupported command
364                 error_exit("unknown command: %s" % cmd)
365             pass
366         elif not tgt:
367             tgt = a
368             pass
369         else:
370             params += [ a ]
371     return tool_opts, cmd, cmd_opts, tgt, params
372
373 ##
374 # function: main
375
376 # main entry point of the tool
377 ##
378
379 def main():
380     # set-up commands
381
382     # - tool itself
383     help_string  = "Command line tool to manage SALOME configuration.\n\n"
384     help_string += "Usage: {prg}{opts} COMMAND [ARGS]\n"
385     help_string += "{opt_list}\n"
386     help_string += "Supported commands:\n"
387     help_string += "{cmd_list}\n\n"
388     help_string += "See '{prg} help COMMAND' for more information on a specific command."
389     options = {}
390     options [ "-s FILE" ] = "Path to the configuration file; default: %s." % defaultConfFile()
391     add_command("",
392                 help_string,
393                 options)
394
395     # - help command
396     help_string  = "Display help information about this tool.\n\n"
397     help_string += "Usage: {prg} {cmd}{opts}\n"
398     help_string += "{opt_list}"
399     add_command("help",
400                 help_string,
401                 {})
402
403     # - set command
404     help_string  = "Create or modify configuration object.\n\n"
405     help_string += "Usage: {prg} {cmd}{opts} TARGET [PARAM VALUE ...]\n\n"
406     help_string += "\tTARGET: configuration object being created / modified\n"
407     help_string += "\tPARAM: parameter of the target object\n"
408     help_string += "\tVALUE: value to be assigned to the attribute of the target object\n"
409     help_string += "{opt_list}"
410     add_command("set",
411                 help_string,
412                 {})
413
414     # - get command
415     help_string  = "Get parameter of configuration object.\n\n"
416     help_string += "Usage: {prg} {cmd}{opts} TARGET PARAM\n\n"
417     help_string += "\tTARGET: configuration object being inspected\n"
418     help_string += "\tPARAM: parameter of the target object\n"
419     help_string += "{opt_list}"
420     add_command("get",
421                 help_string,
422                 {})
423
424     # - remove command
425     help_string  = "Remove configuration object or its attribute(s).\n\n"
426     help_string += "Usage: {prg} {cmd}{opts} TARGET [PARAM ...]\n\n"
427     help_string += "\tTARGET: configuration object\n"
428     help_string += "\tPARAM: attribute of the target object\n"
429     help_string += "{opt_list}"
430     add_command("remove",
431                 help_string,
432                 {})
433
434     # - dump command
435     help_string  = "Dump configuration.\n\n"
436     help_string += "Usage: {prg} {cmd}{opts} [TARGET]\n\n"
437     help_string += "\tTARGET: optional configuration object (if not specified,\n"
438     help_string += "\t        all configuration is dumped).\n"
439     help_string += "{opt_list}"
440     add_command("dump",
441                 help_string,
442                 {})
443
444     # - verify command
445     help_string  = "Verify configuration.\n\n"
446     help_string += "Usage: {prg} {cmd}{opts} [TARGET]\n\n"
447     help_string += "\tTARGET: optional configuration object (if not specified,\n"
448     help_string += "\t        all configuration is verified).\n"
449     help_string += "{opt_list}"
450     add_command("verify",
451                 help_string,
452                 {})
453
454     # - clean command
455     help_string  = "Clean configuration.\n\n"
456     help_string += "Usage: {prg} {cmd}{opts}\n"
457     help_string += "{opt_list}"
458     add_command("clean",
459                 help_string,
460                 {})
461
462     # parse command line
463
464     opts, cmd, cmd_opts, tgt, params = parse_cmd_line(sys.argv[1:])
465     debug("parse command line: options =", opts, "; command =", cmd, "; command options =", cmd_opts, "; target =", tgt, "; arguments =", params)
466
467     # process command
468
469     if not cmd or cmd == "help":
470         # show help and exit
471         usage(tgt)
472         pass
473
474     try:
475         # check if custom source file is specified
476         
477         src_file = opts["s"] if opts.has_key("s") else None
478         debug("source file:", src_file)
479         
480         # create config tool
481
482         cfg_tool = CfgTool(src_file)
483         debug("cfg tool:", cfg_tool)
484
485         # TODO: we should not check commands in a switch-like block below;
486         # instead processing should be done automatically basing on the
487         # predefined set of commands (TODO)
488
489         if cmd == "get":
490             if not tgt:
491                 error_exit("%s: target is not specified!" % cmd)
492             if len(params) < 1:
493                 error_exit("%s: parameter of target %s is not specified!" % (cmd, tgt))
494             # only one parameter can be read!
495             if len(params) > 1:
496                 error_exit("%s: can't read more than one parameter at once!" % cmd)
497             def _toString(v):
498                 if v is None: return ""
499                 elif type(v) is list: return " ".join(v)
500                 else: return v
501             print _toString(cfg_tool.get(tgt, params[0]))
502             pass
503         elif cmd == "set":
504             if not tgt:
505                 error_exit("%s: target is not specified!" % cmd)
506             # empty parameters list is allowed!
507             cfg_tool.set(tgt, *params)
508             pass
509         elif cmd == "remove":
510             if not tgt:
511                 error_exit("%s: target is not specified!" % cmd)
512             # empty parameters list is allowed!
513             cfg_tool.remove(tgt, *params)
514             pass
515         elif cmd == "dump":
516             if len(params) > 0:
517                 error_exit("%s: command does not support parameters!" % cmd)
518             # empty target is allowed!
519             cfg_tool.dump(tgt)
520             pass
521         elif cmd == "verify":
522             if len(params) > 0:
523                 error_exit("%s: command does not support parameters!" % cmd)
524             # empty target is allowed!
525             errors = []
526             if cfg_tool.verify(tgt, errors):
527                 msg = " (no products found)" if not cfg_tool.get("cfg", "products") else ""
528                 print "Configuration is valid%s." % msg
529             else:
530                 raise Exception("Configuration is invalid:\n"+ "\n".join(errors))
531             pass
532         elif cmd == "clean":
533             if tgt:
534                 error_exit("%s: command does not support target!" % cmd)
535             if len(params) > 0:
536                 error_exit("%s: command does not support parameters!" % cmd)
537             # empty target is allowed!
538             cfg_tool.clean()
539             pass
540         else:
541             # unknown command: normally we should not go here
542             error_exit("unknown command: %s" % cmd)
543             pass
544         pass
545     except Exception, e:
546         error_exit(e)
547     pass
548     
549 if __name__ == "__main__":
550     main()