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