1 | #!/usr/bin/env python |
||
2 | # SPDX-License-Identifier: LGPL-3.0-only |
||
3 | 1 | ||
4 | """Command-line interface for Doorstop.""" |
||
5 | 1 | ||
6 | 1 | import argparse |
|
7 | 1 | import os |
|
8 | import sys |
||
9 | 1 | ||
10 | 1 | from doorstop import common, settings |
|
11 | 1 | from doorstop.cli import commands, utilities |
|
12 | from doorstop.core import document, publisher, vcs |
||
13 | 1 | ||
14 | log = common.logger(__name__) |
||
15 | |||
16 | 1 | EDITOR = os.environ.get('EDITOR') |
|
17 | |||
18 | 1 | ||
19 | def main(args=None): # pylint: disable=R0915 |
||
20 | """Process command-line arguments and run the program.""" |
||
21 | 1 | from doorstop import CLI, VERSION, DESCRIPTION |
|
22 | 1 | ||
23 | 1 | # Shared options |
|
24 | 1 | project = argparse.ArgumentParser(add_help=False) |
|
25 | 1 | try: |
|
26 | 1 | root = vcs.find_root(os.getcwd()) |
|
27 | except common.DoorstopError: |
||
28 | root = None |
||
29 | 1 | project.add_argument( |
|
30 | '-j', |
||
31 | 1 | '--project', |
|
32 | 1 | metavar='PATH', |
|
33 | help="path to the root of the project", |
||
34 | default=root, |
||
35 | 1 | ) |
|
36 | project.add_argument('--no-cache', action='store_true', help=argparse.SUPPRESS) |
||
37 | project.add_argument( |
||
38 | 1 | '-b', |
|
39 | '--beta', |
||
40 | 1 | nargs='*', |
|
41 | 1 | help="""enable beta features. Refer to documentation on available beta features. """, |
|
42 | 1 | ) |
|
43 | 1 | server = argparse.ArgumentParser(add_help=False) |
|
44 | server.add_argument( |
||
45 | 1 | '--server', |
|
46 | metavar='HOST', |
||
47 | 1 | help="IP address or hostname for a running server", |
|
48 | default=settings.SERVER_HOST, |
||
49 | ) |
||
50 | server.add_argument( |
||
51 | 1 | '--port', |
|
52 | metavar='NUMBER', |
||
53 | 1 | type=int, |
|
54 | help="use a custom port for the server", |
||
55 | 1 | default=settings.SERVER_PORT, |
|
56 | ) |
||
57 | 1 | server.add_argument( |
|
58 | '-f', |
||
59 | 1 | '--force', |
|
60 | action='store_true', |
||
61 | 1 | help="perform the action without the server", |
|
62 | ) |
||
63 | 1 | debug = argparse.ArgumentParser(add_help=False) |
|
64 | debug.add_argument('-V', '--version', action='version', version=VERSION) |
||
65 | 1 | group = debug.add_mutually_exclusive_group() |
|
66 | group.add_argument( |
||
67 | 1 | '-v', '--verbose', action='count', default=0, help="enable verbose logging" |
|
68 | ) |
||
69 | 1 | group.add_argument( |
|
70 | '-q', |
||
71 | 1 | '--quiet', |
|
72 | action='store_const', |
||
73 | 1 | const=-1, |
|
74 | dest='verbose', |
||
75 | help="only display errors and prompts", |
||
76 | ) |
||
77 | 1 | shared = { |
|
78 | 1 | 'formatter_class': common.HelpFormatter, |
|
79 | 1 | 'parents': [project, server, debug], |
|
80 | 1 | } |
|
81 | 1 | ||
82 | 1 | # Build main parser |
|
83 | 1 | parser = argparse.ArgumentParser(prog=CLI, description=DESCRIPTION, **shared) |
|
84 | 1 | parser.add_argument( |
|
85 | 1 | '-F', |
|
86 | 1 | '--no-reformat', |
|
87 | 1 | action='store_true', |
|
88 | 1 | help="do not reformat item files during validation", |
|
89 | 1 | ) |
|
90 | 1 | parser.add_argument( |
|
91 | '-r', |
||
92 | '--reorder', |
||
93 | 1 | action='store_true', |
|
94 | help="reorder document levels during validation", |
||
95 | ) |
||
96 | 1 | parser.add_argument( |
|
97 | '-L', |
||
98 | '--no-level-check', |
||
99 | 1 | action='store_true', |
|
100 | help="do not validate document levels", |
||
101 | ) |
||
102 | 1 | parser.add_argument( |
|
103 | 1 | '-R', |
|
104 | 1 | '--no-ref-check', |
|
105 | 1 | action='store_true', |
|
106 | 1 | help="do not validate external file references", |
|
107 | 1 | ) |
|
108 | 1 | parser.add_argument( |
|
109 | 1 | '-C', |
|
110 | 1 | '--no-child-check', |
|
111 | 1 | action='store_true', |
|
112 | 1 | help="do not validate child (reverse) links", |
|
113 | ) |
||
114 | 1 | parser.add_argument( |
|
115 | 1 | '-Z', |
|
116 | '--strict-child-check', |
||
117 | action='store_true', |
||
118 | 1 | help="require child (reverse) links from every document", |
|
119 | ) |
||
120 | 1 | parser.add_argument( |
|
121 | 1 | '-S', |
|
122 | '--no-suspect-check', |
||
123 | 1 | action='store_true', |
|
124 | 1 | help="do not check for suspect links", |
|
125 | 1 | ) |
|
126 | 1 | parser.add_argument( |
|
127 | '-W', |
||
128 | '--no-review-check', |
||
129 | action='store_true', |
||
130 | 1 | help="do not check item review status", |
|
131 | ) |
||
132 | 1 | parser.add_argument( |
|
133 | 1 | '-s', |
|
134 | '--skip', |
||
135 | 1 | metavar='PREFIX', |
|
136 | action='append', |
||
137 | help="skip a document during validation", |
||
138 | 1 | ) |
|
139 | parser.add_argument( |
||
140 | 1 | '-w', |
|
141 | 1 | '--warn-all', |
|
142 | action='store_true', |
||
143 | 1 | help="display all info-level issues as warnings", |
|
144 | ) |
||
145 | 1 | parser.add_argument( |
|
146 | 1 | '-e', |
|
147 | '--error-all', |
||
148 | action='store_true', |
||
149 | help="display all warning-level issues as errors", |
||
150 | 1 | ) |
|
151 | |||
152 | 1 | # Build sub-parsers |
|
153 | 1 | subs = parser.add_subparsers(help="", dest='command', metavar="<command>") |
|
154 | _create(subs, shared) |
||
155 | 1 | _delete(subs, shared) |
|
156 | _add(subs, shared) |
||
157 | _remove(subs, shared) |
||
158 | 1 | _edit(subs, shared) |
|
159 | _reorder(subs, shared) |
||
160 | 1 | _link(subs, shared) |
|
161 | 1 | _unlink(subs, shared) |
|
162 | _clear(subs, shared) |
||
163 | 1 | _review(subs, shared) |
|
164 | _import(subs, shared) |
||
165 | 1 | _export(subs, shared) |
|
166 | 1 | _publish(subs, shared) |
|
167 | |||
168 | 1 | # Parse arguments |
|
169 | args = parser.parse_args(args=args) |
||
170 | 1 | ||
171 | 1 | # Configure logging |
|
172 | utilities.configure_logging(args.verbose) |
||
173 | 1 | ||
174 | # Configure settings |
||
175 | 1 | utilities.configure_settings(args) |
|
176 | |||
177 | 1 | # Run the program |
|
178 | function = commands.get(args.command) |
||
179 | 1 | try: |
|
180 | 1 | success = function(args, os.getcwd(), parser.error) |
|
181 | except common.DoorstopFileError as exc: |
||
182 | log.error(exc) |
||
183 | success = False |
||
184 | except KeyboardInterrupt: |
||
185 | 1 | log.debug("command cancelled") |
|
186 | success = False |
||
187 | 1 | if success: |
|
188 | 1 | log.debug("command succeeded") |
|
189 | else: |
||
190 | 1 | log.debug("command failed") |
|
191 | 1 | sys.exit(1) |
|
192 | 1 | ||
193 | |||
194 | 1 | def _create(subs, shared): |
|
195 | """Configure the `doorstop create` subparser.""" |
||
196 | 1 | info = "create a new document directory" |
|
197 | sub = subs.add_parser( |
||
198 | 'create', description=info.capitalize() + '.', help=info, **shared |
||
199 | ) |
||
200 | 1 | sub.add_argument('prefix', help="document prefix for new item UIDs") |
|
201 | sub.add_argument('path', help="path to a directory for item files") |
||
202 | 1 | sub.add_argument('-p', '--parent', help="prefix of parent document") |
|
203 | 1 | sub.add_argument( |
|
204 | '-d', |
||
205 | 1 | '--digits', |
|
206 | help="number of digits in item UIDs", |
||
207 | 1 | default=document.Document.DEFAULT_DIGITS, |
|
208 | ) |
||
209 | |||
210 | |||
211 | 1 | def _delete(subs, shared): |
|
212 | """Configure the `doorstop delete` subparser.""" |
||
213 | 1 | info = "delete a document directory" |
|
214 | 1 | sub = subs.add_parser( |
|
215 | 'delete', description=info.capitalize() + '.', help=info, **shared |
||
216 | 1 | ) |
|
217 | sub.add_argument('prefix', help="prefix of document to delete") |
||
218 | 1 | ||
219 | |||
220 | def _add(subs, shared): |
||
221 | """Configure the `doorstop add` subparser.""" |
||
222 | 1 | info = "create an item file in a document directory" |
|
223 | sub = subs.add_parser( |
||
224 | 1 | 'add', description=info.capitalize() + '.', help=info, **shared |
|
225 | 1 | ) |
|
226 | sub.add_argument('prefix', help="document prefix for the new item") |
||
227 | 1 | sub.add_argument('-l', '--level', help="desired item level (e.g. 1.2.3)") |
|
228 | 1 | sub.add_argument( |
|
229 | 1 | '-c', |
|
230 | '--count', |
||
231 | 1 | default=1, |
|
232 | type=utilities.positive_int, |
||
233 | help="number of items to create", |
||
234 | ) |
||
235 | 1 | sub.add_argument( |
|
236 | '--edit', |
||
237 | 1 | action='store_true', |
|
238 | 1 | help=( |
|
239 | "Open default editor to edit the added item. " |
||
240 | 1 | "Default editor can be set using the environment " |
|
241 | 1 | "variable EDITOR." |
|
242 | 1 | ), |
|
243 | ) |
||
244 | 1 | sub.add_argument( |
|
245 | '-T', |
||
246 | '--tool', |
||
247 | metavar='PROGRAM', |
||
248 | 1 | default=EDITOR, |
|
249 | help=( |
||
250 | 1 | "text editor to open the document item (only" |
|
251 | 1 | "required if $EDITOR is not found in" |
|
252 | "environment). Useless option without --edit" |
||
253 | 1 | ), |
|
254 | ) |
||
255 | 1 | ||
256 | 1 | ||
257 | 1 | def _remove(subs, shared): |
|
258 | """Configure the `doorstop remove` subparser.""" |
||
259 | 1 | info = "remove an item file from a document directory" |
|
260 | sub = subs.add_parser( |
||
261 | 1 | 'remove', description=info.capitalize() + '.', help=info, **shared |
|
262 | ) |
||
263 | 1 | sub.add_argument('uid', help="item UID to remove from its document") |
|
264 | |||
265 | 1 | ||
266 | def _edit(subs, shared): |
||
267 | """Configure the `doorstop edit` subparser.""" |
||
268 | info = "open an existing item or document for editing" |
||
269 | 1 | sub = subs.add_parser( |
|
270 | 'edit', description=info.capitalize() + '.', help=info, **shared |
||
271 | 1 | ) |
|
272 | 1 | sub.add_argument('label', help="item UID or document prefix to open for editing") |
|
273 | sub.add_argument( |
||
274 | 1 | '-a', |
|
275 | 1 | '--all', |
|
276 | action='store_true', |
||
277 | 1 | help=( |
|
278 | 1 | "Edit the whole item with all its attributes. " |
|
279 | "Without this option, only its text is opened for " |
||
280 | 1 | "edition. Useless when editing a whole document." |
|
281 | ), |
||
282 | 1 | ) |
|
283 | group = sub.add_mutually_exclusive_group() |
||
284 | 1 | group.add_argument( |
|
285 | '-i', '--item', action='store_true', help="indicates the 'label' is an item UID" |
||
286 | 1 | ) |
|
287 | group.add_argument( |
||
288 | '-d', |
||
289 | '--document', |
||
290 | 1 | action='store_true', |
|
291 | help="indicates the 'label' is a document prefix", |
||
292 | 1 | ) |
|
293 | 1 | group = sub.add_mutually_exclusive_group() |
|
294 | group.add_argument( |
||
295 | 1 | '-y', |
|
296 | 1 | '--yaml', |
|
297 | action='store_true', |
||
298 | 1 | help="edit document as exported YAML (default)", |
|
299 | 1 | ) |
|
300 | group.add_argument( |
||
301 | 1 | '-c', '--csv', action='store_true', help="edit document as exported CSV" |
|
302 | ) |
||
303 | 1 | group.add_argument( |
|
304 | '-t', '--tsv', action='store_true', help="edit document as exported TSV" |
||
305 | 1 | ) |
|
306 | group.add_argument( |
||
307 | 1 | '-x', '--xlsx', action='store_true', help="edit document as exported XLSX" |
|
308 | ) |
||
309 | 1 | required = sub.add_argument_group('required arguments') |
|
310 | required.add_argument( |
||
311 | '-T', |
||
312 | 1 | '--tool', |
|
313 | metavar='PROGRAM', |
||
314 | 1 | default=EDITOR, |
|
315 | help="text editor to open the document item (only required if $EDITOR is not found in environment)", |
||
316 | ) |
||
317 | |||
318 | |||
319 | def _reorder(subs, shared): |
||
320 | """Configure the `doorstop reorder` subparser.""" |
||
321 | info = "organize the outline structure of a document" |
||
322 | sub = subs.add_parser( |
||
323 | 'reorder', description=info.capitalize() + '.', help=info, **shared |
||
324 | ) |
||
325 | sub.add_argument('prefix', help="prefix of document to reorder") |
||
326 | group = sub.add_mutually_exclusive_group() |
||
327 | group.add_argument( |
||
328 | '-a', |
||
329 | '--auto', |
||
330 | action='store_true', |
||
331 | help="only perform automatic item reordering", |
||
332 | ) |
||
333 | group.add_argument( |
||
334 | '-m', |
||
335 | '--manual', |
||
336 | action='store_true', |
||
337 | help="do not automatically reorder the items", |
||
338 | ) |
||
339 | sub.add_argument( |
||
340 | '-T', |
||
341 | '--tool', |
||
342 | metavar='PROGRAM', |
||
343 | default=EDITOR, |
||
344 | help="text editor to open the document index", |
||
345 | ) |
||
346 | |||
347 | |||
348 | def _link(subs, shared): |
||
349 | """Configure the `doorstop link` subparser.""" |
||
350 | info = "add a new link between two items" |
||
351 | sub = subs.add_parser( |
||
352 | 'link', description=info.capitalize() + '.', help=info, **shared |
||
353 | ) |
||
354 | sub.add_argument('child', help="child item UID to link to the parent") |
||
355 | sub.add_argument('parent', help="parent item UID to link from the child") |
||
356 | |||
357 | |||
358 | def _unlink(subs, shared): |
||
359 | """Configure the `doorstop unlink` subparser.""" |
||
360 | info = "remove a link between two items" |
||
361 | sub = subs.add_parser( |
||
362 | 'unlink', description=info.capitalize() + '.', help=info, **shared |
||
363 | ) |
||
364 | sub.add_argument('child', help="child item UID to unlink from parent") |
||
365 | sub.add_argument('parent', help="parent item UID child is linked to") |
||
366 | |||
367 | |||
368 | def _clear(subs, shared): |
||
369 | """Configure the `doorstop clear` subparser.""" |
||
370 | info = "absolve items of their suspect link status" |
||
371 | sub = subs.add_parser( |
||
372 | 'clear', description=info.capitalize() + '.', help=info, **shared |
||
373 | ) |
||
374 | sub.add_argument('label', help="item UID, document prefix, or 'all'") |
||
375 | group = sub.add_mutually_exclusive_group() |
||
376 | group.add_argument( |
||
377 | '-i', '--item', action='store_true', help="indicates the 'label' is an item UID" |
||
378 | ) |
||
379 | group.add_argument( |
||
380 | '-d', |
||
381 | '--document', |
||
382 | action='store_true', |
||
383 | help="indicates the 'label' is a document prefix", |
||
384 | ) |
||
385 | |||
386 | |||
387 | def _review(subs, shared): |
||
388 | """Configure the `doorstop review` subparser.""" |
||
389 | info = "absolve items of their unreviewed status" |
||
390 | sub = subs.add_parser( |
||
391 | 'review', description=info.capitalize() + '.', help=info, **shared |
||
392 | ) |
||
393 | sub.add_argument('label', help="item UID, document prefix, or 'all'") |
||
394 | group = sub.add_mutually_exclusive_group() |
||
395 | group.add_argument( |
||
396 | '-i', '--item', action='store_true', help="indicates the 'label' is an item UID" |
||
397 | ) |
||
398 | group.add_argument( |
||
399 | '-d', |
||
400 | '--document', |
||
401 | action='store_true', |
||
402 | help="indicates the 'label' is a document prefix", |
||
403 | ) |
||
404 | |||
405 | |||
406 | View Code Duplication | def _import(subs, shared): |
|
0 ignored issues
–
show
Duplication
introduced
by
Loading history...
|
|||
407 | """Configure the `doorstop import` subparser.""" |
||
408 | info = "import an existing document or item" |
||
409 | sub = subs.add_parser( |
||
410 | 'import', description=info.capitalize() + '.', help=info, **shared |
||
411 | ) |
||
412 | sub.add_argument( |
||
413 | 'path', nargs='?', help="path to previously exported document file" |
||
414 | ) |
||
415 | sub.add_argument('prefix', nargs='?', help="prefix of document for import") |
||
416 | group = sub.add_mutually_exclusive_group() |
||
417 | group.add_argument( |
||
418 | '-d', |
||
419 | '--document', |
||
420 | nargs=2, |
||
421 | metavar='ARG', |
||
422 | help="import an existing document by: PREFIX PATH", |
||
423 | ) |
||
424 | group.add_argument( |
||
425 | '-i', |
||
426 | '--item', |
||
427 | nargs=2, |
||
428 | metavar='ARG', |
||
429 | help="import an existing item by: PREFIX UID", |
||
430 | ) |
||
431 | sub.add_argument( |
||
432 | '-p', |
||
433 | '--parent', |
||
434 | metavar='PREFIX', |
||
435 | help="parent document prefix for imported document", |
||
436 | ) |
||
437 | sub.add_argument( |
||
438 | '-a', '--attrs', metavar='DICT', help="dictionary of item attributes to import" |
||
439 | ) |
||
440 | sub.add_argument( |
||
441 | '-m', '--map', metavar='DICT', help="dictionary of custom item attribute names" |
||
442 | ) |
||
443 | |||
444 | |||
445 | View Code Duplication | def _export(subs, shared): |
|
0 ignored issues
–
show
|
|||
446 | """Configure the `doorstop export` subparser.""" |
||
447 | info = "export a document as YAML or another format" |
||
448 | sub = subs.add_parser( |
||
449 | 'export', description=info.capitalize() + '.', help=info, **shared |
||
450 | ) |
||
451 | sub.add_argument('prefix', help="prefix of document to export or 'all'") |
||
452 | sub.add_argument( |
||
453 | 'path', nargs='?', help="path to exported file or directory for 'all'" |
||
454 | ) |
||
455 | group = sub.add_mutually_exclusive_group() |
||
456 | group.add_argument( |
||
457 | '-y', '--yaml', action='store_true', help="output YAML (default when no path)" |
||
458 | ) |
||
459 | group.add_argument( |
||
460 | '-c', '--csv', action='store_true', help="output CSV (default for 'all')" |
||
461 | ) |
||
462 | group.add_argument('-t', '--tsv', action='store_true', help="output TSV") |
||
463 | group.add_argument('-x', '--xlsx', action='store_true', help="output XLSX") |
||
464 | sub.add_argument('-w', '--width', type=int, help="limit line width on text output") |
||
465 | |||
466 | |||
467 | def _publish(subs, shared): |
||
468 | """Configure the `doorstop publish` subparser.""" |
||
469 | info = "publish a document as text or another format" |
||
470 | sub = subs.add_parser( |
||
471 | 'publish', description=info.capitalize() + '.', help=info, **shared |
||
472 | ) |
||
473 | sub.add_argument('prefix', help="prefix of document to publish or 'all'") |
||
474 | sub.add_argument( |
||
475 | 'path', nargs='?', help="path to published file or directory for 'all'" |
||
476 | ) |
||
477 | group = sub.add_mutually_exclusive_group() |
||
478 | group.add_argument( |
||
479 | '-t', '--text', action='store_true', help="output text (default when no path)" |
||
480 | ) |
||
481 | group.add_argument('-m', '--markdown', action='store_true', help="output Markdown") |
||
482 | group.add_argument( |
||
483 | '-H', '--html', action='store_true', help="output HTML (default for 'all')" |
||
484 | ) |
||
485 | sub.add_argument('-w', '--width', type=int, help="limit line width on text output") |
||
486 | sub.add_argument( |
||
487 | '-C', |
||
488 | '--no-child-links', |
||
489 | action='store_true', |
||
490 | help="do not include child links on items", |
||
491 | ) |
||
492 | sub.add_argument( |
||
493 | '-L', |
||
494 | '--no-body-levels', |
||
495 | action='store_true', |
||
496 | default=None, |
||
497 | help="do not include levels on non-heading items", |
||
498 | ) |
||
499 | sub.add_argument( |
||
500 | '--no-levels', |
||
501 | choices=['all', 'body'], |
||
502 | help="do not include levels on heading and non-heading or non-heading items", |
||
503 | ) |
||
504 | sub.add_argument('--template', help="template file", default=publisher.HTMLTEMPLATE) |
||
505 | |||
506 | |||
507 | if __name__ == '__main__': # pragma: no cover (manual test) |
||
508 | main() |
||
509 |