1 | #!/usr/bin/env python |
||
2 | |||
3 | 1 | """Command-line interface for Doorstop.""" |
|
4 | |||
5 | 1 | import os |
|
6 | 1 | import sys |
|
7 | 1 | import argparse |
|
8 | |||
9 | 1 | from doorstop import common, settings |
|
10 | 1 | from doorstop.cli import utilities, commands |
|
11 | 1 | from doorstop.core import publisher, vcs, document |
|
12 | |||
13 | 1 | log = common.logger(__name__) |
|
14 | |||
15 | |||
16 | 1 | def main(args=None): # pylint: disable=R0915 |
|
17 | """Process command-line arguments and run the program.""" |
||
18 | 1 | from doorstop import CLI, VERSION, DESCRIPTION |
|
19 | |||
20 | # Shared options |
||
21 | 1 | project = argparse.ArgumentParser(add_help=False) |
|
22 | 1 | try: |
|
23 | 1 | root = vcs.find_root(os.getcwd()) |
|
24 | 1 | except common.DoorstopError: |
|
25 | 1 | root = None |
|
26 | 1 | project.add_argument('-j', '--project', metavar='PATH', |
|
27 | help="path to the root of the project", |
||
28 | default=root) |
||
29 | 1 | project.add_argument('--no-cache', action='store_true', |
|
30 | help=argparse.SUPPRESS) |
||
31 | 1 | project.add_argument('-b', '--beta', nargs='*', |
|
32 | 1 | help="""enable beta features. Refer to documentation on available beta features. """) |
|
33 | server = argparse.ArgumentParser(add_help=False) |
||
34 | server.add_argument('--server', metavar='HOST', |
||
35 | 1 | help="IP address or hostname for a running server", |
|
36 | default=settings.SERVER_HOST) |
||
37 | server.add_argument('--port', metavar='NUMBER', type=int, |
||
38 | 1 | help="use a custom port for the server", |
|
39 | default=settings.SERVER_PORT) |
||
40 | 1 | server.add_argument('-f', '--force', action='store_true', |
|
41 | 1 | help="perform the action without the server") |
|
42 | 1 | debug = argparse.ArgumentParser(add_help=False) |
|
43 | 1 | debug.add_argument('-V', '--version', action='version', version=VERSION) |
|
44 | group = debug.add_mutually_exclusive_group() |
||
45 | 1 | group.add_argument('-v', '--verbose', action='count', default=0, |
|
46 | help="enable verbose logging") |
||
47 | 1 | group.add_argument('-q', '--quiet', action='store_const', const=-1, |
|
48 | dest='verbose', help="only display errors and prompts") |
||
49 | shared = {'formatter_class': common.HelpFormatter, |
||
50 | 'parents': [project, server, debug]} |
||
51 | 1 | ||
52 | # Build main parser |
||
53 | 1 | parser = argparse.ArgumentParser(prog=CLI, description=DESCRIPTION, |
|
54 | **shared) |
||
55 | 1 | parser.add_argument('-F', '--no-reformat', action='store_true', |
|
56 | help="do not reformat item files during validation") |
||
57 | 1 | parser.add_argument('-r', '--reorder', action='store_true', |
|
58 | help="reorder document levels during validation") |
||
59 | 1 | parser.add_argument('-L', '--no-level-check', action='store_true', |
|
60 | help="do not validate document levels") |
||
61 | 1 | parser.add_argument('-R', '--no-ref-check', action='store_true', |
|
62 | help="do not validate external file references") |
||
63 | 1 | parser.add_argument('-C', '--no-child-check', action='store_true', |
|
64 | help="do not validate child (reverse) links") |
||
65 | 1 | parser.add_argument('-Z', '--strict-child-check', action='store_true', |
|
66 | help="require child (reverse) links from every document") |
||
67 | 1 | parser.add_argument('-S', '--no-suspect-check', action='store_true', |
|
68 | help="do not check for suspect links") |
||
69 | 1 | parser.add_argument('-W', '--no-review-check', action='store_true', |
|
70 | help="do not check item review status") |
||
71 | 1 | parser.add_argument('-s', '--skip', metavar='PREFIX', action='append', |
|
72 | help="skip a document during validation") |
||
73 | 1 | parser.add_argument('-w', '--warn-all', action='store_true', |
|
74 | help="display all info-level issues as warnings") |
||
75 | parser.add_argument('-e', '--error-all', action='store_true', |
||
76 | help="display all warning-level issues as errors") |
||
77 | 1 | ||
78 | 1 | # Build sub-parsers |
|
79 | 1 | subs = parser.add_subparsers(help="", dest='command', metavar="<command>") |
|
80 | 1 | _create(subs, shared) |
|
81 | 1 | _delete(subs, shared) |
|
82 | 1 | _add(subs, shared) |
|
83 | 1 | _remove(subs, shared) |
|
84 | 1 | _edit(subs, shared) |
|
85 | 1 | _reorder(subs, shared) |
|
86 | 1 | _link(subs, shared) |
|
87 | 1 | _unlink(subs, shared) |
|
88 | 1 | _clear(subs, shared) |
|
89 | 1 | _review(subs, shared) |
|
90 | 1 | _import(subs, shared) |
|
91 | _export(subs, shared) |
||
92 | _publish(subs, shared) |
||
93 | 1 | ||
94 | # Parse arguments |
||
95 | args = parser.parse_args(args=args) |
||
96 | 1 | ||
97 | # Configure logging |
||
98 | utilities.configure_logging(args.verbose) |
||
99 | 1 | ||
100 | # Configure settings |
||
101 | utilities.configure_settings(args) |
||
102 | 1 | ||
103 | 1 | # Run the program |
|
104 | 1 | function = commands.get(args.command) |
|
105 | 1 | try: |
|
106 | 1 | success = function(args, os.getcwd(), parser.error) |
|
107 | 1 | except common.DoorstopFileError as exc: |
|
108 | 1 | log.error(exc) |
|
109 | 1 | success = False |
|
110 | 1 | except KeyboardInterrupt: |
|
111 | 1 | log.debug("command cancelled") |
|
112 | 1 | success = False |
|
113 | if success: |
||
114 | 1 | log.debug("command succeeded") |
|
115 | 1 | else: |
|
116 | log.debug("command failed") |
||
117 | sys.exit(1) |
||
118 | 1 | ||
119 | |||
120 | 1 | def _create(subs, shared): |
|
121 | 1 | """Configure the `doorstop create` subparser.""" |
|
122 | info = "create a new document directory" |
||
123 | 1 | sub = subs.add_parser('create', description=info.capitalize() + '.', |
|
124 | 1 | help=info, **shared) |
|
125 | 1 | sub.add_argument('prefix', help="document prefix for new item UIDs") |
|
126 | 1 | sub.add_argument('path', help="path to a directory for item files") |
|
127 | sub.add_argument('-p', '--parent', help="prefix of parent document") |
||
128 | sub.add_argument('-d', '--digits', help="number of digits in item UIDs", |
||
129 | default=document.Document.DEFAULT_DIGITS) |
||
130 | 1 | ||
131 | |||
132 | 1 | def _delete(subs, shared): |
|
133 | 1 | """Configure the `doorstop delete` subparser.""" |
|
134 | info = "delete a document directory" |
||
135 | 1 | sub = subs.add_parser('delete', description=info.capitalize() + '.', |
|
136 | help=info, **shared) |
||
137 | sub.add_argument('prefix', help="prefix of document to delete") |
||
138 | 1 | ||
139 | |||
140 | 1 | def _add(subs, shared): |
|
141 | 1 | """Configure the `doorstop add` subparser.""" |
|
142 | info = "create an item file in a document directory" |
||
143 | 1 | sub = subs.add_parser('add', description=info.capitalize() + '.', |
|
144 | help=info, **shared) |
||
145 | 1 | sub.add_argument('prefix', |
|
146 | 1 | help="document prefix for the new item") |
|
147 | sub.add_argument('-l', '--level', help="desired item level (e.g. 1.2.3)") |
||
148 | sub.add_argument('-c', '--count', default=1, type=utilities.positive_int, |
||
149 | help="number of items to create") |
||
150 | 1 | ||
151 | |||
152 | 1 | def _remove(subs, shared): |
|
153 | 1 | """Configure the `doorstop remove` subparser.""" |
|
154 | info = "remove an item file from a document directory" |
||
155 | 1 | sub = subs.add_parser('remove', description=info.capitalize() + '.', |
|
156 | help=info, **shared) |
||
157 | sub.add_argument('uid', help="item UID to remove from its document") |
||
158 | 1 | View Code Duplication | |
0 ignored issues
–
show
Duplication
introduced
Loading history...
|
|||
159 | |||
160 | 1 | def _edit(subs, shared): |
|
161 | 1 | """Configure the `doorstop edit` subparser.""" |
|
162 | info = "open an existing item or document for editing" |
||
163 | 1 | sub = subs.add_parser('edit', description=info.capitalize() + '.', |
|
164 | help=info, **shared) |
||
165 | 1 | sub.add_argument('label', |
|
166 | 1 | help="item UID or document prefix to open for editing") |
|
167 | group = sub.add_mutually_exclusive_group() |
||
168 | 1 | group.add_argument('-i', '--item', action='store_true', |
|
169 | help="indicates the 'label' is an item UID") |
||
170 | 1 | group.add_argument('-d', '--document', action='store_true', |
|
171 | 1 | help="indicates the 'label' is a document prefix") |
|
172 | group = sub.add_mutually_exclusive_group() |
||
173 | 1 | group.add_argument('-y', '--yaml', action='store_true', |
|
174 | help="edit document as exported YAML (default)") |
||
175 | 1 | group.add_argument('-c', '--csv', action='store_true', |
|
176 | help="edit document as exported CSV") |
||
177 | 1 | group.add_argument('-t', '--tsv', action='store_true', |
|
178 | help="edit document as exported TSV") |
||
179 | 1 | group.add_argument('-x', '--xlsx', action='store_true', |
|
180 | 1 | help="edit document as exported XLSX") |
|
181 | required = sub.add_argument_group('required arguments') |
||
182 | required.add_argument('-T', '--tool', metavar='PROGRAM', |
||
183 | help="text editor to open the document item", |
||
184 | required=True) |
||
185 | 1 | ||
186 | |||
187 | 1 | def _reorder(subs, shared): |
|
188 | 1 | """Configure the `doorstop reorder` subparser.""" |
|
189 | info = "organize the outline structure of a document" |
||
190 | 1 | sub = subs.add_parser('reorder', description=info.capitalize() + '.', |
|
191 | 1 | help=info, **shared) |
|
192 | 1 | sub.add_argument('prefix', help="prefix of document to reorder") |
|
193 | group = sub.add_mutually_exclusive_group() |
||
194 | 1 | group.add_argument('-a', '--auto', action='store_true', |
|
195 | help="only perform automatic item reordering") |
||
196 | 1 | group.add_argument('-m', '--manual', action='store_true', |
|
197 | help="do not automatically reorder the items") |
||
198 | sub.add_argument('-T', '--tool', metavar='PROGRAM', |
||
199 | help="text editor to open the document index") |
||
200 | 1 | ||
201 | |||
202 | 1 | def _link(subs, shared): |
|
203 | 1 | """Configure the `doorstop link` subparser.""" |
|
204 | info = "add a new link between two items" |
||
205 | 1 | sub = subs.add_parser('link', description=info.capitalize() + '.', |
|
206 | help=info, **shared) |
||
207 | 1 | sub.add_argument('child', |
|
208 | help="child item UID to link to the parent") |
||
209 | sub.add_argument('parent', |
||
210 | help="parent item UID to link from the child") |
||
211 | 1 | ||
212 | |||
213 | 1 | def _unlink(subs, shared): |
|
214 | 1 | """Configure the `doorstop unlink` subparser.""" |
|
215 | info = "remove a link between two items" |
||
216 | 1 | sub = subs.add_parser('unlink', description=info.capitalize() + '.', |
|
217 | help=info, **shared) |
||
218 | 1 | sub.add_argument('child', |
|
219 | help="child item UID to unlink from parent") |
||
220 | sub.add_argument('parent', |
||
221 | help="parent item UID child is linked to") |
||
222 | 1 | ||
223 | |||
224 | 1 | def _clear(subs, shared): |
|
225 | 1 | """Configure the `doorstop clear` subparser.""" |
|
226 | info = "absolve items of their suspect link status" |
||
227 | 1 | sub = subs.add_parser('clear', description=info.capitalize() + '.', |
|
228 | 1 | help=info, **shared) |
|
229 | 1 | sub.add_argument('label', help="item UID, document prefix, or 'all'") |
|
230 | group = sub.add_mutually_exclusive_group() |
||
231 | 1 | group.add_argument('-i', '--item', action='store_true', |
|
232 | help="indicates the 'label' is an item UID") |
||
233 | group.add_argument('-d', '--document', action='store_true', |
||
234 | help="indicates the 'label' is a document prefix") |
||
235 | 1 | ||
236 | |||
237 | 1 | def _review(subs, shared): |
|
238 | 1 | """Configure the `doorstop review` subparser.""" |
|
239 | info = "absolve items of their unreviewed status" |
||
240 | 1 | sub = subs.add_parser('review', description=info.capitalize() + '.', |
|
241 | 1 | help=info, **shared) |
|
242 | 1 | sub.add_argument('label', help="item UID, document prefix, or 'all'") |
|
243 | group = sub.add_mutually_exclusive_group() |
||
244 | 1 | group.add_argument('-i', '--item', action='store_true', |
|
245 | help="indicates the 'label' is an item UID") |
||
246 | group.add_argument('-d', '--document', action='store_true', |
||
247 | help="indicates the 'label' is a document prefix") |
||
248 | 1 | View Code Duplication | |
0 ignored issues
–
show
|
|||
249 | |||
250 | 1 | def _import(subs, shared): |
|
251 | 1 | """Configure the `doorstop import` subparser.""" |
|
252 | info = "import an existing document or item" |
||
253 | 1 | sub = subs.add_parser('import', description=info.capitalize() + '.', |
|
254 | help=info, **shared) |
||
255 | 1 | sub.add_argument('path', nargs='?', |
|
256 | 1 | help="path to previously exported document file") |
|
257 | 1 | sub.add_argument('prefix', nargs='?', help="prefix of document for import") |
|
258 | group = sub.add_mutually_exclusive_group() |
||
259 | 1 | group.add_argument('-d', '--document', nargs=2, metavar='ARG', |
|
260 | help="import an existing document by: PREFIX PATH") |
||
261 | 1 | group.add_argument('-i', '--item', nargs=2, metavar='ARG', |
|
262 | help="import an existing item by: PREFIX UID") |
||
263 | 1 | sub.add_argument('-p', '--parent', metavar='PREFIX', |
|
264 | help="parent document prefix for imported document") |
||
265 | 1 | sub.add_argument('-a', '--attrs', metavar='DICT', |
|
266 | help="dictionary of item attributes to import") |
||
267 | sub.add_argument('-m', '--map', metavar='DICT', |
||
268 | help="dictionary of custom item attribute names") |
||
269 | 1 | ||
270 | |||
271 | 1 | def _export(subs, shared): |
|
272 | 1 | """Configure the `doorstop export` subparser.""" |
|
273 | info = "export a document as YAML or another format" |
||
274 | 1 | sub = subs.add_parser('export', description=info.capitalize() + '.', |
|
275 | 1 | help=info, **shared) |
|
276 | sub.add_argument('prefix', help="prefix of document to export or 'all'") |
||
277 | 1 | sub.add_argument('path', nargs='?', |
|
278 | 1 | help="path to exported file or directory for 'all'") |
|
279 | group = sub.add_mutually_exclusive_group() |
||
280 | 1 | group.add_argument('-y', '--yaml', action='store_true', |
|
281 | help="output YAML (default when no path)") |
||
282 | 1 | group.add_argument('-c', '--csv', action='store_true', |
|
283 | help="output CSV (default for 'all')") |
||
284 | 1 | group.add_argument('-t', '--tsv', action='store_true', |
|
285 | help="output TSV") |
||
286 | 1 | group.add_argument('-x', '--xlsx', action='store_true', |
|
287 | help="output XLSX") |
||
288 | sub.add_argument('-w', '--width', type=int, |
||
289 | help="limit line width on text output") |
||
290 | 1 | ||
291 | |||
292 | 1 | def _publish(subs, shared): |
|
293 | 1 | """Configure the `doorstop publish` subparser.""" |
|
294 | info = "publish a document as text or another format" |
||
295 | 1 | sub = subs.add_parser('publish', description=info.capitalize() + '.', |
|
296 | 1 | help=info, **shared) |
|
297 | sub.add_argument('prefix', help="prefix of document to publish or 'all'") |
||
298 | 1 | sub.add_argument('path', nargs='?', |
|
299 | 1 | help="path to published file or directory for 'all'") |
|
300 | group = sub.add_mutually_exclusive_group() |
||
301 | 1 | group.add_argument('-t', '--text', action='store_true', |
|
302 | help="output text (default when no path)") |
||
303 | 1 | group.add_argument('-m', '--markdown', action='store_true', |
|
304 | help="output Markdown") |
||
305 | 1 | group.add_argument('-H', '--html', action='store_true', |
|
306 | help="output HTML (default for 'all')") |
||
307 | 1 | sub.add_argument('-w', '--width', type=int, |
|
308 | help="limit line width on text output") |
||
309 | 1 | sub.add_argument('-C', '--no-child-links', action='store_true', |
|
310 | help="do not include child links on items") |
||
311 | sub.add_argument('-L', '--no-body-levels', action='store_true', |
||
312 | 1 | default=None, |
|
313 | help="do not include levels on non-heading items") |
||
314 | 1 | sub.add_argument('--no-levels', choices=['all', 'body'], |
|
315 | help="do not include levels on heading and non-heading or non-heading items") |
||
316 | sub.add_argument('--template', help="template file", default=publisher.HTMLTEMPLATE) |
||
317 | |||
318 | |||
319 | if __name__ == '__main__': # pragma: no cover (manual test) |
||
320 | main() |
||
321 |