|
16 | 16 | of available options. |
17 | 17 | """ |
18 | 18 |
|
19 | | -import argparse |
20 | | -import os |
21 | | -import os.path |
22 | | -import shlex |
23 | | -import sys |
| 19 | +import functools |
| 20 | +from pathlib import Path |
| 21 | + |
| 22 | +import click |
| 23 | +from click_option_group import optgroup |
24 | 24 |
|
25 | 25 | import fmf |
26 | 26 | import fmf.utils as utils |
27 | 27 |
|
28 | 28 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
29 | | -# Parser |
| 29 | +# Common option groups |
30 | 30 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
31 | 31 |
|
32 | 32 |
|
33 | | -class Parser: |
34 | | - """ Command line options parser """ |
35 | | - |
36 | | - def __init__(self, arguments=None, path=None): |
37 | | - """ Prepare the parser. """ |
38 | | - # Change current working directory (used for testing) |
39 | | - if path is not None: |
40 | | - os.chdir(path) |
41 | | - # Split command line if given as a string (used for testing) |
42 | | - if isinstance(arguments, str): |
43 | | - self.arguments = shlex.split(arguments) |
44 | | - # Otherwise use sys.argv |
45 | | - if arguments is None: |
46 | | - self.arguments = sys.argv |
47 | | - # Enable debugging output if requested |
48 | | - if "--debug" in self.arguments: |
49 | | - utils.log.setLevel(utils.LOG_DEBUG) |
50 | | - # Show current version and exit |
51 | | - if "--version" in self.arguments: |
52 | | - self.output = f"{fmf.__version__}" |
53 | | - print(self.output) |
54 | | - return |
55 | | - |
56 | | - # Handle subcommands (mapped to format_* methods) |
57 | | - self.parser = argparse.ArgumentParser( |
58 | | - usage="fmf command [options]\n" + __doc__) |
59 | | - self.parser.add_argument( |
60 | | - "--version", action="store_true", |
61 | | - help="print fmf version with commit hash and exit") |
62 | | - self.parser.add_argument('command', help='Command to run') |
63 | | - self.command = self.parser.parse_args(self.arguments[1:2]).command |
64 | | - if not hasattr(self, "command_" + self.command): |
65 | | - self.parser.print_help() |
66 | | - raise utils.GeneralError( |
67 | | - "Unrecognized command: '{0}'".format(self.command)) |
68 | | - # Initialize the rest and run the subcommand |
69 | | - self.output = "" |
70 | | - getattr(self, "command_" + self.command)() |
71 | | - |
72 | | - def options_select(self): |
73 | | - """ Select by name, filter """ |
74 | | - group = self.parser.add_argument_group("Select") |
75 | | - group.add_argument( |
76 | | - "--key", dest="keys", action="append", default=[], |
77 | | - help="Key content definition (required attributes)") |
78 | | - group.add_argument( |
79 | | - "--name", dest="names", action="append", default=[], |
80 | | - help="List objects with name matching regular expression") |
81 | | - group.add_argument( |
82 | | - "--source", dest="sources", action="append", default=[], |
83 | | - help="List objects defined in specified source files") |
84 | | - group.add_argument( |
85 | | - "--filter", dest="filters", action="append", default=[], |
86 | | - help="Apply advanced filter (see 'pydoc fmf.filter')") |
87 | | - group.add_argument( |
88 | | - "--condition", dest="conditions", action="append", default=[], |
89 | | - metavar="EXPR", |
90 | | - help="Use arbitrary Python expression for filtering") |
91 | | - group.add_argument( |
92 | | - "--whole", dest="whole", action="store_true", |
93 | | - help="Consider the whole tree (leaves only by default)") |
94 | | - |
95 | | - def options_formatting(self): |
96 | | - """ Formating options """ |
97 | | - group = self.parser.add_argument_group("Format") |
98 | | - group.add_argument( |
99 | | - "--format", dest="formatting", default=None, |
100 | | - help="Custom output format using the {} expansion") |
101 | | - group.add_argument( |
102 | | - "--value", dest="values", action="append", default=[], |
103 | | - help="Values for the custom formatting string") |
104 | | - |
105 | | - def options_utils(self): |
106 | | - """ Utilities """ |
107 | | - group = self.parser.add_argument_group("Utils") |
108 | | - group.add_argument( |
109 | | - "--path", action="append", dest="paths", |
110 | | - help="Path to the metadata tree (default: current directory)") |
111 | | - group.add_argument( |
112 | | - "--verbose", action="store_true", |
113 | | - help="Print information about parsed files to stderr") |
114 | | - group.add_argument( |
115 | | - "--debug", action="store_true", |
116 | | - help="Turn on debugging output, do not catch exceptions") |
117 | | - |
118 | | - def command_ls(self): |
119 | | - """ List names """ |
120 | | - self.parser = argparse.ArgumentParser( |
121 | | - description="List names of available objects") |
122 | | - self.options_select() |
123 | | - self.options_utils() |
124 | | - self.options = self.parser.parse_args(self.arguments[2:]) |
125 | | - self.show(brief=True) |
126 | | - |
127 | | - def command_clean(self): |
128 | | - """ Clean cache """ |
129 | | - self.parser = argparse.ArgumentParser( |
130 | | - description="Remove cache directory and its content") |
131 | | - self.clean() |
132 | | - |
133 | | - def command_show(self): |
134 | | - """ Show metadata """ |
135 | | - self.parser = argparse.ArgumentParser( |
136 | | - description="Show metadata of available objects") |
137 | | - self.options_select() |
138 | | - self.options_formatting() |
139 | | - self.options_utils() |
140 | | - self.options = self.parser.parse_args(self.arguments[2:]) |
141 | | - self.show(brief=False) |
142 | | - |
143 | | - def command_init(self): |
144 | | - """ Initialize tree """ |
145 | | - self.parser = argparse.ArgumentParser( |
146 | | - description="Initialize a new metadata tree") |
147 | | - self.options_utils() |
148 | | - self.options = self.parser.parse_args(self.arguments[2:]) |
149 | | - # For each path create an .fmf directory and version file |
150 | | - for path in self.options.paths or ["."]: |
151 | | - root = fmf.Tree.init(path) |
152 | | - print("Metadata tree '{0}' successfully initialized.".format(root)) |
153 | | - |
154 | | - def show(self, brief=False): |
155 | | - """ Show metadata for each path given """ |
156 | | - output = [] |
157 | | - for path in self.options.paths or ["."]: |
158 | | - if self.options.verbose: |
159 | | - utils.info("Checking {0} for metadata.".format(path)) |
160 | | - tree = fmf.Tree(path) |
161 | | - for node in tree.prune( |
162 | | - self.options.whole, |
163 | | - self.options.keys, |
164 | | - self.options.names, |
165 | | - self.options.filters, |
166 | | - self.options.conditions, |
167 | | - self.options.sources): |
168 | | - if brief: |
169 | | - show = node.show(brief=True) |
170 | | - else: |
171 | | - show = node.show( |
172 | | - brief=False, |
173 | | - formatting=self.options.formatting, |
174 | | - values=self.options.values) |
175 | | - # List source files when in debug mode |
176 | | - if self.options.debug: |
177 | | - for source in node.sources: |
178 | | - show += utils.color("{0}\n".format(source), "blue") |
179 | | - if show is not None: |
180 | | - output.append(show) |
181 | | - |
182 | | - # Print output and summary |
183 | | - if brief or self.options.formatting: |
184 | | - joined = "".join(output) |
185 | | - else: |
186 | | - joined = "\n".join(output) |
187 | | - print(joined, end="") |
188 | | - if self.options.verbose: |
189 | | - utils.info("Found {0}.".format( |
190 | | - utils.listed(len(output), "object"))) |
191 | | - self.output = joined |
192 | | - |
193 | | - def clean(self): |
194 | | - """ Remove cache directory """ |
195 | | - try: |
196 | | - cache = utils.get_cache_directory(create=False) |
197 | | - utils.clean_cache_directory() |
198 | | - print("Cache directory '{0}' has been removed.".format(cache)) |
199 | | - except Exception as error: # pragma: no cover |
200 | | - utils.log.error( |
201 | | - "Unable to remove cache, exception was: {0}".format(error)) |
| 33 | +def _select_options(func): |
| 34 | + """Select group options""" |
| 35 | + |
| 36 | + @optgroup.group("Select") |
| 37 | + @optgroup.option("--key", "keys", metavar="KEY", default=[], multiple=True, |
| 38 | + help="Key content definition (required attributes)") |
| 39 | + @optgroup.option("--name", "names", metavar="NAME", default=[], multiple=True, |
| 40 | + help="List objects with name matching regular expression") |
| 41 | + @optgroup.option("--source", "sources", metavar="SOURCE", default=[], multiple=True, |
| 42 | + help="List objects defined in specified source files") |
| 43 | + @optgroup.option("--filter", "filters", metavar="FILTER", default=[], multiple=True, |
| 44 | + help="Apply advanced filter (see 'pydoc fmf.filter')") |
| 45 | + @optgroup.option("--condition", "conditions", metavar="EXPR", default=[], multiple=True, |
| 46 | + help="Use arbitrary Python expression for filtering") |
| 47 | + @optgroup.option("--whole", is_flag=True, default=False, |
| 48 | + help="Consider the whole tree (leaves only by default)") |
| 49 | + @functools.wraps(func) |
| 50 | + def wrapper(*args, **kwargs): |
| 51 | + # Hack to group the options into one variable |
| 52 | + select = { |
| 53 | + opt: kwargs.pop(opt) |
| 54 | + for opt in ("keys", "names", "sources", "filters", "conditions", "whole") |
| 55 | + } |
| 56 | + return func(*args, select=select, **kwargs) |
| 57 | + |
| 58 | + return wrapper |
| 59 | + |
| 60 | + |
| 61 | +def _format_options(func): |
| 62 | + """Formating group options""" |
| 63 | + |
| 64 | + @optgroup.group("Format") |
| 65 | + @optgroup.option("--format", "formatting", metavar="FORMAT", default=None, |
| 66 | + help="Custom output format using the {} expansion") |
| 67 | + @optgroup.option("--value", "values", metavar="VALUE", default=[], multiple=True, |
| 68 | + help="Values for the custom formatting string") |
| 69 | + @functools.wraps(func) |
| 70 | + def wrapper(*args, **kwargs): |
| 71 | + # Hack to group the options into one variable |
| 72 | + format = { |
| 73 | + opt: kwargs.pop(opt) |
| 74 | + for opt in ("formatting", "values") |
| 75 | + } |
| 76 | + return func(*args, format=format, **kwargs) |
| 77 | + |
| 78 | + return wrapper |
| 79 | + |
| 80 | + |
| 81 | +def _utils_options(func): |
| 82 | + """Utilities group options""" |
| 83 | + |
| 84 | + @optgroup.group("Utils") |
| 85 | + @optgroup.option("--path", "paths", metavar="PATH", multiple=True, |
| 86 | + type=Path, default=["."], |
| 87 | + show_default="current directory", |
| 88 | + help="Path to the metadata tree") |
| 89 | + @functools.wraps(func) |
| 90 | + def wrapper(*args, **kwargs): |
| 91 | + return func(*args, **kwargs) |
| 92 | + |
| 93 | + return wrapper |
202 | 94 |
|
203 | 95 |
|
204 | 96 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
205 | 97 | # Main |
206 | 98 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
207 | | - |
208 | | -def main(arguments=None, path=None): |
209 | | - """ Parse options, do what is requested """ |
210 | | - parser = Parser(arguments, path) |
211 | | - return parser.output |
| 99 | +class CatchAllExceptions(click.Group): |
| 100 | + def __call__(self, *args, **kwargs): |
| 101 | + # TODO: This actually has no effect |
| 102 | + try: |
| 103 | + return self.main(*args, **kwargs) |
| 104 | + except fmf.utils.GeneralError as error: |
| 105 | + # TODO: Better handling of --debug |
| 106 | + if "--debug" not in kwargs: |
| 107 | + fmf.utils.log.error(error) |
| 108 | + raise |
| 109 | + |
| 110 | + |
| 111 | +@click.group("fmf", cls=CatchAllExceptions) |
| 112 | +@click.version_option(fmf.__version__, message="%(version)s") |
| 113 | +@click.option("--verbose", is_flag=True, default=False, type=bool, |
| 114 | + help="Print information about parsed files to stderr") |
| 115 | +@click.option("--debug", "-d", count=True, default=0, type=int, |
| 116 | + help="Provide debugging information. Repeat to see more details.") |
| 117 | +@click.pass_context |
| 118 | +def main(ctx, debug, verbose) -> None: |
| 119 | + """This is command line interface for the Flexible Metadata Format.""" |
| 120 | + ctx.ensure_object(dict) |
| 121 | + if debug: |
| 122 | + utils.log.setLevel(debug) |
| 123 | + ctx.obj["verbose"] = verbose |
| 124 | + ctx.obj["debug"] = debug |
212 | 125 |
|
213 | 126 |
|
214 | | -def cli_entry(): |
| 127 | +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 128 | +# Sub-commands |
| 129 | +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 130 | +@main.command("ls") |
| 131 | +@_select_options |
| 132 | +@_utils_options |
| 133 | +@click.pass_context |
| 134 | +def ls(ctx, paths, select) -> None: |
| 135 | + """List names of available objects""" |
| 136 | + _show(ctx, paths, select, brief=True) |
| 137 | + |
| 138 | + |
| 139 | +@main.command("clean") |
| 140 | +def clean() -> None: |
| 141 | + """Remove cache directory and its content""" |
| 142 | + _clean() |
| 143 | + |
| 144 | + |
| 145 | +@main.command("show") |
| 146 | +@_select_options |
| 147 | +@_format_options |
| 148 | +@_utils_options |
| 149 | +@click.pass_context |
| 150 | +def show(ctx, paths, select, format) -> None: |
| 151 | + """Show metadata of available objects""" |
| 152 | + _show(ctx, paths, select, format_opts=format, brief=False) |
| 153 | + |
| 154 | + |
| 155 | +@main.command("init") |
| 156 | +@_utils_options |
| 157 | +def init(paths) -> None: |
| 158 | + """Initialize a new metadata tree""" |
| 159 | + # For each path create an .fmf directory and version file |
| 160 | + for path in paths: |
| 161 | + root = fmf.Tree.init(path) |
| 162 | + click.echo("Metadata tree '{0}' successfully initialized.".format(root)) |
| 163 | + |
| 164 | + |
| 165 | +def _show(ctx, paths, select_opts, format_opts=None, brief=False): |
| 166 | + """ Show metadata for each path given """ |
| 167 | + output = [] |
| 168 | + for path in paths: |
| 169 | + if ctx.obj["verbose"]: |
| 170 | + utils.info("Checking {0} for metadata.".format(path)) |
| 171 | + tree = fmf.Tree(path) |
| 172 | + for node in tree.prune(**select_opts): |
| 173 | + if brief: |
| 174 | + show = node.show(brief=True) |
| 175 | + else: |
| 176 | + assert format_opts is not None |
| 177 | + show = node.show(brief=False, **format_opts) |
| 178 | + # List source files when in debug mode |
| 179 | + if ctx.obj["debug"]: |
| 180 | + for source in node.sources: |
| 181 | + show += utils.color("{0}\n".format(source), "blue") |
| 182 | + if show is not None: |
| 183 | + output.append(show) |
| 184 | + |
| 185 | + # Print output and summary |
| 186 | + if brief or format_opts and format_opts["formatting"]: |
| 187 | + joined = "".join(output) |
| 188 | + else: |
| 189 | + joined = "\n".join(output) |
| 190 | + click.echo(joined, nl=False) |
| 191 | + if ctx.obj["verbose"]: |
| 192 | + utils.info("Found {0}.".format( |
| 193 | + utils.listed(len(output), "object"))) |
| 194 | + |
| 195 | + |
| 196 | +def _clean(): |
| 197 | + """Remove cache directory""" |
215 | 198 | try: |
216 | | - main() |
217 | | - except fmf.utils.GeneralError as error: |
218 | | - if "--debug" not in sys.argv: |
219 | | - fmf.utils.log.error(error) |
220 | | - raise |
| 199 | + cache = utils.get_cache_directory(create=False) |
| 200 | + utils.clean_cache_directory() |
| 201 | + click.echo("Cache directory '{0}' has been removed.".format(cache)) |
| 202 | + except Exception as error: # pragma: no cover |
| 203 | + utils.log.error( |
| 204 | + "Unable to remove cache, exception was: {0}".format(error)) |
0 commit comments