PKN^docopt_sh/__init__.py""" docopt.sh Bash argument parser generator. This program looks for a "doc" variable in SCRIPT and appends a matching parser to it. """ __all__ = ['docopt_sh'] __version__ = '0.7.3' PK梬Nixdocopt_sh/__main__.py#!/usr/bin/env python3 import sys import re import os from docopt import docopt, DocoptExit from . import __doc__ as pkg_doc from .script import Script, DocoptScriptValidationError from .parser import Parser import logging log = logging.getLogger(__name__) __doc__ = pkg_doc + """ Usage: docopt.sh [options] [SCRIPT] Parser generation options: --prefix=PREFIX Parameter variable name prefix [default: ] --no-doc-check Disable check for whether if the parser matches the doc --no-help Disable automatic help on -h or --help --options-first Require that options precede positional arguments --no-teardown Disable teardown of functions and variables after parsing --no-version Disable automatic --version despite $version being present --no-minimize Disable parsing code minimization --line-length N Maximum line length when minimizing [default: 80] Other options: --only-parser Only output the parser to stdout -d --debug Whether to enable debugging mode (embedded in the parser) -h --help This help message --version Version of this program Notes: You can pass the script on stdin as well, docopt.sh will then output the modified script to stdout. If the script has a $version defined anywhere before the invocation of docopt --version will automatically output the value of that variable. """ def docopt_sh(params): try: if params['SCRIPT'] is None: if sys.stdin.isatty(): raise DocoptExit('Not reading from stdin when it is a tty') script = Script(sys.stdin.read()) else: with open(params['SCRIPT'], 'r') as h: script = Script(h.read(), params['SCRIPT']) parser = Parser(script, params) if params['--only-parser']: sys.stdout.write(str(parser)) else: patched_script = str(parser.patched_script) if params['SCRIPT'] is None: sys.stdout.write(patched_script) else: with open(params['SCRIPT'], 'w') as h: h.write(patched_script) except DocoptScriptValidationError as e: print(str(e)) # Exit code 74: input/output error (sysexits.h) sys.exit(74) def main(): params = docopt(__doc__) docopt_sh(params) if __name__ == '__main__': main() PK梬Nh--docopt_sh/doc.pyimport re from collections import OrderedDict from itertools import chain class DocAst(object): def __init__(self, settings, doc): from .bash.tree.node import BranchNode, LeafNode doc = doc root, usage_match = parse_doc(doc) node_map = OrderedDict([]) param_sort_order = [Option, Argument, Command] unique_params = list(OrderedDict.fromkeys(root.flat(*param_sort_order))) sorted_params = sorted(unique_params, key=lambda p: param_sort_order.index(type(p))) for idx, param in enumerate(sorted_params): node_map[param] = LeafNode(settings, param, idx) offset = len(node_map) for idx, pattern in enumerate(iter(root)): if isinstance(pattern, BranchPattern): node_map[pattern] = BranchNode(settings, pattern, offset + idx, node_map) self.root_node = node_map[root] self.node_map = node_map self.usage_match = usage_match @property def nodes(self): return self.node_map.values() @property def leaf_nodes(self): from .bash.tree.node import LeafNode return [node for node in self.nodes if type(node) is LeafNode] @property def usage_section(self): return self.usage_match.start(0), self.usage_match.end(0) def parse_doc(doc): 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).') usage, usage_match = usage_sections[0] options = parse_defaults(doc) pattern = parse_pattern(formal_usage(usage), options) 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) return pattern.fix(), usage_match class DocoptLanguageError(Exception): """Error in construction of usage-message by developer.""" 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 __iter__(self): yield self def flat(self, *types): return [self] if not types or type(self) in types else [] 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 __iter__(self): for child in chain(*map(iter, self.children)): yield child yield self def flat(self, *types): if type(self) in types: return [self] return sum([child.flat(*types) for child in self.children], []) class Argument(LeafPattern): @classmethod def parse(class_, source): name = re.findall('(<\S*?>)', source)[0] value = re.findall('\[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 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('\[default: (.*)\]', description, flags=re.I) value = matched[0] if matched else None return class_(short, long, argcount, value) @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): pass class Optional(BranchPattern): pass class OptionsShortcut(Optional): """Marker/placeholder for [options] shortcut.""" class OneOrMore(BranchPattern): pass class Either(BranchPattern): pass class Tokens(list): def __init__(self, source): self += source.split() if hasattr(source, 'split') else source @staticmethod def from_pattern(source): source = re.sub(r'([\[\]\(\)\|]|\.\.\.)', r' \1 ', source) source = [s for s in re.split('\s+|(\S*<.*?>)', source) if s] return Tokens(source) 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 len(similar) > 1: # might be simply specified ambiguously 2+ times? raise DocoptLanguageError('%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) 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 DocoptLanguageError('%s must not have an argument' % o.long) else: if value is None: if tokens.current() in [None, '--']: raise DocoptLanguageError('%s requires argument' % o.long) value = tokens.move() 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 DocoptLanguageError('%s is specified ambiguously %d times' % (short, len(similar))) elif len(similar) < 1: o = Option(short, None, 0) options.append(o) 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 DocoptLanguageError('%s requires argument' % short) value = tokens.move() else: value = left left = '' 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 DocoptLanguageError('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 DocoptLanguageError("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_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('\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 [(m.group(0).strip(), m) for m in pattern.finditer(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:]) + ' )' PK Nmx docopt_sh/parser.pyimport os.path from .doc import DocAst from .bash import helpers, tree, minimize class Parser(object): def __init__(self, script, params): self.script = script self.settings = ParserSettings(script, params) self.doc_ast = DocAst(self.settings, script.doc.value) @property def patched_script(self): return self.script.insert_parser(str(self), self.settings.refresh_command) def __str__(self): nodes = [node for node in self.doc_ast.nodes if node is not self.doc_ast.root_node] ast_helper_names = set([n.helper_name for n in nodes]) ast_helper_fns = list(filter(lambda h: h.name in ast_helper_names, [ tree.Command(self.settings), tree.Either(self.settings), tree.OneOrMore(self.settings), tree.Optional(self.settings), tree.Required(self.settings), tree.Switch(self.settings), tree.Value(self.settings), ])) helper_fns = [ helpers.ParseShorts(self.settings), helpers.ParseLong(self.settings), helpers.Error(self.settings, usage_section=self.doc_ast.usage_section), helpers.Main(self.settings, root_node=self.doc_ast.root_node, leaf_nodes=self.doc_ast.leaf_nodes), ] if len(self.doc_ast.leaf_nodes) > 0: helper_fns.append(helpers.Defaults(self.settings, leaf_nodes=self.doc_ast.leaf_nodes)) parser_str = '\n'.join(map(str, nodes + ast_helper_fns + helper_fns)) if self.settings.minimize: parser_str = minimize(parser_str, self.settings.max_line_length) return parser_str + '\n' class ParserSettings(object): def __init__(self, script, docopt_params): self.script = script self.docopt_params = docopt_params @property def name_prefix(self): return self.docopt_params['--prefix'] @property def add_doc_check(self): return not self.docopt_params['--no-doc-check'] @property def options_first(self): return self.docopt_params['--options-first'] @property def add_help(self): return not self.docopt_params['--no-help'] @property def add_version(self): if self.docopt_params['--no-version']: return False return self.script.version.present @property def add_teardown(self): return not self.docopt_params['--no-teardown'] @property def minimize(self): return not self.docopt_params['--no-minimize'] @property def max_line_length(self): return int(self.docopt_params['--line-length']) @property def refresh_command(self): command = 'docopt.sh' if self.docopt_params['--debug']: command += ' --debug' if self.docopt_params['--prefix'] != '': command += ' --prefix=' + self.docopt_params['--prefix'] if self.docopt_params['SCRIPT'] is not None: command += ' ' + os.path.basename(self.docopt_params['SCRIPT']) return command PK梬NQOdocopt_sh/script.pyimport re import logging log = logging.getLogger(__name__) class Script(object): def __init__(self, contents, path=None): self.contents = contents self.path = path self.doc = Doc(self.contents) self.parser = Parser(self.contents, self.doc) self.invocation = Invocation(self.contents, self.parser) self.version = Version(self.contents, self.invocation) self.validate_script_locations() def validate_script_locations(self): if not self.doc.present: raise DocoptScriptValidationError('Variable containing usage doc not found.', self.path) if self.doc.count > 1: raise DocoptScriptValidationError('More than one variable contain usage doc found.', self.path) if self.parser.start_guard.count > 1: raise DocoptScriptValidationError('Multiple docopt parser start guards found', self.path) if self.parser.end_guard.count > 1: raise DocoptScriptValidationError('Multiple docopt parser end guards found', self.path) if self.parser.start_guard.present and not self.parser.end_guard.present: raise DocoptScriptValidationError('Parser begin guard found, but no end guard detected', self.path) if self.parser.end_guard.present and not self.parser.start_guard.present: raise DocoptScriptValidationError('Parser end guard found, but no begin guard detected', self.path) if self.invocation.count > 1: log.warning('Multiple invocations of docopt found, check your script to make sure this is correct.') if not self.invocation.present: log.warning( 'No invocations of docopt found, check your script to make sure this is correct.\n' 'docopt.sh is invoked with `docopt "$@"`' ) def insert_parser(self, parser, refresh_command=None): guard_begin = "# docopt parser below" guard_end = "# docopt parser above" if refresh_command: guard_begin += ", refresh this parser with `%s`" % refresh_command guard_end += ", refresh this parser with `%s`" % refresh_command return Script( "{start}{guard_begin}\n{parser}{guard_end}\n{end}".format( start=self.contents[:self.parser.start], guard_begin=guard_begin, parser=parser, guard_end=guard_end, end=self.contents[self.parser.end:], ) ) def __str__(self): return self.contents class ScriptLocation(object): def __init__(self, matches, offset): self.matches = matches self.match = next(matches, None) self.offset = 0 if offset is None else offset def __len__(self): return self.end - self.start if self.present else 0 @property def present(self): return self.match is not None @property def count(self): return len(list(self.matches)) @property def start(self): if self.present: return self.match.start(0) + self.offset @property def end(self): if self.present: return self.match.end(0) + self.offset class Doc(ScriptLocation): def __init__(self, script): matches = re.finditer( r'([a-zA-Z_][a-zA-Z_0-9]*)="((\\"|[^"])*Usage:(\\"|[^"])+)"\s*(\n|;)', script, re.MULTILINE | re.IGNORECASE ) super(Doc, self).__init__(matches, 0) @property def name(self): if self.present: return self.match.group(1) @property def value(self): if self.present: return self.match.group(2) class ParserStartGuard(ScriptLocation): def __init__(self, script, doc): matches = re.finditer(r'# docopt parser below[^\n]*\n', script[doc.end:], re.MULTILINE) super(ParserStartGuard, self).__init__(matches, doc.end) class ParserEndGuard(ScriptLocation): def __init__(self, script, start_guard): matches = re.finditer(r'# docopt parser above[^\n]*\n', script[start_guard.end:], re.MULTILINE) super(ParserEndGuard, self).__init__(matches, start_guard.end) class Parser(object): def __init__(self, script, doc): self.start_guard = ParserStartGuard(script, doc) if self.start_guard.present: self.end_guard = ParserEndGuard(script, self.start_guard) else: self.end_guard = ParserEndGuard(script, doc) def __len__(self): return self.end - self.start if self.present else 0 @property def present(self): return self.start_guard.present and self.end_guard.present @property def start(self): if self.present: return self.start_guard.start else: # Convenience location to easily handle none-presence of parser return self.start_guard.offset @property def end(self): if self.present: return self.end_guard.end else: return self.start_guard.offset class Invocation(ScriptLocation): def __init__(self, script, parser): matches = re.finditer(r'docopt\s+"\$\@"', script[parser.end:]) super(Invocation, self).__init__(matches, parser.end) class Version(ScriptLocation): def __init__(self, script, invocation): matches = re.finditer(r'^version=', script[:invocation.start], re.MULTILINE) super(Version, self).__init__(matches, 0) if self.count > 1: # Override parent class selection of first match, previous assignments # would be overwritten so it's the last match that has an effect self.match = matches[-1] class DocoptScriptValidationError(Exception): def __init__(self, message, path=None): super(DocoptScriptValidationError, self).__init__(message) self.message = message self.path = path def __str__(self): if self.path: return 'Error in %s: %s' % (self.path, self.message) return self.message PKN[o  docopt_sh/bash/__init__.pyimport re from shlex import quote class Function(object): def __init__(self, settings, name): self.name = name self.settings = settings def fn_wrap(self, body): indented = '\n'.join([' ' + line for line in body.strip().split('\n')]) return '{name}() {{\n{body}\n}}'.format(name=self.name, body=indented) def __str__(self): return self.fn_wrap(self.body) def bash_variable_name(name, prefix=''): name = name.replace('<', '_') name = name.replace('>', '_') name = name.replace('-', '_') name = name.replace(' ', '_') return prefix + name def bash_variable_value(value): if value is None: return '' if type(value) is bool: return 'true' if value else 'false' if type(value) is int: return str(value) if type(value) is str: return quote(value) if type(value) is list: return '(%s)' % ' '.join(bash_variable_value(v) for v in value) raise Exception('Unhandled value type %s' % type(value)) def bash_ifs_value(value): if value is None or value == '': return "''" if type(value) is bool: return 'true' if value else 'false' if type(value) is int: return str(value) if type(value) is str: return quote(value) if type(value) is list: raise Exception('Unable to convert list to bash value intended for an IFS separated field') raise Exception('Unhandled value type %s' % type(value)) def minimize(parser_str, max_length): lines = parser_str.split('\n') lines = remove_leading_spaces(lines) lines = remove_empty_lines(lines) lines = remove_newlines(lines, max_length) return '\n'.join(lines) def remove_leading_spaces(lines): for line in lines: yield re.sub(r'^\s*', '', line) def remove_empty_lines(lines): for line in lines: if line != '': yield line def remove_newlines(lines, max_length): def is_comment(line): return re.match(r'^\s*#', line) is not None def needs_separator(line): return re.search(r'; (then|do)$|else$|\{$', line) is None def has_continuation(line): return re.search(r'\\\s*$', line) is not None def remove_continuation(line): return re.sub(r'\s*\\\s*$', '', line) def combine(line1, line2): if is_comment(line1) or is_comment(line2): if not is_comment(line1): return line1 if not is_comment(line2): return line2 return None if has_continuation(line1): return remove_continuation(line1) + ' ' + line2 if needs_separator(line1): return line1 + '; ' + line2 else: return line1 + ' ' + line2 previous = next(lines) for line in lines: combined = combine(previous, line) if combined is None: previous = next(lines, None) elif len(combined) > max_length: yield previous previous = line else: previous = combined if previous: yield previous PK梬NQR"docopt_sh/bash/helpers/__init__.pyfrom .defaults import Defaults from .error import Error from .main import Main from .parse_long import ParseLong from .parse_shorts import ParseShorts PKNfW(ss"docopt_sh/bash/helpers/defaults.pyfrom .. import Function, bash_variable_value class Defaults(Function): def __init__(self, settings, leaf_nodes): super(Defaults, self).__init__(settings, '_do_def') self.leaf_nodes = leaf_nodes @property def body(self): defaults = [] for node in self.leaf_nodes: if type(node.default_value) is list: tpl = "[[ -z ${{{name}+x}} ]] && {name}={default}" else: tpl = "{name}=${{{name}:-{default}}}" defaults.append(tpl.format( name=node.variable_name, default=bash_variable_value(node.default_value) )) body = '\n'.join(defaults) return body PK梬N!@@docopt_sh/bash/helpers/error.pyfrom .. import Function class Error(Function): def __init__(self, settings, usage_section): super(Error, self).__init__(settings, '_do_err') self.usage_start, self.usage_end = usage_section if self.settings.script.doc.value.endswith('\n'): self.usage_end -= 1 @property def body(self): body = ''' [[ -n $1 ]] && printf "%s\\n" "$1" printf "%s\\n" "${{{docname}:{start}:{length}}}" exit 1 '''.format( docname=self.settings.script.doc.name, start=self.usage_start, length=self.usage_end - self.usage_start, ) return body PKNLj docopt_sh/bash/helpers/main.pyimport hashlib from .. import Function, bash_ifs_value from ...doc import Option class Main(Function): def __init__(self, settings, root_node, leaf_nodes): super(Main, self).__init__(settings, 'docopt') self.root_node = root_node self.leaf_nodes = leaf_nodes @property def body(self): body = '' if self.settings.add_doc_check: body += ''' local doc_hash local digest="{digest}" doc_hash=$(printf "%s" "${docname}" | shasum -a 256) if [[ ${{doc_hash:0:5}} != "$digest" ]]; then printf "The current usage doc (%s) does not match what the parser was generated with (%s)\\n" \\ "$doc_hash" "$digest" >&2 exit 70 fi '''.format( docname=self.settings.script.doc.name, digest=hashlib.sha256(self.settings.script.doc.value.encode('utf-8')).hexdigest()[0:5] ) # variables setup option_nodes = [o for o in self.leaf_nodes if o.type is Option] body += ''' _do_av=("$@") _do_sh=({options_short}) _do_lo=({options_long}) _do_ac=({options_argcount}) _do_pn=({param_names}) _do_pp=() _do_pv=() _lft=() _do_tm=false local var for var in "${{_do_pn[@]}}"; do unset "$var" done '''.format( options_short=' '.join([bash_ifs_value(o.pattern.short) for o in option_nodes]), options_long=' '.join([bash_ifs_value(o.pattern.long) for o in option_nodes]), options_argcount=' '.join([bash_ifs_value(o.pattern.argcount) for o in option_nodes]), param_names=' '.join([node.variable_name for node in self.leaf_nodes]), ) # parse _do_av body += ''' local arg while [[ ${#_do_av[@]} -gt 0 ]]; do if [[ ${_do_av[0]} = "--" ]]; then for arg in "${_do_av[@]}"; do _do_pp+=('a') _do_pv+=("$arg") done break elif [[ ${_do_av[0]} = --* ]]; then _do_long elif [[ ${_do_av[0]} = -* && ${_do_av[0]} != "-" ]]; then _do_shorts else ''' if self.settings.options_first: body += ''' for arg in "${_do_av[@]}"; do _do_pp+=('a') _do_pv+=("$arg") done break ''' else: body += ''' _do_pp+=('a') _do_pv+=("${_do_av[0]}") _do_av=("${_do_av[@]:1}") ''' body += ''' fi done ''' # extras if self.settings.add_help or self.settings.add_version: body += 'local idx' if self.settings.add_help: body += ''' for idx in "${{_do_pp[@]}}"; do [[ $idx = 'a' ]] && continue if [[ ${{_do_sh[$idx]}} = "-h" || ${{_do_lo[$idx]}} = "--help" ]]; then printf -- "{template}" "${docname}" exit 0 fi done '''.format( template='%s' if self.settings.script.doc.value.endswith('\n') else '%s\\n', docname=self.settings.script.doc.name, ) if self.settings.add_version: body += ''' for idx in "${_do_pp[@]}"; do [[ $idx = 'a' ]] && continue if [[ ${_do_lo[$idx]} = "--version" ]]; then printf "%s\\n" "$version" exit 0 fi done ''' # setup $_lft body += ''' local i=0 while [[ $i -lt ${#_do_pp[@]} ]]; do _lft+=("$i") ((i++)) done ''' # run the parser body += ''' if ! {root} || [ ${{#_lft[@]}} -gt 0 ]; then _do_err fi '''.format(root=self.root_node.body) # defaults if len(self.leaf_nodes) > 0: body += ''' _do_def ''' # teardown if self.settings.add_teardown: body += ''' unset _do_av _do_sh _do_lo _do_ac _do_pn _lft _do_pp _do_pv _do_tm unset -f _do_eith _do_oom _do_opt _do_req _do_cmd _do_sw _do_val _do_def \\ _do_err _do_long _do_shorts docopt ''' body += ''' return 0 ''' return body PKNbf$docopt_sh/bash/helpers/parse_long.pyfrom .. import Function class ParseLong(Function): def __init__(self, settings): super(ParseLong, self).__init__(settings, '_do_long') @property def body(self): body = ''' local token=${_do_av[0]} local long=${token%%=*} local value=${token#*=} local argcount _do_av=("${_do_av[@]:1}") [[ $token = --* ]] || assert_fail if [[ $token = *=* ]]; then eq='=' else eq='' value=false fi local i=0 local similar=() local similar_idx=false for o in "${_do_lo[@]}"; do if [[ $o = "$long" ]]; then similar+=("$long") [[ $similar_idx = false ]] && similar_idx=$i fi ((i++)) done if [[ ${#similar[@]} -eq 0 ]]; then i=0 for o in "${_do_lo[@]}"; do if [[ $o = $long* ]]; then similar+=("$long") [[ $similar_idx = false ]] && similar_idx=$i fi ((i++)) done fi if [[ ${#similar[@]} -gt 1 ]]; then _do_err "$(printf "%s is not a unique prefix: %s?" "$long" "${similar[*]}")" elif [[ ${#similar[@]} -lt 1 ]]; then if [[ $eq = '=' ]]; then argcount=1 else argcount=0 fi similar_idx=${#_do_sh[@]} if [[ $argcount -eq 0 ]]; then value=true fi _do_sh+=('') _do_lo+=("$long") _do_ac+=("$argcount") else if [[ ${_do_ac[$similar_idx]} -eq 0 ]]; then if [[ $value != false ]]; then _do_err "$(printf "%s must not have an argument" "${_do_lo[$similar_idx]}")" fi elif [[ $value = false ]]; then if [[ ${#_do_av[@]} -eq 0 || ${_do_av[0]} = '--' ]]; then _do_err "$(printf "%s requires argument" "$long")" fi value=${_do_av[0]} _do_av=("${_do_av[@]:1}") fi if [[ $value = false ]]; then value=true fi fi _do_pp+=("$similar_idx") _do_pv+=("$value") ''' return body PK梬N= P&docopt_sh/bash/helpers/parse_shorts.pyfrom .. import Function class ParseShorts(Function): def __init__(self, settings): super(ParseShorts, self).__init__(settings, '_do_shorts') @property def body(self): body = ''' local token=${_do_av[0]} local value _do_av=("${_do_av[@]:1}") [[ $token = -* && $token != --* ]] || assert_fail local remaining=${token#-} while [[ -n $remaining ]]; do local short="-${remaining:0:1}" remaining="${remaining:1}" local i=0 local similar=() local similar_idx=false for o in "${_do_sh[@]}"; do if [[ $o = "$short" ]]; then similar+=("$short") [[ $similar_idx = false ]] && similar_idx=$i fi ((i++)) done if [[ ${#similar[@]} -gt 1 ]]; then _do_err "$(printf "%s is specified ambiguously %d times" "$short" "${#similar[@]}")" elif [[ ${#similar[@]} -lt 1 ]]; then similar_idx=${#_do_sh[@]} value=true _do_sh+=("$short") _do_lo+=('') _do_ac+=(0) else value=false if [[ ${_do_ac[$similar_idx]} -ne 0 ]]; then if [[ $remaining = '' ]]; then if [[ ${#_do_av[@]} -eq 0 || ${_do_av[0]} = '--' ]]; then _do_err "$(printf "%s requires argument" "$short")" fi value=${_do_av[0]} _do_av=("${_do_av[@]:1}") else value=$remaining remaining='' fi fi if [[ $value = false ]]; then value=true fi fi _do_pp+=("$similar_idx") _do_pv+=("$value") done ''' return body PK瘬NE>docopt_sh/bash/tree/__init__.pyfrom .command import Command from .either import Either from .node import BranchNode from .node import LeafNode from .oneormore import OneOrMore from .optional import Optional from .required import Required from .switch import Switch from .value import Value PKרN~F̾docopt_sh/bash/tree/command.pyfrom .. import Function class Command(Function): def __init__(self, settings): super(Command, self).__init__(settings, '_do_cmd') @property def body(self): # $1=variable name # $2=name of the command # $3=should the value be incremented? body = ''' local i local name=${2:-$1} for i in "${!_lft[@]}"; do local l=${_lft[$i]} if [[ ${_do_pp[$l]} = 'a' ]]; then if [[ ${_do_pv[$l]} != "$name" ]]; then return 1 fi _lft=("${_lft[@]:0:$i}" "${_lft[@]:((i+1))}") $_do_tm && return 0 if [[ $3 = true ]]; then eval "(($1++))" else eval "$1=true" fi return 0 fi done return 1 ''' return body PKuN1RRdocopt_sh/bash/tree/either.pyfrom .. import Function class Either(Function): def __init__(self, settings): super(Either, self).__init__(settings, '_do_eith') @property def body(self): body = ''' local i_lft=("${_lft[@]}") local best local p_lft local p local unset_tm=true $_do_tm && unset_tm=false _do_tm=true for p in "$@"; do if "_do$p"; then if [[ -z $p_lft || ${#_lft[@]} -lt $p_lft ]]; then best=_do$p p_lft=${#_lft[@]} fi fi _lft=("${i_lft[@]}") done $unset_tm && _do_tm=false if [[ -n $best ]]; then $best return 0 fi _lft=("${i_lft[@]}") return 1 ''' return body PKNxxdocopt_sh/bash/tree/node.pyfrom .. import Function, bash_variable_name, bash_ifs_value from ...doc import Option, Command, Argument, Required, Optional, OptionsShortcut, OneOrMore, Either helper_map = { Required: '_do_req', Optional: '_do_opt', OptionsShortcut: '_do_opt', OneOrMore: '_do_oom', Either: '_do_eith', } class Node(Function): def __init__(self, settings, pattern, idx): self.type = type(pattern) self.idx = idx super(Node, self).__init__(settings, '_do' + str(idx)) class BranchNode(Node): def __init__(self, settings, pattern, idx, function_map): super(BranchNode, self).__init__(settings, pattern, idx) self.helper_name = helper_map[self.type] self.child_indexes = map(lambda child: function_map[child].idx, pattern.children) @property def body(self): # minimize arg list by only specifying node idx body = ' '.join([self.helper_name] + list(map(str, self.child_indexes))) return body class LeafNode(Node): def __init__(self, settings, pattern, idx): super(LeafNode, self).__init__(settings, pattern, idx) self.default_value = pattern.value self.pattern = pattern if self.type is Option: self.helper_name = '_do_sw' if type(self.default_value) in [bool, int] else '_do_val' self.needle = idx elif self.type is Command: self.helper_name = '_do_cmd' self.needle = pattern.name else: self.helper_name = '_do_val' self.needle = 'a' self.multiple = type(self.default_value) in [list, int] self.variable_name = bash_variable_name(pattern.name, settings.name_prefix) @property def body(self): args = [self.variable_name, bash_ifs_value(self.needle)] if self.multiple: args.append(bash_ifs_value(self.multiple)) if self.helper_name == '_do_cmd' and args[0] == args[1] and len(args) == 2: args = [args[0]] body = ' '.join([self.helper_name] + args) return body PK|Nn4 docopt_sh/bash/tree/oneormore.pyfrom .. import Function class OneOrMore(Function): def __init__(self, settings): super(OneOrMore, self).__init__(settings, '_do_oom') @property def body(self): # This entire $prev thing doesn't make sense. # I couldn't find a case anywhere, where we would match something # but not remove something from $_lft. body = ''' local i=0 local prev=${#_lft[@]} while "_do$1"; do ((i++)) [[ $prev -eq ${#_lft[@]} ]] && break prev=${#_lft[@]} done if [[ $i -ge 1 ]]; then return 0 fi return 1 ''' return body PKzN(.docopt_sh/bash/tree/optional.pyfrom .. import Function class Optional(Function): def __init__(self, settings): super(Optional, self).__init__(settings, '_do_opt') @property def body(self): body = ''' local p for p in "$@"; do "_do$p" done return 0 ''' return body PKxNZdocopt_sh/bash/tree/required.pyfrom .. import Function class Required(Function): def __init__(self, settings): super(Required, self).__init__(settings, '_do_req') @property def body(self): body = ''' local init_lft=("${_lft[@]}") local p local unset_tm=true $_do_tm && unset_tm=false _do_tm=true for p in "$@"; do if ! "_do$p"; then _lft=("${init_lft[@]}") $unset_tm && _do_tm=false return 1 fi done if $unset_tm; then _do_tm=false _lft=("${init_lft[@]}") for p in "$@"; do "_do$p" done fi return 0 ''' return body PK᤬N9y[[docopt_sh/bash/tree/switch.pyfrom .. import Function class Switch(Function): def __init__(self, settings): super(Switch, self).__init__(settings, '_do_sw') @property def body(self): # $1=variable name # $2=option idx to find in the options list # $3=should the value be incremented? body = ''' local i for i in "${!_lft[@]}"; do local l=${_lft[$i]} if [[ ${_do_pp[$l]} = "$2" ]]; then _lft=("${_lft[@]:0:$i}" "${_lft[@]:((i+1))}") $_do_tm && return 0 if [[ $3 = true ]]; then eval "(($1++))" else eval "$1=true" fi return 0 fi done return 1 ''' return body PKפNxdocopt_sh/bash/tree/value.pyfrom .. import Function class Value(Function): def __init__(self, settings): super(Value, self).__init__(settings, '_do_val') @property def body(self): # $1=variable name # $2=option idx to find in the options list # $3=is it a list? body = ''' local i for i in "${!_lft[@]}"; do local l=${_lft[$i]} if [[ ${_do_pp[$l]} = "$2" ]]; then _lft=("${_lft[@]:0:$i}" "${_lft[@]:((i+1))}") $_do_tm && return 0 local value value=$(printf -- "%q" "${_do_pv[$l]}") if [[ $3 = true ]]; then eval "$1+=($value)" else eval "$1=$value" fi return 0 fi done return 1 ''' return body PK!HY05*docopt_sh-0.7.3.dist-info/entry_points.txtN+I/N.,()JO/(+ΰ3s3@PK梬N(##!docopt_sh-0.7.3.dist-info/LICENSECopyright (c) 2019 Anders Ingemann Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HPOdocopt_sh-0.7.3.dist-info/WHEEL HM K-*ϳR03rOK-J,/RH,szd&Y)r$[)T&UrPK!H1t"docopt_sh-0.7.3.dist-info/METADATAMOA +,z춍JH%"B@M gg쎔`_o)6X%1d7 /g5CGP:QT|+j9i' !΃#9,m,J&?$=]Ԩ6 FH3zk ڝZR辳 )3 W5\'Yg3y֋mL.7:eVg+YƋm LveR>Sh:'a=k)[q|P3ؚL_N{ɡͼ_ٸeĜ0Q鍱o#/YaBY[tNS yJ!)5{ha)wQjN<  (PK!H#Vs docopt_sh-0.7.3.dist-info/RECORD}ɒH}fHE/PD@aC0$̣ӷ]5re9Q ~*|$w"&kW(yܟE^/!*(J!5N[\c_ +eOn8gyXrޝ"|jfqڈR|9^n9qFmM@bw `&j>k(YIM[aZ?|KM;:@[MS;]y,˚aeU4 ݇AXj4Ϲ""pPx'?t~ ŜłQOL99ϽmO'/٨.ˠW$qN#$O٧%/ހzECB(SU =*#h7*`4^F!O{G:Do"JJ?x=#&*}5^!^dW;qS>B/h .6CVWAb9^i& 諘B?G C\k[zvq6T/\޾snLE `ʭ1A|ln20HʴaZyzd۽reͼoL@Pz)PumaaC&V۰ҵp'jl7x~dURΪW͆L_QxҐqXݒ=kfb;tv9Uk?\vfOc0Di~ث1 .T@)40#:2 PCɟnKve'*$Qtj(]i8,GRUx~3!/s*X&t9큜docopt_sh/bash/tree/__init__.pyPKרN~F̾܅docopt_sh/bash/tree/command.pyPKuN1RR큸docopt_sh/bash/tree/either.pyPKNxxEdocopt_sh/bash/tree/node.pyPK|Nn4 docopt_sh/bash/tree/oneormore.pyPKzN(.Rdocopt_sh/bash/tree/optional.pyPKxNZ큏docopt_sh/bash/tree/required.pyPK᤬N9y[[docopt_sh/bash/tree/switch.pyPKפNxwdocopt_sh/bash/tree/value.pyPK!HY05*:docopt_sh-0.7.3.dist-info/entry_points.txtPK梬N(##!큲docopt_sh-0.7.3.dist-info/LICENSEPK!HPOdocopt_sh-0.7.3.dist-info/WHEELPK!H1t"docopt_sh-0.7.3.dist-info/METADATAPK!H#Vs Udocopt_sh-0.7.3.dist-info/RECORDPKG