Issues (16)

doorstop/cli/main.py (2 issues)

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
This code seems to be duplicated in your project.
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
This code seems to be duplicated in your project.
Loading history...
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