cli: add short command descriptions to their help text
[deliverable/barectf.git] / barectf / cli.py
1 # The MIT License (MIT)
2 #
3 # Copyright (c) 2014-2020 Philippe Proulx <pproulx@efficios.com>
4 #
5 # Permission is hereby granted, free of charge, to any person obtaining
6 # a copy of this software and associated documentation files (the
7 # "Software"), to deal in the Software without restriction, including
8 # without limitation the rights to use, copy, modify, merge, publish,
9 # distribute, sublicense, and/or sell copies of the Software, and to
10 # permit persons to whom the Software is furnished to do so, subject to
11 # the following conditions:
12 #
13 # The above copyright notice and this permission notice shall be
14 # included in all copies or substantial portions of the Software.
15 #
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
24 import pkg_resources
25 import collections
26 import termcolor
27 import argparse
28 import os.path
29 import barectf
30 import barectf.config_parse_common as barectf_config_parse_common
31 import barectf.argpar as barectf_argpar
32 import sys
33 import os
34
35
36 # Colors and prints the error message `msg` and exits with status code
37 # 1.
38 def _print_error(msg):
39 termcolor.cprint('Error: ', 'red', end='', file=sys.stderr)
40 termcolor.cprint(msg, 'red', attrs=['bold'], file=sys.stderr)
41 sys.exit(1)
42
43
44 # Pretty-prints the barectf configuration error `exc` and exits with
45 # status code 1.
46 def _print_config_error(exc):
47 # reverse: most precise message comes last
48 for ctx in reversed(exc.context):
49 msg = ''
50
51 if ctx.message is not None:
52 msg = f' {ctx.message}'
53
54 color = 'red'
55 termcolor.cprint(f'{ctx.name}', color, attrs=['bold'], file=sys.stderr, end='')
56 termcolor.cprint(':', color, file=sys.stderr, end='')
57 termcolor.cprint(msg, color, file=sys.stderr)
58
59 sys.exit(1)
60
61
62 # Pretty-prints the unknown exception `exc`.
63 def _print_unknown_exc(exc):
64 import traceback
65
66 traceback.print_exc()
67 _print_error(f'Unknown exception: {exc}')
68
69
70 # Finds and returns all the option items in `items` having the long name
71 # `long_name`.
72 def _find_opt_items(items, long_name):
73 ret_items = []
74
75 for item in items:
76 if type(item) is barectf_argpar._OptItem and item.descr.long_name == long_name:
77 ret_items.append(item)
78
79 return ret_items
80
81
82 # Returns:
83 #
84 # For an option item without an argument:
85 # `True`.
86 #
87 # For an option item with an argument:
88 # Its argument.
89 #
90 # Uses the last option item having the long name `long_name` found in
91 # `items`.
92 #
93 # Returns `default` if there's no such option item.
94 def _opt_item_val(items, long_name, default=None):
95 opt_items = _find_opt_items(items, long_name)
96
97 if len(opt_items) == 0:
98 return default
99
100 opt_item = opt_items[-1]
101
102 if opt_item.descr.has_arg:
103 return opt_item.arg_text
104
105 return True
106
107
108 class _CliError(Exception):
109 pass
110
111
112 def _cfg_file_path_from_parse_res(parse_res):
113 cfg_file_path = None
114
115 for item in parse_res.items:
116 if type(item) is barectf_argpar._NonOptItem:
117 if cfg_file_path is not None:
118 raise _CliError('Multiple configuration file paths provided')
119
120 cfg_file_path = item.text
121
122 if cfg_file_path is None:
123 raise _CliError('Missing configuration file path')
124
125 if not os.path.isfile(cfg_file_path):
126 raise _CliError(f'`{cfg_file_path}` is not an existing, regular file')
127
128 return cfg_file_path
129
130
131 # Returns a `_CfgCmdCfg` object from the command-line parsing results
132 # `parse_res`.
133 def _cfg_cmd_cfg_from_parse_res(parse_res):
134 # check configuration file path
135 cfg_file_path = _cfg_file_path_from_parse_res(parse_res)
136
137 # inclusion directories
138 inclusion_dirs = [item.arg_text for item in _find_opt_items(parse_res.items, 'include-dir')]
139
140 for dir in inclusion_dirs:
141 if not os.path.isdir(dir):
142 raise _CliError(f'`{dir}` is not an existing directory')
143
144 inclusion_dirs.append(os.getcwd())
145
146 # other options
147 ignore_inclusion_file_not_found = _opt_item_val(parse_res.items, 'ignore-include-not-found',
148 False)
149
150 return _CfgCmdCfg(cfg_file_path, inclusion_dirs, ignore_inclusion_file_not_found)
151
152
153 def _print_gen_cmd_usage():
154 print('''Usage: barectf generate [--code-dir=DIR] [--headers-dir=DIR]
155 [--metadata-dir=DIR] [--prefix=PREFIX]
156 [--include-dir=DIR]... [--ignore-include-not-found]
157 CONFIG-FILE-PATH
158 barectf generate --help
159
160 Generate the C source and CTF metadata stream files of a tracer from the
161 configuration file CONFIG-FILE-PATH.
162
163 Options:
164 -c DIR, --code-dir=DIR Write C source files to DIR instead of the CWD
165 -H DIR, --headers-dir=DIR Write C header files to DIR instead of the CWD
166 -h, --help Show this help and quit
167 --ignore-include-not-found Continue to process the configuration file when
168 included files are not found
169 -I DIR, --include-dir=DIR Add DIR to the list of directories to be
170 searched for inclusion files
171 -m DIR, --metadata-dir=DIR Write the metadata stream file to DIR instead of
172 the CWD
173 -p PREFIX, --prefix=PREFIX Set the configuration prefix to PREFIX''')
174
175
176 # Returns a source and metadata stream file generating command object
177 # from the specific command-line arguments `orig_args`.
178 def _gen_cmd_cfg_from_args(orig_args):
179 # parse original arguments
180 opt_descrs = [
181 barectf_argpar.OptDescr('h', 'help'),
182 barectf_argpar.OptDescr('c', 'code-dir', True),
183 barectf_argpar.OptDescr('H', 'headers-dir', True),
184 barectf_argpar.OptDescr('I', 'include-dir', True),
185 barectf_argpar.OptDescr('m', 'metadata-dir', True),
186 barectf_argpar.OptDescr('p', 'prefix', True),
187 barectf_argpar.OptDescr(long_name='dump-config'),
188 barectf_argpar.OptDescr(long_name='ignore-include-not-found'),
189 ]
190 res = barectf_argpar.parse(orig_args, opt_descrs)
191 assert len(res.ingested_orig_args) == len(orig_args)
192
193 # command help?
194 if len(_find_opt_items(res.items, 'help')) > 0:
195 _print_gen_cmd_usage()
196 sys.exit()
197
198 # get common configuration file command CLI configuration
199 cfg_cmd_cfg = _cfg_cmd_cfg_from_parse_res(res)
200
201 # directories
202 c_source_dir = _opt_item_val(res.items, 'code-dir', os.getcwd())
203 c_header_dir = _opt_item_val(res.items, 'headers-dir', os.getcwd())
204 metadata_stream_dir = _opt_item_val(res.items, 'metadata-dir', os.getcwd())
205
206 for dir in [c_source_dir, c_header_dir, metadata_stream_dir]:
207 if not os.path.isdir(dir):
208 raise _CliError(f'`{dir}` is not an existing directory')
209
210 # other options
211 dump_config = _opt_item_val(res.items, 'dump-config', False)
212 v2_prefix = _opt_item_val(res.items, 'prefix')
213
214 return _GenCmd(_GenCmdCfg(cfg_cmd_cfg.cfg_file_path, c_source_dir, c_header_dir,
215 metadata_stream_dir, cfg_cmd_cfg.inclusion_dirs,
216 cfg_cmd_cfg.ignore_inclusion_file_not_found, dump_config, v2_prefix))
217
218
219 def _show_effective_cfg_cmd_usage():
220 print('''Usage: barectf show-effective-configuration [--include-dir=DIR]...
221 [--ignore-include-not-found]
222 [--indent-spaces=COUNT] CONFIG-FILE-PATH
223 barectf show-effective-configuration --help
224
225 Print the effective configuration file for a the configuration file
226 CONFIG-FILE-PATH.
227
228 Options:
229 -h, --help Show this help and quit
230 --ignore-include-not-found Continue to process the configuration file when
231 included files are not found
232 -I DIR, --include-dir=DIR Add DIR to the list of directories to be
233 searched for inclusion files
234 --indent-spaces=COUNT Use COUNT spaces at a time to indent YAML lines
235 instead of 2''')
236
237
238 # Returns an effective configuration showing command object from the
239 # specific command-line arguments `orig_args`.
240 def _show_effective_cfg_cfg_from_args(orig_args):
241 # parse original arguments
242 opt_descrs = [
243 barectf_argpar.OptDescr('h', 'help'),
244 barectf_argpar.OptDescr('I', 'include-dir', True),
245 barectf_argpar.OptDescr(long_name='indent-spaces', has_arg=True),
246 barectf_argpar.OptDescr(long_name='ignore-include-not-found'),
247 ]
248 res = barectf_argpar.parse(orig_args, opt_descrs)
249 assert len(res.ingested_orig_args) == len(orig_args)
250
251 # command help?
252 if len(_find_opt_items(res.items, 'help')) > 0:
253 _show_effective_cfg_cmd_usage()
254 sys.exit()
255
256 # get common configuration command CLI configuration
257 cfg_cmd_cfg = _cfg_cmd_cfg_from_parse_res(res)
258
259 # other options
260 indent_space_count = _opt_item_val(res.items, 'indent-spaces', 2)
261
262 try:
263 indent_space_count = int(indent_space_count)
264 except (ValueError, TypeError):
265 raise _CliError(f'Invalid `--indent-spaces` option argument: `{indent_space_count}`')
266
267 if indent_space_count < 1 or indent_space_count > 8:
268 raise _CliError(f'Invalid `--indent-spaces` option argument (`{indent_space_count}`): expecting a value in [1, 8]')
269
270 return _ShowEffectiveCfgCmd(_ShowEffectiveCfgCmdCfg(cfg_cmd_cfg.cfg_file_path,
271 cfg_cmd_cfg.inclusion_dirs,
272 cfg_cmd_cfg.ignore_inclusion_file_not_found,
273 indent_space_count))
274
275
276 def _show_cfg_version_cmd_usage():
277 print('''Usage: barectf show-configuration-version CONFIG-FILE-PATH
278 barectf show-configuration-version --help
279
280 Print the major version (2 or 3) of the configuration file CONFIG-FILE-PATH.
281
282 Options:
283 -h, --help Show this help and quit''')
284
285
286 # Returns a configuration version showing command object from the
287 # specific command-line arguments `orig_args`.
288 def _show_cfg_version_cfg_from_args(orig_args):
289 # parse original arguments
290 opt_descrs = [
291 barectf_argpar.OptDescr('h', 'help'),
292 ]
293 res = barectf_argpar.parse(orig_args, opt_descrs)
294 assert len(res.ingested_orig_args) == len(orig_args)
295
296 # command help?
297 if len(_find_opt_items(res.items, 'help')) > 0:
298 _show_cfg_version_cmd_usage()
299 sys.exit()
300
301 # check configuration file path
302 cfg_file_path = _cfg_file_path_from_parse_res(res)
303
304 return _ShowCfgVersionCmd(_ShowCfgVersionCmdCfg(cfg_file_path))
305
306
307 def _print_general_usage():
308 print('''Usage: barectf COMMAND COMMAND-ARGS
309 barectf --help
310 barectf --version
311
312 General options:
313 -h, --help Show this help and quit
314 -V, --version Show version and quit
315
316 Available commands:
317 gen:
318 generate:
319 Generate the C source and CTF metadata stream files of a tracer from a
320 configuration file.
321
322 show-effective-configuration:
323 show-effective-config:
324 show-effective-cfg:
325 Print the effective configuration file for a given configuration file and
326 inclusion directories.
327
328 show-configuration-version:
329 show-config-version:
330 show-cfg-version
331 Print the major version of a given configuration file.
332
333 Run `barectf COMMAND --help` to show the help of COMMAND.''')
334
335
336 # Returns a command object from the command-line arguments `orig_args`.
337 #
338 # All the `orig_args` elements are considered.
339 def _cmd_from_args(orig_args):
340 # We use our `argpar` module here instead of Python's `argparse`
341 # because we need to support the two following use cases:
342 #
343 # $ barectf config.yaml
344 # $ barectf generate config.yaml
345 #
346 # In other words, the default command is `generate` (for backward
347 # compatibility reasons). The argument parser must not consider
348 # `config.yaml` as being a command name.
349 general_opt_descrs = [
350 barectf_argpar.OptDescr('V', 'version'),
351 barectf_argpar.OptDescr('h', 'help'),
352 ]
353 res = barectf_argpar.parse(orig_args, general_opt_descrs, False)
354
355 # find command name, collecting preceding (common) option items
356 cmd_from_args_funcs = {
357 'generate': _gen_cmd_cfg_from_args,
358 'gen': _gen_cmd_cfg_from_args,
359 'show-effective-configuration': _show_effective_cfg_cfg_from_args,
360 'show-effective-config': _show_effective_cfg_cfg_from_args,
361 'show-effective-cfg': _show_effective_cfg_cfg_from_args,
362 'show-configuration-version': _show_cfg_version_cfg_from_args,
363 'show-config-version': _show_cfg_version_cfg_from_args,
364 'show-cfg-version': _show_cfg_version_cfg_from_args,
365 }
366 general_opt_items = []
367 cmd_first_orig_arg_index = None
368 cmd_from_args_func = None
369
370 for item in res.items:
371 if type(item) is barectf_argpar._NonOptItem:
372 cmd_from_args_func = cmd_from_args_funcs.get(item.text)
373
374 if cmd_from_args_func is None:
375 cmd_first_orig_arg_index = item.orig_arg_index
376 else:
377 cmd_first_orig_arg_index = item.orig_arg_index + 1
378
379 break
380 else:
381 assert type(item) is barectf_argpar._OptItem
382 general_opt_items.append(item)
383
384 # general help?
385 if len(_find_opt_items(general_opt_items, 'help')) > 0:
386 _print_general_usage()
387 sys.exit()
388
389 # version?
390 if len(_find_opt_items(general_opt_items, 'version')) > 0:
391 print(f'barectf {barectf.__version__}')
392 sys.exit()
393
394 # execute command
395 cmd_orig_args = orig_args[cmd_first_orig_arg_index:]
396
397 if cmd_from_args_func is None:
398 # default `generate` command
399 return _gen_cmd_cfg_from_args(cmd_orig_args)
400 else:
401 return cmd_from_args_func(cmd_orig_args)
402
403
404 class _CmdCfg:
405 pass
406
407
408 class _CfgCmdCfg(_CmdCfg):
409 def __init__(self, cfg_file_path, inclusion_dirs, ignore_inclusion_file_not_found):
410 self._cfg_file_path = cfg_file_path
411 self._inclusion_dirs = inclusion_dirs
412 self._ignore_inclusion_file_not_found = ignore_inclusion_file_not_found
413
414 @property
415 def cfg_file_path(self):
416 return self._cfg_file_path
417
418 @property
419 def inclusion_dirs(self):
420 return self._inclusion_dirs
421
422 @property
423 def ignore_inclusion_file_not_found(self):
424 return self._ignore_inclusion_file_not_found
425
426
427 class _Cmd:
428 def __init__(self, cfg):
429 self._cfg = cfg
430
431 @property
432 def cfg(self):
433 return self._cfg
434
435 def exec(self):
436 raise NotImplementedError
437
438
439 class _GenCmdCfg(_CfgCmdCfg):
440 def __init__(self, cfg_file_path, c_source_dir, c_header_dir, metadata_stream_dir,
441 inclusion_dirs, ignore_inclusion_file_not_found, dump_config, v2_prefix):
442 super().__init__(cfg_file_path, inclusion_dirs, ignore_inclusion_file_not_found)
443 self._c_source_dir = c_source_dir
444 self._c_header_dir = c_header_dir
445 self._metadata_stream_dir = metadata_stream_dir
446 self._dump_config = dump_config
447 self._v2_prefix = v2_prefix
448
449 @property
450 def c_source_dir(self):
451 return self._c_source_dir
452
453 @property
454 def c_header_dir(self):
455 return self._c_header_dir
456
457 @property
458 def metadata_stream_dir(self):
459 return self._metadata_stream_dir
460
461 @property
462 def dump_config(self):
463 return self._dump_config
464
465 @property
466 def v2_prefix(self):
467 return self._v2_prefix
468
469
470 # Source and metadata stream file generating command.
471 class _GenCmd(_Cmd):
472 def exec(self):
473 # create configuration
474 try:
475 with open(self.cfg.cfg_file_path) as f:
476 if self.cfg.dump_config:
477 # print effective configuration file
478 print(barectf.effective_configuration_file(f, True, self.cfg.inclusion_dirs,
479 self.cfg.ignore_inclusion_file_not_found))
480
481 # barectf.configuration_from_file() reads the file again
482 # below: rewind.
483 f.seek(0)
484
485 config = barectf.configuration_from_file(f, True, self.cfg.inclusion_dirs,
486 self.cfg.ignore_inclusion_file_not_found)
487 except barectf._ConfigurationParseError as exc:
488 _print_config_error(exc)
489 except Exception as exc:
490 _print_unknown_exc(exc)
491
492 if self.cfg.v2_prefix is not None:
493 # Override prefixes.
494 #
495 # For historical reasons, the `--prefix` option applies the
496 # barectf 2 configuration prefix rules. Therefore, get the
497 # equivalent barectf 3 prefixes first.
498 v3_prefixes = barectf_config_parse_common._v3_prefixes_from_v2_prefix(self.cfg.v2_prefix)
499 cg_opts = config.options.code_generation_options
500 cg_opts = barectf.ConfigurationCodeGenerationOptions(v3_prefixes.identifier,
501 v3_prefixes.file_name,
502 cg_opts.default_stream_type,
503 cg_opts.header_options,
504 cg_opts.clock_type_c_types)
505 config = barectf.Configuration(config.trace, barectf.ConfigurationOptions(cg_opts))
506
507 # create a barectf code generator
508 code_gen = barectf.CodeGenerator(config)
509
510 def write_file(dir, file):
511 with open(os.path.join(dir, file.name), 'w') as f:
512 f.write(file.contents)
513
514 def write_files(dir, files):
515 for file in files:
516 write_file(dir, file)
517
518 try:
519 # generate and write metadata stream file
520 write_file(self.cfg.metadata_stream_dir, code_gen.generate_metadata_stream())
521
522 # generate and write C header files
523 write_files(self.cfg.c_header_dir, code_gen.generate_c_headers())
524
525 # generate and write C source files
526 write_files(self.cfg.c_source_dir, code_gen.generate_c_sources())
527 except Exception as exc:
528 # We know `config` is valid, therefore the code generator cannot
529 # fail for a reason known to barectf.
530 _print_unknown_exc(exc)
531
532
533 class _ShowEffectiveCfgCmdCfg(_CfgCmdCfg):
534 def __init__(self, cfg_file_path, inclusion_dirs, ignore_inclusion_file_not_found,
535 indent_space_count):
536 super().__init__(cfg_file_path, inclusion_dirs, ignore_inclusion_file_not_found)
537 self._indent_space_count = indent_space_count
538
539 @property
540 def indent_space_count(self):
541 return self._indent_space_count
542
543
544 # Effective configuration showing command.
545 class _ShowEffectiveCfgCmd(_Cmd):
546 def exec(self):
547 try:
548 with open(self.cfg.cfg_file_path) as f:
549 print(barectf.effective_configuration_file(f, True, self.cfg.inclusion_dirs,
550 self.cfg.ignore_inclusion_file_not_found,
551 self.cfg.indent_space_count))
552 except barectf._ConfigurationParseError as exc:
553 _print_config_error(exc)
554 except Exception as exc:
555 _print_unknown_exc(exc)
556
557
558 class _ShowCfgVersionCmdCfg(_CmdCfg):
559 def __init__(self, cfg_file_path):
560 self._cfg_file_path = cfg_file_path
561
562 @property
563 def cfg_file_path(self):
564 return self._cfg_file_path
565
566
567 class _ShowCfgVersionCmd(_Cmd):
568 def exec(self):
569 try:
570 with open(self.cfg.cfg_file_path) as f:
571 print(barectf.configuration_file_major_version(f))
572 except barectf._ConfigurationParseError as exc:
573 _print_config_error(exc)
574 except Exception as exc:
575 _print_unknown_exc(exc)
576
577
578 def _run():
579 # create command from arguments
580 try:
581 cmd = _cmd_from_args(sys.argv[1:])
582 except barectf_argpar._Error as exc:
583 _print_error(f'Command-line: For argument `{exc.orig_arg}`: {exc.msg}')
584 except _CliError as exc:
585 _print_error(f'Command-line: {exc}')
586
587 # execute command
588 cmd.exec()
This page took 0.044313 seconds and 4 git commands to generate.