PK`NS\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.0' PK`N0docopt_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)) sys.exit(1) def main(): params = docopt(__doc__) docopt_sh(params) if __name__ == '__main__': main() PK YNpK}-}-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 self.settings = settings self.doc = doc self.root, self.usage_match = parse_doc(doc) self.nodes = OrderedDict([]) param_sort_order = [Option, Argument, Command] unique_params = list(OrderedDict.fromkeys(self.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): self.nodes[param] = LeafNode(settings, param, idx) for idx, pattern in enumerate(iter(self.root)): if isinstance(pattern, BranchPattern): self.nodes[pattern] = BranchNode(settings, pattern, idx, self.nodes) self.nodes[self.root].name = 'root' @property def functions(self): return list(self.nodes.values()) @property def sorted_params(self): params = [Option, Argument, Command] return [key for key in self.nodes.keys() if type(key) in params] @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:]) + ' )' PKYN docopt_sh/parser.pyfrom .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): all_functions = self.doc_ast.functions + [ 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), helpers.ParseShorts(self.settings), helpers.ParseLong(self.settings), helpers.ParseArgv(self.settings), helpers.Help(self.settings), helpers.Error(self.settings, usage_section=self.doc_ast.usage_section), helpers.Extras(self.settings), helpers.Setup(self.settings, sorted_params=self.doc_ast.sorted_params), helpers.Teardown(self.settings), helpers.Check(self.settings), helpers.Defaults(self.settings, sorted_params=self.doc_ast.sorted_params), helpers.Main(self.settings), ] parser_str = '\n'.join([str(function) for function in all_functions if function.include()]) 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 += ' ' + 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 PK뎫Ndocopt_sh/bash/__init__.pyimport re from shlex import quote class Function(object): def __init__(self, settings, name): self.name = name self.settings = settings def include(self): return True def fn_wrap(self, script): return '{name}() {{\n{script}\n}}'.format(name=self.name, script=script.strip()) 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): no_separator = re.compile(r'; (then|do)$|else$|\{$') comment = re.compile(r'^\s*#') current = None for line in lines: if not current: current = line continue if comment.match(line): if current: yield current yield line current = None continue separator = ' ' if no_separator.search(current) else '; ' if len(current + separator + line) < max_length: current += separator + line else: yield current current = line if current != '': yield current PKbNMā<<"docopt_sh/bash/helpers/__init__.pyfrom .check import Check from .defaults import Defaults from .error import Error from .extras import Extras from .help import Help from .main import Main from .parse_argv import ParseArgv from .parse_long import ParseLong from .parse_shorts import ParseShorts from .setup import Setup from .teardown import Teardown PKL{N8*Cdocopt_sh/bash/helpers/check.pyfrom .. import Function import hashlib class Check(Function): def __init__(self, settings): super(Check, self).__init__(settings, 'check') self.digest = hashlib.sha256(settings.script.doc.value.encode('utf-8')).hexdigest() def include(self): return self.settings.add_doc_check def __str__(self): script = ''' local current_doc_hash local digest="{digest}" current_doc_hash=$(printf "%s" "${docname}" | shasum -a 256 | cut -f 1 -d " ") if [[ $current_doc_hash != "$digest" ]]; then printf "The current usage doc (%s) does not match what the parser was generated with (%s)\\n" \\ "$current_doc_hash" "$digest" >&2 exit 1 fi '''.format(docname=self.settings.script.doc.name, digest=self.digest) return self.fn_wrap(script) PKNm֟"""docopt_sh/bash/helpers/defaults.pyfrom .. import Function, bash_variable_name, bash_variable_value class Defaults(Function): def __init__(self, settings, sorted_params): super(Defaults, self).__init__(settings, 'defaults') self.sorted_params = sorted_params def include(self): return len(self.sorted_params) > 0 def __str__(self): defaults = [ "[[ -z ${{{name}+x}} ]] && {name}={default}".format( name=bash_variable_name(p.name, self.settings.name_prefix), default=bash_variable_value(p.value) ) if type(p.value) is list else "{name}=${{{name}:-{default}}}".format( name=bash_variable_name(p.name, self.settings.name_prefix), default=bash_variable_value(p.value) ) for p in self.sorted_params ] script = '\n'.join(defaults) return self.fn_wrap(script) PKYN/GGdocopt_sh/bash/helpers/error.pyfrom .. import Function class Error(Function): def __init__(self, settings, usage_section): super(Error, self).__init__(settings, 'error') self.usage_start, self.usage_end = usage_section if self.settings.script.doc.value.endswith('\n'): self.usage_end -= 1 def __str__(self): script = ''' [[ -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 self.fn_wrap(script) PKN] docopt_sh/bash/helpers/extras.pyfrom .. import Function class Extras(Function): def __init__(self, settings): super(Extras, self).__init__(settings, 'extras') def include(self): return self.settings.add_help or self.settings.add_version def __str__(self): script = '' if self.settings.add_help: script += ''' for idx in "${parsed_params[@]}"; do [[ $idx == 'a' ]] && continue if [[ ${options_short[$idx]} == "-h" || ${options_long[$idx]} == "--help" ]]; then help exit 0 fi done ''' if self.settings.add_version: script += ''' for idx in "${parsed_params[@]}"; do [[ $idx == 'a' ]] && continue if [[ ${options_long[$idx]} == "--version" ]]; then printf "%s\\n" "$version" exit 0 fi done ''' return self.fn_wrap(script) PKiZNx docopt_sh/bash/helpers/help.pyfrom .. import Function class Help(Function): def __init__(self, settings): super(Help, self).__init__(settings, 'help') if self.settings.script.doc.value.endswith('\n'): self.printf_template = "%s" else: self.printf_template = "%s\\n" def __str__(self): script = ''' printf -- "{template}" "${docname}" '''.format( template=self.printf_template, docname=self.settings.script.doc.name, ) return self.fn_wrap(script) PKXNݵdocopt_sh/bash/helpers/main.pyfrom .. import Function class Main(Function): def __init__(self, settings): super(Main, self).__init__(settings, 'docopt') def __str__(self): script = ''' type check &>/dev/null && check setup "$@" parse_argv extras local i=0 while [[ $i -lt ${#parsed_params[@]} ]]; do left+=("$i"); ((i++)); done if ! root; then error fi type defaults &>/dev/null && defaults if [[ ${#left[@]} -gt 0 ]]; then error fi type teardown &>/dev/null && teardown return 0 ''' return self.fn_wrap(script) PKZNAv`[[$docopt_sh/bash/helpers/parse_argv.pyfrom .. import Function class ParseArgv(Function): def __init__(self, settings): super(ParseArgv, self).__init__(settings, 'parse_argv') def __str__(self): script = ''' while [[ ${#argv[@]} -gt 0 ]]; do if [[ ${argv[0]} == "--" ]]; then for arg in "${argv[@]}"; do parsed_params+=('a') parsed_values+=("$arg") done return elif [[ ${argv[0]} = --* ]]; then parse_long elif [[ ${argv[0]} == -* && ${argv[0]} != "-" ]]; then parse_shorts ''' if self.settings.options_first: script += ''' else for arg in "${argv[@]}"; do parsed_params+=('a') parsed_values+=("$arg") done return ''' else: script += ''' else parsed_params+=('a') parsed_values+=("${argv[0]}") argv=("${argv[@]:1}") ''' script += ''' fi done ''' return self.fn_wrap(script) PKN8$C$docopt_sh/bash/helpers/parse_long.pyfrom .. import Function class ParseLong(Function): def __init__(self, settings): super(ParseLong, self).__init__(settings, 'parse_long') def __str__(self): script = ''' token=${argv[0]} long=${token%%=*} value=${token#*=} argv=("${argv[@]: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 "${options_long[@]}"; 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 "${options_long[@]}"; do if [[ $o == $long* ]]; then similar+=("$long") [[ $similar_idx == false ]] && similar_idx=$i fi ((i++)) done fi if [[ ${#similar[@]} -gt 1 ]]; then error "$(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=${#options_short[@]} if [[ argcount -eq 0 ]]; then value=true fi options_short+=('') options_long+=("$long") options_argcount+=("$argcount") else if [[ ${options_argcount[$similar_idx]} -eq 0 ]]; then if [[ $value != false ]]; then error "$(printf "%s must not have an argument" "${options_long[$similar_idx]}")" fi else if [[ $value == false ]]; then if [[ ${#argv[@]} -eq 0 || ${argv[0]} == '--' ]]; then error "$(printf "%s requires argument" "$long")" fi value=${argv[0]} argv=("${argv[@]:1}") fi fi if [[ $value == false ]]; then value=true fi fi parsed_params+=("$similar_idx") parsed_values+=("$value") ''' return self.fn_wrap(script) PKNӿ&docopt_sh/bash/helpers/parse_shorts.pyfrom .. import Function class ParseShorts(Function): def __init__(self, settings): super(ParseShorts, self).__init__(settings, 'parse_shorts') def __str__(self): script = ''' token=${argv[0]} argv=("${argv[@]:1}") [[ $token == -* && $token != --* ]] || assert_fail local remaining=${token#-} while [[ -n $remaining ]]; do short="-${remaining:0:1}" remaining="${remaining:1}" local i=0 local similar=() local similar_idx=false for o in "${options_short[@]}"; do if [[ $o == "$short" ]]; then similar+=("$short") [[ $similar_idx == false ]] && similar_idx=$i fi ((i++)) done if [[ ${#similar[@]} -gt 1 ]]; then error "$(printf "%s is specified ambiguously %d times" "$short" "${#similar[@]}")" elif [[ ${#similar[@]} -lt 1 ]]; then similar_idx=${#options_short[@]} value=true options_short+=("$short") options_long+=('') options_argcount+=(0) else value=false if [[ ${options_argcount[$similar_idx]} -ne 0 ]]; then if [[ $remaining == '' ]]; then if [[ ${#argv[@]} -eq 0 || ${argv[0]} == '--' ]]; then error "$(printf "%s requires argument" "$short")" fi value=${argv[0]} argv=("${argv[@]:1}") else value=$remaining remaining='' fi fi if [[ $value == false ]]; then value=true fi fi parsed_params+=("$similar_idx") parsed_values+=("$value") done ''' return self.fn_wrap(script) PK.NzNdocopt_sh/bash/helpers/setup.pyfrom .. import Function, bash_variable_name, bash_ifs_value from ...doc import Option class Setup(Function): def __init__(self, settings, sorted_params): super(Setup, self).__init__(settings, 'setup') self.sorted_params = sorted_params def __str__(self): sorted_options = [o for o in self.sorted_params if type(o) is Option] script = ''' argv=("$@") options_short=({options_short}) options_long=({options_long}) options_argcount=({options_argcount}) param_names=({param_names}) parsed_params=() parsed_values=() left=() test_match=false for var in "${{param_names[@]}}"; do unset "$var"; done '''.format( options_short=' '.join([bash_ifs_value(o.short) for o in sorted_options]), options_long=' '.join([bash_ifs_value(o.long) for o in sorted_options]), options_argcount=' '.join([bash_ifs_value(o.argcount) for o in sorted_options]), param_names=' '.join([bash_variable_name(p.name, self.settings.name_prefix) for p in self.sorted_params]), ) return self.fn_wrap(script) PK%Nq**"docopt_sh/bash/helpers/teardown.pyfrom .. import Function class Teardown(Function): def __init__(self, settings): super(Teardown, self).__init__(settings, 'teardown') def include(self): return self.settings.add_teardown def __str__(self): script = ''' unset argv options_short options_long options_argcount param_names \\ parsed_params parsed_values left test_match unset -f either oneormore optional required _command _switch _value \\ check defaults extras help error docopt \\ parse_argv parse_long parse_shorts setup teardown ''' return self.fn_wrap(script) PKNE>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 PKaN+docopt_sh/bash/tree/command.pyfrom .. import Function class Command(Function): def __init__(self, settings): super(Command, self).__init__(settings, '_command') def __str__(self): # $1=variable name # $2=should the value be incremented? # $3=name of the command script = ''' local i for i in "${!left[@]}"; do local l=${left[$i]} if [[ ${parsed_params[$l]} == 'a' ]]; then if [[ ${parsed_values[$l]} != "$3" ]]; then return 1 fi left=("${left[@]:0:$i}" "${left[@]:((i+1))}") $test_match && return 0 if [[ $2 == true ]]; then eval "(($1++))" else eval "$1=true" fi return 0 fi done return 1 ''' return self.fn_wrap(script) PKN docopt_sh/bash/tree/either.pyfrom .. import Function class Either(Function): def __init__(self, settings): super(Either, self).__init__(settings, 'either') def __str__(self): script = ''' local initial_left=("${left[@]}") local best_match local previous_best local pattern local unset_test_match=true $test_match && unset_test_match=false test_match=true for pattern in "$@"; do if $pattern; then if [[ -z $previous_best || ${#left[@]} -lt $previous_best ]]; then best_match=$pattern previous_best=${#left[@]} fi fi left=("${initial_left[@]}") done $unset_test_match && test_match=false if [[ -n $best_match ]]; then $best_match return 0 fi left=("${initial_left[@]}") return 1 ''' return self.fn_wrap(script) PKNdocopt_sh/bash/tree/node.pyfrom .. import Function, bash_variable_name, bash_ifs_value from ...doc import Option, Command, Argument, Required, Optional, OptionsShortcut, OneOrMore, Either prefix_map = { Option: 'arg', Command: 'cmd', Argument: 'opt', Required: 'req', Optional: 'optional', OptionsShortcut: 'options', OneOrMore: 'oneormore', Either: 'either', } class Node(Function): def __init__(self, settings, pattern, idx): name = prefix_map[type(pattern)] + str(idx) super(Node, self).__init__(settings, name) class BranchNode(Node): def __init__(self, settings, pattern, idx, function_map): if type(pattern) is OptionsShortcut: self.helper_name = 'optional' else: self.helper_name = pattern.__class__.__name__.lower() self.child_names = map(lambda child: function_map[child].name, pattern.children) super(BranchNode, self).__init__(settings, pattern, idx) def __str__(self): script = ' '.join([self.helper_name] + list(self.child_names)) return self.fn_wrap(script) class LeafNode(Node): def __init__(self, settings, pattern, idx): if type(pattern) is Option: self.helper_name = '_switch' if type(pattern.value) in [bool, int] else '_value' self.needle = idx elif type(pattern) is Command: self.helper_name = '_command' self.needle = pattern.name else: self.helper_name = '_value' self.needle = None self.variable_name = bash_variable_name(pattern.name, settings.name_prefix) self.multiple = type(pattern.value) in [list, int] super(LeafNode, self).__init__(settings, pattern, idx) def __str__(self): script = ' '.join([ self.helper_name, self.variable_name, bash_ifs_value(self.multiple), bash_ifs_value(self.needle) ]) return self.fn_wrap(script) PKN docopt_sh/bash/tree/oneormore.pyfrom .. import Function class OneOrMore(Function): def __init__(self, settings): super(OneOrMore, self).__init__(settings, 'oneormore') def __str__(self): script = ''' local times=0 # shellcheck disable=SC2154 local previous_left=${#left[@]} while $1; do ((times++)) if [[ $previous_left -eq ${#left[@]} ]]; then # This entire $previous_left thing doesn't make sense. # I couldn't find a case anywhere, where we would match something # but not remove something from $left. break fi previous_left=${#left[@]} done if [[ $times -ge 1 ]]; then return 0 fi return 1 ''' return self.fn_wrap(script) PKNV_bdocopt_sh/bash/tree/optional.pyfrom .. import Function class Optional(Function): def __init__(self, settings): super(Optional, self).__init__(settings, 'optional') def __str__(self): script = ''' local pattern for pattern in "$@"; do $pattern done return 0 ''' return self.fn_wrap(script) PKN0jjdocopt_sh/bash/tree/required.pyfrom .. import Function class Required(Function): def __init__(self, settings): super(Required, self).__init__(settings, 'required') def __str__(self): script = ''' local initial_left=("${left[@]}") local pattern local unset_test_match=true $test_match && unset_test_match=false test_match=true for pattern in "$@"; do if ! $pattern; then left=("${initial_left[@]}") $unset_test_match && test_match=false return 1 fi done if $unset_test_match; then test_match=false left=("${initial_left[@]}") for pattern in "$@"; do $pattern; done fi return 0 ''' return self.fn_wrap(script) PKYN򃰠rrdocopt_sh/bash/tree/switch.pyfrom .. import Function class Switch(Function): def __init__(self, settings): super(Switch, self).__init__(settings, '_switch') def __str__(self): # $1=variable name # $2=should the value be incremented? # $3=option idx to find in the options list script = ''' local i for i in "${!left[@]}"; do local l=${left[$i]} if [[ ${parsed_params[$l]} == "$3" ]]; then left=("${left[@]:0:$i}" "${left[@]:((i+1))}") $test_match && return 0 if [[ $2 == true ]]; then eval "(($1++))" else eval "$1=true" fi return 0 fi done return 1 ''' return self.fn_wrap(script) PKN],t$docopt_sh/bash/tree/value.pyfrom .. import Function class Value(Function): def __init__(self, settings): super(Value, self).__init__(settings, '_value') def __str__(self): # $1=variable name # $2=is it a list? # $3=option idx to find in the options list script = ''' local i local needle=${3:-'a'} for i in "${!left[@]}"; do local l=${left[$i]} if [[ ${parsed_params[$l]} == "$needle" ]]; then left=("${left[@]:0:$i}" "${left[@]:((i+1))}") $test_match && return 0 local value value=$(printf -- "%q" "${parsed_values[$l]}") if [[ $2 == true ]]; then eval "$1+=($value)" else eval "$1=$value" fi return 0 fi done return 1 ''' return self.fn_wrap(script) PK!HY05*docopt_sh-0.7.0.dist-info/entry_points.txtN+I/N.,()JO/(+ΰ3s3@PKD^N(##!docopt_sh-0.7.0.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.0.dist-info/WHEEL HM K-*ϳR03rOK-J,/RH,szd&Y)r$[)T&UrPK!H3]tt"docopt_sh-0.7.0.dist-info/METADATAMOA +,zBQ* /5=;;fwGg{4iPncekO A)+T)::DֹU8V9L=Qq鈍1WeXnUQW7|.֒nz;amH f0 9~u^؝ZRWge= )3~:2 ɳ^ntdp)\cޒ4^hkd"k˖+yB9i cM']{yg*v&l1a))505AĔ)ACrHJ-[dXZOCtuJ=PK!H docopt_sh-0.7.0.dist-info/RECORD}DzH}? TAY <$XmuMt=uo X|dg])3Aߠ)16k0񀽓DV<=WrƪgTdm BOk¢g(Jw;<,Tm p/\7sp|@zbNU±RLqf}U`ኮXE)@Eq{C"[ x\|eSB]gV)m-AAߩm籴J "o{5^e<qݢ;arz5 ߩ?+raQǫ7`3Fʫf'I_5k @ϮuD qWMm"|&Sލ8$ѕt$~dNrR6P= wP&ua3.E8t jcҭNHQ(`# :.Ga 'R/)sE1M |kq4aːVy9t+6WMYyXD_i%%2vKc"r/lQh Gv|_F'vIz B5\sUę#L}޻jB䗬ivmڍM7aSݻ1c;"?!fޣjL6U]χkνq6bs pz'Hϥ5;\](+v) %F*I0THi 1R EEdAvGE B>c(?g&zԋ])]JYsseqD-VbH"7I" Y%8D+]>IAPK`NS\docopt_sh/__init__.pyPK`N0docopt_sh/__main__.pyPK YNpK}-}-큸 docopt_sh/doc.pyPKYN c7docopt_sh/parser.pyPK-]NQO큿docopt_sh/bash/tree/__init__.pyPKaN+docopt_sh/bash/tree/command.pyPKN docopt_sh/bash/tree/either.pyPKNdocopt_sh/bash/tree/node.pyPKN >docopt_sh/bash/tree/oneormore.pyPKNV_bdocopt_sh/bash/tree/optional.pyPKN0jjOdocopt_sh/bash/tree/required.pyPKYN򃰠rrdocopt_sh/bash/tree/switch.pyPKN],t$큣docopt_sh/bash/tree/value.pyPK!HY05*docopt_sh-0.7.0.dist-info/entry_points.txtPKD^N(##!docopt_sh-0.7.0.dist-info/LICENSEPK!HPOxdocopt_sh-0.7.0.dist-info/WHEELPK!H3]tt"docopt_sh-0.7.0.dist-info/METADATAPK!H docopt_sh-0.7.0.dist-info/RECORDPK