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