Changed the command line parser from argparse to docopt.
This make the code cleaner and better documented. Now the usage is more clear, and also a little bit more strict. I'm using a modified docopt because I preffer using args.option instead of args['--option'], I also fixed a few flake8 issues in docopt.py.
This commit is contained in:
parent
43b7e27a22
commit
2f0f3f755d
|
|
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Removed the "plot" option "check_zone_fills". Use the preflight option.
|
||||
- Drill outputs: map.type and report.filename now should be map and report.
|
||||
The old mechanism is currently supported, but deprecated.
|
||||
- Now the command line usage is more clearly documented, but also more strict.
|
||||
|
||||
### Added
|
||||
- Help for the supported outputs (--help-list-outputs, --help-outputs and
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
"""KiPlot
|
||||
|
||||
Usage:
|
||||
kiplot [-b BOARD_FILE] [-c PLOT_CONFIG] [-d OUT_DIR] [-s SKIP_PRE]
|
||||
[-q | -v] [-i] [TARGET...]
|
||||
kiplot -l | --list [-c PLOT_CONFIG]
|
||||
kiplot --help-list-outputs
|
||||
kiplot --help-output=HELP_OUTPUT
|
||||
kiplot --help-outputs
|
||||
kiplot --help-preflights
|
||||
kiplot -h | --help
|
||||
kiplot --version
|
||||
|
||||
Arguments:
|
||||
TARGET Outputs to generate, default is all
|
||||
|
||||
Options:
|
||||
-h, --help Show this help message and exit
|
||||
-b BOARD, --board-file BOARD The PCB .kicad-pcb board file
|
||||
-c CONFIG, --plot-config CONFIG The plotting config file to use
|
||||
-d OUT_DIR, --out-dir OUT_DIR The output directory (cwd if not given)
|
||||
--help-list-outputs List supported outputs
|
||||
--help-output HELP_OUTPUT Help for this particular output
|
||||
--help-outputs List supported outputs and details
|
||||
--help-preflights List supported preflights and details
|
||||
-i, --invert-sel Generate the outputs not listed as targets
|
||||
-l, --list List available outputs (in the config file)
|
||||
-q, --quiet Remove information logs
|
||||
-s PRE, --skip-pre PRE Skip preflights, comma separated or `all`
|
||||
-v, --verbose Show debugging information
|
||||
--version, -V Show program's version number and exit
|
||||
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
cur_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(cur_dir)))
|
||||
print(sys.path)
|
||||
from kiplot.docopt import docopt
|
||||
|
||||
if __name__ == '__main__':
|
||||
arguments = docopt(__doc__, version='KiPlot 0.5.0', options_first=True)
|
||||
print(arguments)
|
||||
print(arguments.__dict__)
|
||||
|
|
@ -1,4 +1,37 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""KiPlot: Command-line Plotting for KiCad
|
||||
|
||||
Usage:
|
||||
kiplot [-b BOARD] [-c CONFIG] [-d OUT_DIR] [-s PRE] [-q | -v...] [-i]
|
||||
[TARGET...]
|
||||
kiplot [-b BOARD] [-c PLOT_CONFIG] --list
|
||||
kiplot --help-list-outputs
|
||||
kiplot --help-output=HELP_OUTPUT
|
||||
kiplot --help-outputs
|
||||
kiplot --help-preflights
|
||||
kiplot -h | --help
|
||||
kiplot --version
|
||||
|
||||
Arguments:
|
||||
TARGET Outputs to generate, default is all
|
||||
|
||||
Options:
|
||||
-h, --help Show this help message and exit
|
||||
-b BOARD, --board-file BOARD The PCB .kicad-pcb board file
|
||||
-c CONFIG, --plot-config CONFIG The plotting config file to use
|
||||
-d OUT_DIR, --out-dir OUT_DIR The output directory [default: .]
|
||||
--help-list-outputs List supported outputs
|
||||
--help-output HELP_OUTPUT Help for this particular output
|
||||
--help-outputs List supported outputs and details
|
||||
--help-preflights List supported preflights and details
|
||||
-i, --invert-sel Generate the outputs not listed as targets
|
||||
-l, --list List available outputs (in the config file)
|
||||
-q, --quiet Remove information logs
|
||||
-s PRE, --skip-pre PRE Skip preflights, comma separated or `all`
|
||||
-v, --verbose Show debugging information
|
||||
--version, -V Show program's version number and exit
|
||||
|
||||
"""
|
||||
__author__ = 'John Beard, Salvador E. Tropea'
|
||||
__copyright__ = 'Copyright 2018-2020, INTI/John Beard/Salvador E. Tropea'
|
||||
__credits__ = ['Salvador E. Tropea', 'John Beard']
|
||||
|
|
@ -6,7 +39,6 @@ __license__ = 'GPL v3+'
|
|||
__email__ = 'salvador@inti.gob.ar'
|
||||
__status__ = 'beta'
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import gzip
|
||||
|
|
@ -20,28 +52,26 @@ from .kiplot import (GS, generate_outputs)
|
|||
from .pre_base import (BasePreFlight)
|
||||
from .config_reader import (CfgYamlReader, print_outputs_help, print_output_help, print_preflights_help)
|
||||
from .misc import (NO_PCB_FILE, EXIT_BAD_ARGS)
|
||||
from .docopt import docopt
|
||||
from .__version__ import __version__
|
||||
|
||||
|
||||
def list_pre_and_outs(logger, outputs):
|
||||
logger.info('Available actions:\n')
|
||||
pf = BasePreFlight.get_in_use_objs()
|
||||
if len(pf):
|
||||
logger.info('Pre-flight:')
|
||||
for c in pf:
|
||||
logger.info('- '+str(c))
|
||||
if len(outputs):
|
||||
logger.info('Outputs:')
|
||||
for o in outputs:
|
||||
logger.info('- '+str(o))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Command-line Plotting for KiCad')
|
||||
parser.add_argument('target', nargs='*', help='Outputs to generate, default is all')
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
parser.add_argument('-b', '--board-file', help='The PCB .kicad-pcb board file')
|
||||
parser.add_argument('-c', '--plot-config', help='The plotting config file to use')
|
||||
parser.add_argument('-d', '--out-dir', default='.', help='The output directory (cwd if not given)')
|
||||
parser.add_argument('--help-list-outputs', action='store_true', help='List supported outputs')
|
||||
parser.add_argument('--help-output', help='Help for this particular output')
|
||||
parser.add_argument('--help-outputs', action='store_true', help='List supported outputs and details')
|
||||
parser.add_argument('--help-preflights', action='store_true', help='List supported preflights and details')
|
||||
parser.add_argument('-i', '--invert-sel', action='store_true', help='Generate the outputs not listed as targets')
|
||||
parser.add_argument('-l', '--list', action='store_true', help='List available outputs (in the config file)')
|
||||
group.add_argument('-q', '--quiet', action='store_true', help='remove information logs')
|
||||
parser.add_argument('-s', '--skip-pre', nargs=1, help='skip pre-flight actions, comma separated list or `all`')
|
||||
group.add_argument('-v', '--verbose', action='store_true', help='show debugging information')
|
||||
parser.add_argument('--version', '-V', action='version', version='%(prog)s '+__version__+' - ' +
|
||||
__copyright__+' - License: '+__license__)
|
||||
args = parser.parse_args()
|
||||
ver = 'KiPlot '+__version__+' - '+__copyright__+' - License: '+__license__
|
||||
args = docopt(__doc__, version=ver, options_first=True)
|
||||
|
||||
# Create a logger with the specified verbosity
|
||||
logger = log.init(args.verbose, args.quiet)
|
||||
|
|
@ -104,7 +134,7 @@ def main():
|
|||
sys.exit(EXIT_BAD_ARGS)
|
||||
|
||||
# Read the config file
|
||||
cr = CfgYamlReader(board_file)
|
||||
cr = CfgYamlReader()
|
||||
outputs = None
|
||||
try:
|
||||
# The Python way ...
|
||||
|
|
@ -116,17 +146,12 @@ def main():
|
|||
with open(plot_config) as cf_file:
|
||||
outputs = cr.read(cf_file)
|
||||
|
||||
# Actions
|
||||
# Is just list the available targets?
|
||||
if args.list:
|
||||
logger.info('\nPre-flight:')
|
||||
pf = BasePreFlight.get_in_use_objs()
|
||||
for c in pf:
|
||||
logger.info('- '+str(c))
|
||||
logger.info('Outputs:')
|
||||
for o in outputs:
|
||||
logger.info('- '+str(o))
|
||||
else:
|
||||
generate_outputs(outputs, args.target, args.invert_sel, args.skip_pre)
|
||||
list_pre_and_outs(logger, outputs)
|
||||
sys.exit(0)
|
||||
|
||||
generate_outputs(outputs, args.target, args.invert_sel, args.skip_pre)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ def config_error(msg):
|
|||
|
||||
|
||||
class CfgYamlReader(object):
|
||||
def __init__(self, brd_file):
|
||||
def __init__(self):
|
||||
super(CfgYamlReader, self).__init__()
|
||||
|
||||
def _check_version(self, v):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,597 @@
|
|||
"""Pythonic command-line interface parser that will make you smile.
|
||||
|
||||
* http://docopt.org
|
||||
* Repository and issue-tracker: https://github.com/docopt/docopt
|
||||
* Licensed under terms of MIT license (see LICENSE-MIT)
|
||||
* Copyright (c) 2013 Vladimir Keleshev, vladimir@keleshev.com
|
||||
|
||||
"""
|
||||
import sys
|
||||
import re
|
||||
|
||||
|
||||
__all__ = ['docopt']
|
||||
__version__ = '0.6.2'
|
||||
|
||||
|
||||
class DocoptLanguageError(Exception):
|
||||
|
||||
"""Error in construction of usage-message by developer."""
|
||||
|
||||
|
||||
class DocoptExit(SystemExit):
|
||||
|
||||
"""Exit in case user invoked program with incorrect arguments."""
|
||||
|
||||
usage = ''
|
||||
|
||||
def __init__(self, message=''):
|
||||
SystemExit.__init__(self, (message + '\n' + self.usage).strip())
|
||||
|
||||
|
||||
class Pattern(object):
|
||||
|
||||
def __eq__(self, other):
|
||||
return repr(self) == repr(other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(repr(self))
|
||||
|
||||
def fix(self):
|
||||
self.fix_identities()
|
||||
self.fix_repeating_arguments()
|
||||
return self
|
||||
|
||||
def fix_identities(self, uniq=None):
|
||||
"""Make pattern-tree tips point to same object if they are equal."""
|
||||
if not hasattr(self, 'children'):
|
||||
return self
|
||||
uniq = list(set(self.flat())) if uniq is None else uniq
|
||||
for i, child in enumerate(self.children):
|
||||
if not hasattr(child, 'children'):
|
||||
assert child in uniq
|
||||
self.children[i] = uniq[uniq.index(child)]
|
||||
else:
|
||||
child.fix_identities(uniq)
|
||||
|
||||
def fix_repeating_arguments(self):
|
||||
"""Fix elements that should accumulate/increment values."""
|
||||
either = [list(child.children) for child in transform(self).children]
|
||||
for case in either:
|
||||
for e in [child for child in case if case.count(child) > 1]:
|
||||
if type(e) is Argument or type(e) is Option and e.argcount:
|
||||
if e.value is None:
|
||||
e.value = []
|
||||
elif type(e.value) is not list:
|
||||
e.value = e.value.split()
|
||||
if type(e) is Command or type(e) is Option and e.argcount == 0:
|
||||
e.value = 0
|
||||
return self
|
||||
|
||||
|
||||
def transform(pattern):
|
||||
"""Expand pattern into an (almost) equivalent one, but with single Either.
|
||||
|
||||
Example: ((-a | -b) (-c | -d)) => (-a -c | -a -d | -b -c | -b -d)
|
||||
Quirks: [-a] => (-a), (-a...) => (-a -a)
|
||||
|
||||
"""
|
||||
result = []
|
||||
groups = [[pattern]]
|
||||
while groups:
|
||||
children = groups.pop(0)
|
||||
parents = [Required, Optional, OptionsShortcut, Either, OneOrMore]
|
||||
if any(t in map(type, children) for t in parents):
|
||||
child = [c for c in children if type(c) in parents][0]
|
||||
children.remove(child)
|
||||
if type(child) is Either:
|
||||
for c in child.children:
|
||||
groups.append([c] + children)
|
||||
elif type(child) is OneOrMore:
|
||||
groups.append(child.children * 2 + children)
|
||||
else:
|
||||
groups.append(child.children + children)
|
||||
else:
|
||||
result.append(children)
|
||||
return Either(*[Required(*e) for e in result])
|
||||
|
||||
|
||||
class LeafPattern(Pattern):
|
||||
|
||||
"""Leaf/terminal node of a pattern tree."""
|
||||
|
||||
def __init__(self, name, value=None):
|
||||
self.name, self.value = name, value
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%r, %r)' % (self.__class__.__name__, self.name, self.value)
|
||||
|
||||
def flat(self, *types):
|
||||
return [self] if not types or type(self) in types else []
|
||||
|
||||
def match(self, left, collected=None):
|
||||
collected = [] if collected is None else collected
|
||||
pos, match = self.single_match(left)
|
||||
if match is None:
|
||||
return False, left, collected
|
||||
left_ = left[:pos] + left[pos + 1:]
|
||||
same_name = [a for a in collected if a.name == self.name]
|
||||
if type(self.value) in (int, list):
|
||||
if type(self.value) is int:
|
||||
increment = 1
|
||||
else:
|
||||
increment = ([match.value] if type(match.value) is str
|
||||
else match.value)
|
||||
if not same_name:
|
||||
match.value = increment
|
||||
return True, left_, collected + [match]
|
||||
same_name[0].value += increment
|
||||
return True, left_, collected
|
||||
return True, left_, collected + [match]
|
||||
|
||||
|
||||
class BranchPattern(Pattern):
|
||||
|
||||
"""Branch/inner node of a pattern tree."""
|
||||
|
||||
def __init__(self, *children):
|
||||
self.children = list(children)
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%s)' % (self.__class__.__name__,
|
||||
', '.join(repr(a) for a in self.children))
|
||||
|
||||
def flat(self, *types):
|
||||
if type(self) in types:
|
||||
return [self]
|
||||
return sum([child.flat(*types) for child in self.children], [])
|
||||
|
||||
|
||||
class Argument(LeafPattern):
|
||||
|
||||
def single_match(self, left):
|
||||
for n, pattern in enumerate(left):
|
||||
if type(pattern) is Argument:
|
||||
return n, Argument(self.name, pattern.value)
|
||||
return None, None
|
||||
|
||||
@classmethod
|
||||
def parse(class_, source):
|
||||
name = re.findall(r'(<\S*?>)', source)[0]
|
||||
value = re.findall(r'\[default: (.*)\]', source, flags=re.I)
|
||||
return class_(name, value[0] if value else None)
|
||||
|
||||
|
||||
class Command(Argument):
|
||||
|
||||
def __init__(self, name, value=False):
|
||||
self.name, self.value = name, value
|
||||
|
||||
def single_match(self, left):
|
||||
for n, pattern in enumerate(left):
|
||||
if type(pattern) is Argument:
|
||||
if pattern.value == self.name:
|
||||
return n, Command(self.name, True)
|
||||
else:
|
||||
break
|
||||
return None, None
|
||||
|
||||
|
||||
class Option(LeafPattern):
|
||||
|
||||
def __init__(self, short=None, long=None, argcount=0, value=False):
|
||||
assert argcount in (0, 1)
|
||||
self.short, self.long, self.argcount = short, long, argcount
|
||||
self.value = None if value is False and argcount else value
|
||||
|
||||
@classmethod
|
||||
def parse(class_, option_description):
|
||||
short, long, argcount, value = None, None, 0, False
|
||||
options, _, description = option_description.strip().partition(' ')
|
||||
options = options.replace(',', ' ').replace('=', ' ')
|
||||
for s in options.split():
|
||||
if s.startswith('--'):
|
||||
long = s
|
||||
elif s.startswith('-'):
|
||||
short = s
|
||||
else:
|
||||
argcount = 1
|
||||
if argcount:
|
||||
matched = re.findall(r'\[default: (.*)\]', description, flags=re.I)
|
||||
value = matched[0] if matched else None
|
||||
return class_(short, long, argcount, value)
|
||||
|
||||
def single_match(self, left):
|
||||
for n, pattern in enumerate(left):
|
||||
if self.name == pattern.name:
|
||||
return n, pattern
|
||||
return None, None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.long or self.short
|
||||
|
||||
def __repr__(self):
|
||||
return 'Option(%r, %r, %r, %r)' % (self.short, self.long,
|
||||
self.argcount, self.value)
|
||||
|
||||
|
||||
class Required(BranchPattern):
|
||||
|
||||
def match(self, left, collected=None):
|
||||
collected = [] if collected is None else collected
|
||||
le = left
|
||||
c = collected
|
||||
for pattern in self.children:
|
||||
matched, le, c = pattern.match(le, c)
|
||||
if not matched:
|
||||
return False, left, collected
|
||||
return True, le, c
|
||||
|
||||
|
||||
class Optional(BranchPattern):
|
||||
|
||||
def match(self, left, collected=None):
|
||||
collected = [] if collected is None else collected
|
||||
for pattern in self.children:
|
||||
m, left, collected = pattern.match(left, collected)
|
||||
return True, left, collected
|
||||
|
||||
|
||||
class OptionsShortcut(Optional):
|
||||
|
||||
"""Marker/placeholder for [options] shortcut."""
|
||||
|
||||
|
||||
class OneOrMore(BranchPattern):
|
||||
|
||||
def match(self, left, collected=None):
|
||||
assert len(self.children) == 1
|
||||
collected = [] if collected is None else collected
|
||||
le = left
|
||||
c = collected
|
||||
l_ = None
|
||||
matched = True
|
||||
times = 0
|
||||
while matched:
|
||||
# could it be that something didn't match but changed le or c?
|
||||
matched, le, c = self.children[0].match(le, c)
|
||||
times += 1 if matched else 0
|
||||
if l_ == le:
|
||||
break
|
||||
l_ = le
|
||||
if times >= 1:
|
||||
return True, le, c
|
||||
return False, left, collected
|
||||
|
||||
|
||||
class Either(BranchPattern):
|
||||
|
||||
def match(self, left, collected=None):
|
||||
collected = [] if collected is None else collected
|
||||
outcomes = []
|
||||
for pattern in self.children:
|
||||
matched, _, _ = outcome = pattern.match(left, collected)
|
||||
if matched:
|
||||
outcomes.append(outcome)
|
||||
if outcomes:
|
||||
return min(outcomes, key=lambda outcome: len(outcome[1]))
|
||||
return False, left, collected
|
||||
|
||||
|
||||
class Tokens(list):
|
||||
|
||||
def __init__(self, source, error=DocoptExit):
|
||||
self += source.split() if hasattr(source, 'split') else source
|
||||
self.error = error
|
||||
|
||||
@staticmethod
|
||||
def from_pattern(source):
|
||||
source = re.sub(r'([\[\]\(\)\|]|\.\.\.)', r' \1 ', source)
|
||||
source = [s for s in re.split(r'\s+|(\S*<.*?>)', source) if s]
|
||||
return Tokens(source, error=DocoptLanguageError)
|
||||
|
||||
def move(self):
|
||||
return self.pop(0) if len(self) else None
|
||||
|
||||
def current(self):
|
||||
return self[0] if len(self) else None
|
||||
|
||||
|
||||
def parse_long(tokens, options):
|
||||
"""long ::= '--' chars [ ( ' ' | '=' ) chars ] ;"""
|
||||
long, eq, value = tokens.move().partition('=')
|
||||
assert long.startswith('--')
|
||||
value = None if eq == value == '' else value
|
||||
similar = [o for o in options if o.long == long]
|
||||
if tokens.error is DocoptExit and similar == []: # if no exact match
|
||||
similar = [o for o in options if o.long and o.long.startswith(long)]
|
||||
if len(similar) > 1: # might be simply specified ambiguously 2+ times?
|
||||
raise tokens.error('%s is not a unique prefix: %s?' %
|
||||
(long, ', '.join(o.long for o in similar)))
|
||||
elif len(similar) < 1:
|
||||
argcount = 1 if eq == '=' else 0
|
||||
o = Option(None, long, argcount)
|
||||
options.append(o)
|
||||
if tokens.error is DocoptExit:
|
||||
o = Option(None, long, argcount, value if argcount else True)
|
||||
else:
|
||||
o = Option(similar[0].short, similar[0].long,
|
||||
similar[0].argcount, similar[0].value)
|
||||
if o.argcount == 0:
|
||||
if value is not None:
|
||||
raise tokens.error('%s must not have an argument' % o.long)
|
||||
else:
|
||||
if value is None:
|
||||
if tokens.current() in [None, '--']:
|
||||
raise tokens.error('%s requires argument' % o.long)
|
||||
value = tokens.move()
|
||||
if tokens.error is DocoptExit:
|
||||
o.value = value if value is not None else True
|
||||
return [o]
|
||||
|
||||
|
||||
def parse_shorts(tokens, options):
|
||||
"""shorts ::= '-' ( chars )* [ [ ' ' ] chars ] ;"""
|
||||
token = tokens.move()
|
||||
assert token.startswith('-') and not token.startswith('--')
|
||||
left = token.lstrip('-')
|
||||
parsed = []
|
||||
while left != '':
|
||||
short, left = '-' + left[0], left[1:]
|
||||
similar = [o for o in options if o.short == short]
|
||||
if len(similar) > 1:
|
||||
raise tokens.error('%s is specified ambiguously %d times' %
|
||||
(short, len(similar)))
|
||||
elif len(similar) < 1:
|
||||
o = Option(short, None, 0)
|
||||
options.append(o)
|
||||
if tokens.error is DocoptExit:
|
||||
o = Option(short, None, 0, True)
|
||||
else: # why copying is necessary here?
|
||||
o = Option(short, similar[0].long,
|
||||
similar[0].argcount, similar[0].value)
|
||||
value = None
|
||||
if o.argcount != 0:
|
||||
if left == '':
|
||||
if tokens.current() in [None, '--']:
|
||||
raise tokens.error('%s requires argument' % short)
|
||||
value = tokens.move()
|
||||
else:
|
||||
value = left
|
||||
left = ''
|
||||
if tokens.error is DocoptExit:
|
||||
o.value = value if value is not None else True
|
||||
parsed.append(o)
|
||||
return parsed
|
||||
|
||||
|
||||
def parse_pattern(source, options):
|
||||
tokens = Tokens.from_pattern(source)
|
||||
result = parse_expr(tokens, options)
|
||||
if tokens.current() is not None:
|
||||
raise tokens.error('unexpected ending: %r' % ' '.join(tokens))
|
||||
return Required(*result)
|
||||
|
||||
|
||||
def parse_expr(tokens, options):
|
||||
"""expr ::= seq ( '|' seq )* ;"""
|
||||
seq = parse_seq(tokens, options)
|
||||
if tokens.current() != '|':
|
||||
return seq
|
||||
result = [Required(*seq)] if len(seq) > 1 else seq
|
||||
while tokens.current() == '|':
|
||||
tokens.move()
|
||||
seq = parse_seq(tokens, options)
|
||||
result += [Required(*seq)] if len(seq) > 1 else seq
|
||||
return [Either(*result)] if len(result) > 1 else result
|
||||
|
||||
|
||||
def parse_seq(tokens, options):
|
||||
"""seq ::= ( atom [ '...' ] )* ;"""
|
||||
result = []
|
||||
while tokens.current() not in [None, ']', ')', '|']:
|
||||
atom = parse_atom(tokens, options)
|
||||
if tokens.current() == '...':
|
||||
atom = [OneOrMore(*atom)]
|
||||
tokens.move()
|
||||
result += atom
|
||||
return result
|
||||
|
||||
|
||||
def parse_atom(tokens, options):
|
||||
"""atom ::= '(' expr ')' | '[' expr ']' | 'options'
|
||||
| long | shorts | argument | command ;
|
||||
"""
|
||||
token = tokens.current()
|
||||
result = []
|
||||
if token in '([':
|
||||
tokens.move()
|
||||
matching, pattern = {'(': [')', Required], '[': [']', Optional]}[token]
|
||||
result = pattern(*parse_expr(tokens, options))
|
||||
if tokens.move() != matching:
|
||||
raise tokens.error("unmatched '%s'" % token)
|
||||
return [result]
|
||||
elif token == 'options':
|
||||
tokens.move()
|
||||
return [OptionsShortcut()]
|
||||
elif token.startswith('--') and token != '--':
|
||||
return parse_long(tokens, options)
|
||||
elif token.startswith('-') and token not in ('-', '--'):
|
||||
return parse_shorts(tokens, options)
|
||||
elif token.startswith('<') and token.endswith('>') or token.isupper():
|
||||
return [Argument(tokens.move())]
|
||||
else:
|
||||
return [Command(tokens.move())]
|
||||
|
||||
|
||||
def parse_argv(tokens, options, options_first=False):
|
||||
"""Parse command-line argument vector.
|
||||
|
||||
If options_first:
|
||||
argv ::= [ long | shorts ]* [ argument ]* [ '--' [ argument ]* ] ;
|
||||
else:
|
||||
argv ::= [ long | shorts | argument ]* [ '--' [ argument ]* ] ;
|
||||
|
||||
"""
|
||||
parsed = []
|
||||
while tokens.current() is not None:
|
||||
if tokens.current() == '--':
|
||||
return parsed + [Argument(None, v) for v in tokens]
|
||||
elif tokens.current().startswith('--'):
|
||||
parsed += parse_long(tokens, options)
|
||||
elif tokens.current().startswith('-') and tokens.current() != '-':
|
||||
parsed += parse_shorts(tokens, options)
|
||||
elif options_first:
|
||||
return parsed + [Argument(None, v) for v in tokens]
|
||||
else:
|
||||
parsed.append(Argument(None, tokens.move()))
|
||||
return parsed
|
||||
|
||||
|
||||
def parse_defaults(doc):
|
||||
defaults = []
|
||||
for s in parse_section('options:', doc):
|
||||
# FIXME corner case "bla: options: --foo"
|
||||
_, _, s = s.partition(':') # get rid of "options:"
|
||||
split = re.split(r'\n[ \t]*(-\S+?)', '\n' + s)[1:]
|
||||
split = [s1 + s2 for s1, s2 in zip(split[::2], split[1::2])]
|
||||
options = [Option.parse(s) for s in split if s.startswith('-')]
|
||||
defaults += options
|
||||
return defaults
|
||||
|
||||
|
||||
def parse_section(name, source):
|
||||
pattern = re.compile('^([^\n]*' + name + '[^\n]*\n?(?:[ \t].*?(?:\n|$))*)',
|
||||
re.IGNORECASE | re.MULTILINE)
|
||||
return [s.strip() for s in pattern.findall(source)]
|
||||
|
||||
|
||||
def formal_usage(section):
|
||||
_, _, section = section.partition(':') # drop "usage:"
|
||||
pu = section.split()
|
||||
return '( ' + ' '.join(') | (' if s == pu[0] else s for s in pu[1:]) + ' )'
|
||||
|
||||
|
||||
def extras(help, version, options, doc):
|
||||
if help and any((o.name in ('-h', '--help')) and o.value for o in options):
|
||||
print(doc.strip("\n"))
|
||||
sys.exit()
|
||||
if version and any(o.name == '--version' and o.value for o in options):
|
||||
print(version)
|
||||
sys.exit()
|
||||
|
||||
|
||||
class Dict(dict):
|
||||
def __repr__(self):
|
||||
return '{%s}' % ',\n '.join('%r: %r' % i for i in sorted(self.items()))
|
||||
|
||||
|
||||
def clean_name(name):
|
||||
""" Make name suitable for a class attribute """
|
||||
# Remove starting -/--
|
||||
if name[1] == '-':
|
||||
name = name[2:]
|
||||
elif name[0] == '-':
|
||||
name = name[1:]
|
||||
# Convert - to _
|
||||
name = name.replace('-', '_')
|
||||
# Finally make it lower case
|
||||
return name.lower()
|
||||
|
||||
|
||||
def docopt(doc, argv=None, help=True, version=None, options_first=False):
|
||||
"""Parse `argv` based on command-line interface described in `doc`.
|
||||
|
||||
`docopt` creates your command-line interface based on its
|
||||
description that you pass as `doc`. Such description can contain
|
||||
--options, <positional-argument>, commands, which could be
|
||||
[optional], (required), (mutually | exclusive) or repeated...
|
||||
|
||||
Parameters
|
||||
----------
|
||||
doc : str
|
||||
Description of your command-line interface.
|
||||
argv : list of str, optional
|
||||
Argument vector to be parsed. sys.argv[1:] is used if not
|
||||
provided.
|
||||
help : bool (default: True)
|
||||
Set to False to disable automatic help on -h or --help
|
||||
options.
|
||||
version : any object
|
||||
If passed, the object will be printed if --version is in
|
||||
`argv`.
|
||||
options_first : bool (default: False)
|
||||
Set to True to require options precede positional arguments,
|
||||
i.e. to forbid options and positional arguments intermix.
|
||||
|
||||
Returns
|
||||
-------
|
||||
args : dict
|
||||
A dictionary, where keys are names of command-line elements
|
||||
such as e.g. "--verbose" and "<path>", and values are the
|
||||
parsed values of those elements.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> from docopt import docopt
|
||||
>>> doc = '''
|
||||
... Usage:
|
||||
... my_program tcp <host> <port> [--timeout=<seconds>]
|
||||
... my_program serial <port> [--baud=<n>] [--timeout=<seconds>]
|
||||
... my_program (-h | --help | --version)
|
||||
...
|
||||
... Options:
|
||||
... -h, --help Show this screen and exit.
|
||||
... --baud=<n> Baudrate [default: 9600]
|
||||
... '''
|
||||
>>> argv = ['tcp', '127.0.0.1', '80', '--timeout', '30']
|
||||
>>> docopt(doc, argv)
|
||||
{'--baud': '9600',
|
||||
'--help': False,
|
||||
'--timeout': '30',
|
||||
'--version': False,
|
||||
'<host>': '127.0.0.1',
|
||||
'<port>': '80',
|
||||
'serial': False,
|
||||
'tcp': True}
|
||||
|
||||
See also
|
||||
--------
|
||||
* For video introduction see http://docopt.org
|
||||
* Full documentation is available in README.rst as well as online
|
||||
at https://github.com/docopt/docopt#readme
|
||||
|
||||
"""
|
||||
argv = sys.argv[1:] if argv is None else argv
|
||||
|
||||
usage_sections = parse_section('usage:', doc)
|
||||
if len(usage_sections) == 0:
|
||||
raise DocoptLanguageError('"usage:" (case-insensitive) not found.')
|
||||
if len(usage_sections) > 1:
|
||||
raise DocoptLanguageError('More than one "usage:" (case-insensitive).')
|
||||
DocoptExit.usage = usage_sections[0]
|
||||
|
||||
options = parse_defaults(doc)
|
||||
pattern = parse_pattern(formal_usage(DocoptExit.usage), options)
|
||||
# [default] syntax for argument is disabled
|
||||
# for a in pattern.flat(Argument):
|
||||
# same_name = [d for d in arguments if d.name == a.name]
|
||||
# if same_name:
|
||||
# a.value = same_name[0].value
|
||||
argv = parse_argv(Tokens(argv), list(options), options_first)
|
||||
pattern_options = set(pattern.flat(Option))
|
||||
for options_shortcut in pattern.flat(OptionsShortcut):
|
||||
doc_options = parse_defaults(doc)
|
||||
options_shortcut.children = list(set(doc_options) - pattern_options)
|
||||
# if any_options:
|
||||
# options_shortcut.children += [Option(o.short, o.long, o.argcount)
|
||||
# for o in argv if type(o) is Option]
|
||||
extras(help, version, argv, doc)
|
||||
matched, left, collected = pattern.fix().match(argv)
|
||||
if matched and left == []: # better error message if left?
|
||||
d = Dict((a.name, a.value) for a in (pattern.flat() + collected))
|
||||
for a in (pattern.flat() + collected):
|
||||
setattr(d, clean_name(a.name), a.value)
|
||||
return d
|
||||
raise DocoptExit()
|
||||
|
|
@ -133,11 +133,11 @@ def preflight_checks(skip_pre):
|
|||
logger.debug("Preflight checks")
|
||||
|
||||
if skip_pre is not None:
|
||||
if skip_pre[0] == 'all':
|
||||
if skip_pre == 'all':
|
||||
logger.debug("Skipping all pre-flight actions")
|
||||
return
|
||||
else:
|
||||
skip_list = skip_pre[0].split(',')
|
||||
skip_list = skip_pre.split(',')
|
||||
for skip in skip_list:
|
||||
if skip == 'all':
|
||||
logger.error('All can\'t be part of a list of actions '
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ def test_auto_pcb_and_cfg_2():
|
|||
|
||||
def test_list():
|
||||
ctx = context.TestContext('List', '3Rs', 'pre_and_position', POS_DIR)
|
||||
ctx.run(extra=['--list'])
|
||||
ctx.run(extra=['--list'], no_verbose=True, no_out_dir=True)
|
||||
|
||||
assert ctx.search_err('run_erc: True')
|
||||
assert ctx.search_err('run_drc: True')
|
||||
|
|
|
|||
Loading…
Reference in New Issue