PKрNydocopt_sh/__init__.py""" docopt.sh - Bash argument parser generator. This program looks for `DOC="... Usage​: ..."` in a script and appends a matching parser to it. """ # The "... Usage: ..." above contains a zero-width space between `Usage` and `:` # in order to prevent docopt from parsing it as a `usage:`` section __all__ = ['docopt_sh'] __version__ = '0.9.1' class DocoptError(Exception): def __init__(self, message, exit_code=1): self.message = message self.exit_code = exit_code PKpzN[W docopt_sh/__main__.py#!/usr/bin/env python3 import sys import os import docopt import logging import termcolor from . import __doc__ as pkg_doc, __name__ as root_name, DocoptError, __version__ from .parser import ParserParameters, Parser from .script import Script log = logging.getLogger(root_name) __doc__ = pkg_doc + """ Usage: docopt.sh generate-library docopt.sh [options] [SCRIPT] Options: --prefix PREFIX Parameter variable name prefix (default: "") --line-length N Max line length when minifying (0 to disable, default: 80) --library -l SRC Generates only the dynamic part of the parser and includes the static parts using `source SRC`, use `generate-library` to create that file --no-auto-params Disable auto-detection parser generation parameters --parser Output parser instead of inserting it in the script -h --help This help message --version Version of this program Note: You can pass the script on stdin as well, docopt.sh will then output the modified script to stdout. Parameters: You can set the following global variables before invoking docopt with `docopt "$@"` to change the behavior of docopt. $DOCOPT_PROGRAM_VERSION The string to print when --version is specified [default: none/disabled] $DOCOPT_ADD_HELP Set to `false` to not print usage on --help [default: true] $DOCOPT_OPTIONS_FIRST Set to `true` to fail when options are specified after arguments/commands [default: false] """ def docopt_sh(params): if params['generate-library']: parser = Parser(params) sys.stdout.write('#!/usr/bin/env bash\n\n' + str(parser.generate_library(check_version=True))) else: try: if params['SCRIPT'] is None: if sys.stdin.isatty(): raise docopt.DocoptExit( 'Not reading from stdin when it is a tty. ' 'Use either `docopt.sh script.sh` or `docopt.sh < script.sh`.' ) script = Script(sys.stdin.read()) else: with open(params['SCRIPT'], 'r') as h: script = Script(h.read(), params['SCRIPT']) script.validate() parser_parameters = ParserParameters(params, script) parser = Parser(parser_parameters) if params['--parser']: sys.stdout.write(parser.generate(script)) else: patched_script = script.patch(parser) if params['SCRIPT'] is None: sys.stdout.write(str(patched_script)) else: with open(params['SCRIPT'], 'w') as h: h.write(str(patched_script)) if patched_script == script: log.info('The parser in %s is already up-to-date.', params['SCRIPT']) else: log.info('%s has been updated.', params['SCRIPT']) except DocoptError as e: log.error(str(e)) sys.exit(e.exit_code) def setup_logging(): level_colors = { logging.ERROR: 'red', logging.WARN: 'yellow', } class ColorFormatter(logging.Formatter): def format(self, record): record.msg = termcolor.colored(record.msg, level_colors.get(record.levelno, None)) return super(ColorFormatter, self).format(record) stderr = logging.StreamHandler(sys.stderr) if os.isatty(2): stderr.setFormatter(ColorFormatter()) log.setLevel(level=logging.INFO) log.addHandler(stderr) def main(): setup_logging() params = docopt.docopt(__doc__, version=__version__) docopt_sh(params) if __name__ == '__main__': main() PKCLNodocopt_sh/bash.pyimport re from shlex import quote from collections import OrderedDict class Code(object): def __init__(self, code): self._code = self._get_list(code) @property def code(self): return self._code def minify(self, max_line_length): return minify(str(self), max_line_length) def _get_list(self, elem): if type(elem) is list: return elem elif type(elem) is OrderedDict: return list(elem.values()) elif type(elem) is str: return [elem] elif isinstance(elem, Function): return [elem] elif type(elem) is Code: return elem.code else: raise Exception('Unhandled data-type: %s' % type(elem)) def __add__(self, other): return Code(self.code + self._get_list(other)) def __str__(self): return '\n'.join(map(str, self.code)) class Function(Code): def __init__(self, name): self.name = name @property def code(self): return [str(self)] def __str__(self): return '{name}(){{\n{body}\n}}\n'.format(name=self.name, body=self.body) def __repr__(self): lines = self.body.split('\n') if len(lines) > 5: shortened_body = '\n'.join(lines[:2]) + '\n ...\n' + '\n'.join(lines[-2:]) else: shortened_body = self.body return '{name}(){{\n{body}\n}}'.format(name=self.name, body=shortened_body) class HelperTemplate(Function): def __init__(self, name, function_body): self.function_body = function_body super(HelperTemplate, self).__init__(name) def render(self, replacements={}): return Helper(self, replacements) @property def body(self): return self.function_body class Helper(Function): def __init__(self, template, replacements): self.template = template self.replacements = replacements super(Helper, self).__init__(template.name) @property def body(self): body = self.template.function_body for placeholder, replacement in self.replacements.items(): body = body.replace(placeholder, replacement) return body def indent(script, level=1): indentation = ' ' * level return '\n'.join(map(lambda l: indentation + l, script.split('\n'))) def bash_variable_name(name, prefix=''): return prefix + re.sub(r'^[^a-z_]|[^a-z0-9_]', '_', name, 0, re.IGNORECASE) 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 minify(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) + '\n' 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 PKerN21V--docopt_sh/doc_ast.pyimport re from collections import OrderedDict from itertools import chain class DocAst(object): def __init__(self, doc, name_prefix): from .node import BranchNode, LeafNode doc = doc root, self.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(param, idx, name_prefix) offset = len(node_map) for idx, pattern in enumerate(iter(root)): if isinstance(pattern, BranchPattern): node_map[pattern] = BranchNode(pattern, offset + idx, node_map) node_map[root].name = 'docopt_node_root' self.root_node = node_map[root] self.node_map = node_map @property def nodes(self): return self.node_map.values() @property def leaf_nodes(self): from .node import LeafNode return [node for node in self.nodes if type(node) is LeafNode] 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] # Trim newlines in usage_match match_fix = re.search(r'\A\n*(.+?)\n*\Z', usage, re.MULTILINE | re.DOTALL) usage_match = usage_match.start(0) + match_fix.start(0), usage_match.start(0) + match_fix.end(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 sNx*>%%docopt_sh/docopt.sh#!/usr/bin/env bash docopt() { "LIBRARY SOURCE" docopt_doc="DOC VALUE" docopt_usage="DOC USAGE" docopt_digest="DOC DIGEST" docopt_shorts=("SHORTS") docopt_longs=("LONGS") docopt_argcount=("ARGCOUNT") docopt_param_names=("PARAM NAMES") "NODES" docopt_parse "$@" "DEFAULTS" ${DOCOPT_TEARDOWN:-true} && docopt_do_teardown "MAX NODE IDX" } lib_version_check() { if [[ $1 != '"VERSION"' && ${DOCOPT_LIB_CHECK:-true} != 'false' ]]; then printf "The version of the included docopt library (%s) \ does not match the version of the invoking docopt parser (%s)\n" \ '"VERSION"' "$1" >&2 exit 70 fi } docopt_do_teardown() { local max_node_idx=$1 local var for var in "${docopt_param_names[@]}"; do unset "docopt_var_$var" done local i for ((i=0; i<=max_node_idx; i++)); do unset -f "docopt_node_$i" done unset docopt_doc docopt_digest docopt_shorts docopt_longs docopt_argcount \ docopt_param_names docopt_argv docopt_left docopt_parsed_params \ docopt_parsed_values docopt_testmatch unset -f docopt docopt_parse docopt_either docopt_oneormore docopt_optional \ docopt_required docopt_command docopt_switch docopt_value docopt_parse_long \ docopt_parse_shorts docopt_node_root docopt_do_teardown } docopt_either() { local initial_left=("${docopt_left[@]}") local best_match_idx local match_count local node_idx local unset_testmatch=true $docopt_testmatch && unset_testmatch=false docopt_testmatch=true for node_idx in "$@"; do if "docopt_node_$node_idx"; then if [[ -z $match_count || ${#docopt_left[@]} -lt $match_count ]]; then best_match_idx=$node_idx match_count=${#docopt_left[@]} fi fi docopt_left=("${initial_left[@]}") done $unset_testmatch && docopt_testmatch=false if [[ -n $best_match_idx ]]; then "docopt_node_$best_match_idx" return 0 fi docopt_left=("${initial_left[@]}") return 1 } docopt_oneormore() { local i=0 local prev=${#docopt_left[@]} while "docopt_node_$1"; do ((i++)) [[ $prev -eq ${#docopt_left[@]} ]] && break prev=${#docopt_left[@]} done if [[ $i -ge 1 ]]; then return 0 fi return 1 } docopt_optional() { local node_idx for node_idx in "$@"; do "docopt_node_$node_idx" done return 0 } docopt_required() { local initial_left=("${docopt_left[@]}") local node_idx local unset_testmatch=true $docopt_testmatch && unset_testmatch=false docopt_testmatch=true for node_idx in "$@"; do if ! "docopt_node_$node_idx"; then docopt_left=("${initial_left[@]}") $unset_testmatch && docopt_testmatch=false return 1 fi done if $unset_testmatch; then docopt_testmatch=false docopt_left=("${initial_left[@]}") for node_idx in "$@"; do "docopt_node_$node_idx" done fi return 0 } docopt_switch() { local i for i in "${!docopt_left[@]}"; do local l=${docopt_left[$i]} if [[ ${docopt_parsed_params[$l]} = "$2" ]]; then docopt_left=("${docopt_left[@]:0:$i}" "${docopt_left[@]:((i+1))}") $docopt_testmatch && return 0 if [[ $3 = true ]]; then eval "((docopt_var_$1++))" else eval "docopt_var_$1=true" fi return 0 fi done return 1 } docopt_value() { local i for i in "${!docopt_left[@]}"; do local l=${docopt_left[$i]} if [[ ${docopt_parsed_params[$l]} = "$2" ]]; then docopt_left=("${docopt_left[@]:0:$i}" "${docopt_left[@]:((i+1))}") $docopt_testmatch && return 0 local value value=$(printf -- "%q" "${docopt_parsed_values[$l]}") if [[ $3 = true ]]; then eval "docopt_var_$1+=($value)" else eval "docopt_var_$1=$value" fi return 0 fi done return 1 } docopt_command() { local i local name=${2:-$1} for i in "${!docopt_left[@]}"; do local l=${docopt_left[$i]} if [[ ${docopt_parsed_params[$l]} = 'a' ]]; then if [[ ${docopt_parsed_values[$l]} != "$name" ]]; then return 1 fi docopt_left=("${docopt_left[@]:0:$i}" "${docopt_left[@]:((i+1))}") $docopt_testmatch && return 0 if [[ $3 = true ]]; then eval "((docopt_var_$1++))" else eval "docopt_var_$1=true" fi return 0 fi done return 1 } docopt_parse_shorts() { local token=${docopt_argv[0]} local value docopt_argv=("${docopt_argv[@]:1}") [[ $token = -* && $token != --* ]] || assert_fail local rem=${token#-} while [[ -n $rem ]]; do local short="-${rem:0:1}" rem="${rem:1}" local i=0 local similar=() local match=false for o in "${docopt_shorts[@]}"; do if [[ $o = "$short" ]]; then similar+=("$short") [[ $match = false ]] && match=$i fi ((i++)) done if [[ ${#similar[@]} -gt 1 ]]; then docopt_error "$(printf "%s is specified ambiguously %d times" \ "$short" "${#similar[@]}")" elif [[ ${#similar[@]} -lt 1 ]]; then match=${#docopt_shorts[@]} value=true docopt_shorts+=("$short") docopt_longs+=('') docopt_argcount+=(0) else value=false if [[ ${docopt_argcount[$match]} -ne 0 ]]; then if [[ $rem = '' ]]; then if [[ ${#docopt_argv[@]} -eq 0 || ${docopt_argv[0]} = '--' ]]; then docopt_error "$(printf "%s requires argument" "$short")" fi value=${docopt_argv[0]} docopt_argv=("${docopt_argv[@]:1}") else value=$rem rem='' fi fi if [[ $value = false ]]; then value=true fi fi docopt_parsed_params+=("$match") docopt_parsed_values+=("$value") done } docopt_parse_long() { local token=${docopt_argv[0]} local long=${token%%=*} local value=${token#*=} local argcount docopt_argv=("${docopt_argv[@]:1}") [[ $token = --* ]] || assert_fail if [[ $token = *=* ]]; then eq='=' else eq='' value=false fi local i=0 local similar=() local match=false for o in "${docopt_longs[@]}"; do if [[ $o = "$long" ]]; then similar+=("$long") [[ $match = false ]] && match=$i fi ((i++)) done if [[ $match = false ]]; then i=0 for o in "${docopt_longs[@]}"; do if [[ $o = $long* ]]; then similar+=("$long") [[ $match = false ]] && match=$i fi ((i++)) done fi if [[ ${#similar[@]} -gt 1 ]]; then docopt_error "$(printf "%s is not a unique prefix: %s?" \ "$long" "${similar[*]}")" elif [[ ${#similar[@]} -lt 1 ]]; then [[ $eq = '=' ]] && argcount=1 || argcount=0 match=${#docopt_shorts[@]} [[ $argcount -eq 0 ]] && value=true docopt_shorts+=('') docopt_longs+=("$long") docopt_argcount+=("$argcount") else if [[ ${docopt_argcount[$match]} -eq 0 ]]; then if [[ $value != false ]]; then docopt_error "$(printf "%s must not have an argument" \ "${docopt_longs[$match]}")" fi elif [[ $value = false ]]; then if [[ ${#docopt_argv[@]} -eq 0 || ${docopt_argv[0]} = '--' ]]; then docopt_error "$(printf "%s requires argument" "$long")" fi value=${docopt_argv[0]} docopt_argv=("${docopt_argv[@]:1}") fi if [[ $value = false ]]; then value=true fi fi docopt_parsed_params+=("$match") docopt_parsed_values+=("$value") } docopt_error() { [[ -n $1 ]] && printf "%s\n" "$1" printf "%s\n" "${docopt_usage}" exit 1 } docopt_parse() { if ${DOCOPT_DOC_CHECK:-true}; then local doc_hash doc_hash=$(printf "%s" "$docopt_doc" | shasum -a 256) if [[ ${doc_hash:0:5} != "$docopt_digest" ]]; then printf "The current usage doc (%s) does not match what the parser was generated with (%s)\n" \ "${doc_hash:0:5}" "$docopt_digest" >&2 exit 70 fi fi docopt_argv=("$@") docopt_parsed_params=() docopt_parsed_values=() docopt_left=() docopt_testmatch=false local arg while [[ ${#docopt_argv[@]} -gt 0 ]]; do if [[ ${docopt_argv[0]} = "--" ]]; then for arg in "${docopt_argv[@]}"; do docopt_parsed_params+=('a') docopt_parsed_values+=("$arg") done break elif [[ ${docopt_argv[0]} = --* ]]; then docopt_parse_long elif [[ ${docopt_argv[0]} = -* && ${docopt_argv[0]} != "-" ]]; then docopt_parse_shorts elif ${DOCOPT_OPTIONS_FIRST:-false}; then for arg in "${docopt_argv[@]}"; do docopt_parsed_params+=('a') docopt_parsed_values+=("$arg") done break else docopt_parsed_params+=('a') docopt_parsed_values+=("${docopt_argv[0]}") docopt_argv=("${docopt_argv[@]:1}") fi done local idx if ${DOCOPT_ADD_HELP:-true}; then for idx in "${docopt_parsed_params[@]}"; do [[ $idx = 'a' ]] && continue if [[ ${docopt_shorts[$idx]} = "-h" || ${docopt_longs[$idx]} = "--help" ]]; then printf -- "%s\n" "$docopt_doc" exit 0 fi done fi if [[ ${DOCOPT_PROGRAM_VERSION:-false} != 'false' ]]; then for idx in "${docopt_parsed_params[@]}"; do [[ $idx = 'a' ]] && continue if [[ ${docopt_longs[$idx]} = "--version" ]]; then printf "%s\n" "$DOCOPT_PROGRAM_VERSION" exit 0 fi done fi local i=0 while [[ $i -lt ${#docopt_parsed_params[@]} ]]; do docopt_left+=("$i") ((i++)) done if ! docopt_required root || [ ${#docopt_left[@]} -gt 0 ]; then docopt_error fi return 0 } PKcN adocopt_sh/node.pyfrom .doc_ast import Option, Command, Required, Optional, OptionsShortcut, OneOrMore, Either from .bash import Function, bash_variable_name, bash_ifs_value helper_map = { Required: 'docopt_required', Optional: 'docopt_optional', OptionsShortcut: 'docopt_optional', OneOrMore: 'docopt_oneormore', Either: 'docopt_either', } class Node(Function): def __init__(self, pattern, idx): self.type = type(pattern) self.idx = idx super(Node, self).__init__('docopt_node_' + str(idx)) class BranchNode(Node): def __init__(self, pattern, idx, function_map): super(BranchNode, self).__init__(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): # minify 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, pattern, idx, name_prefix): super(LeafNode, self).__init__(pattern, idx) self.default_value = pattern.value self.pattern = pattern if self.type is Option: self.helper_name = 'docopt_switch' if type(self.default_value) in [bool, int] else 'docopt_value' self.needle = idx elif self.type is Command: self.helper_name = 'docopt_command' self.needle = pattern.name else: self.helper_name = 'docopt_value' self.needle = 'a' self.multiple = type(self.default_value) in [list, int] self.variable_name = bash_variable_name(pattern.name, 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 == 'docopt_command' and args[0] == args[1] and len(args) == 2: args = [args[0]] body = ' '.join([self.helper_name] + args) return ' ' + body PKNYdocopt_sh/parser.pyimport os.path import re import hashlib import logging import shlex from collections import OrderedDict from . import __version__, DocoptError from .doc_ast import DocAst, Option from .bash import Code, HelperTemplate, Helper, indent, bash_variable_value, bash_ifs_value, minify log = logging.getLogger(__name__) class Parser(object): def __init__(self, parser_parameters): self.parameters = parser_parameters self.library = Library() def patched_script(self): return self.script.insert_parser(str(self), self.parameters.refresh_command) def generate(self, script): generated = self.generate_main(script) if not self.parameters.library_path: generated = generated + self.generate_library() if self.parameters.minify: return generated.minify(self.parameters.max_line_length) else: return str(generated) def generate_main(self, script): if self.parameters.library_path: library_source = 'source %s \'%s\'' % (self.parameters.library_path, __version__) else: library_source = '' doc_value_start, doc_value_end = script.doc.stripped_value_boundaries stripped_doc = '${{DOC:{start}:{length}}}'.format( start=doc_value_start, length=doc_value_end - doc_value_start, ) doc_ast = DocAst(script.doc.value, self.parameters.name_prefix) usage_start, usage_end = doc_ast.usage_match usage_doc = '${{DOC:{start}:{length}}}'.format( start=str(doc_value_start + usage_start), length=str(usage_end - usage_start), ) option_nodes = [o for o in doc_ast.leaf_nodes if o.type is Option] defaults = [] for node in doc_ast.leaf_nodes: if type(node.default_value) is list: tpl = "[[ -z ${{{docopt_name}+x}} ]] && {name}={default} || {name}=(\"${{{docopt_name}[@]}}\")" else: tpl = "{name}=${{{docopt_name}:-{default}}}" defaults.append(tpl.format( name=node.variable_name, docopt_name='docopt_var_' + node.variable_name, default=bash_variable_value(node.default_value) )) replacements = { '"LIBRARY SOURCE"': library_source, '"DOC VALUE"': stripped_doc, '"DOC USAGE"': usage_doc, '"DOC DIGEST"': hashlib.sha256(script.doc.value.encode('utf-8')).hexdigest()[0:5], '"SHORTS"': ' '.join([bash_ifs_value(o.pattern.short) for o in option_nodes]), '"LONGS"': ' '.join([bash_ifs_value(o.pattern.long) for o in option_nodes]), '"ARGCOUNT"': ' '.join([bash_ifs_value(o.pattern.argcount) for o in option_nodes]), '"PARAM NAMES"': ' '.join([node.variable_name for node in doc_ast.leaf_nodes]), ' "NODES"': indent('\n'.join(map(str, list(doc_ast.nodes)))), ' "DEFAULTS"': indent('\n'.join(defaults)), '"MAX NODE IDX"': str(max([n.idx for n in doc_ast.nodes if n is not doc_ast.root_node])), } return self.library.main.render(replacements) def generate_library(self, check_version=False): functions = OrderedDict([]) replacements = { 'lib_version_check': { '"VERSION"': __version__, } } for name, tpl in self.library.functions.items(): functions[name] = tpl.render(replacements.get(name, {})) if check_version: functions['lib_version_check'] = Code(functions['lib_version_check'].body) else: del functions['lib_version_check'] return Code(functions) class Library(object): def __init__(self): function_re = re.compile(( r'^(?P[a-z_][a-z0-9_]*)\(\)\s*\{' r'\n+' r'(?P.*?)' r'\n+\}$' ), re.MULTILINE | re.IGNORECASE | re.DOTALL) self.functions = OrderedDict([]) with open(os.path.join(os.path.dirname(__file__), 'docopt.sh'), 'r') as handle: for match in function_re.finditer(handle.read()): name = match.group('name') body = match.group('body') if name == 'docopt': self.main = HelperTemplate(name, body) else: self.functions[name] = HelperTemplate(name, body) class ParserParameter(object): def __init__(self, name, invocation_params, script_params, default): if script_params is None: script_value = None else: script_value = script_params[name] defined_in_invocation = invocation_params[name] is not None defined_in_script = script_value is not None auto_params = not invocation_params['--no-auto-params'] self.name = name self.defined = (defined_in_invocation or defined_in_script) and auto_params self.invocation_value = invocation_params[name] if defined_in_invocation else default self.script_value = script_value if defined_in_script else default self.merged_from_script = not defined_in_invocation and auto_params and defined_in_script self.value = self.script_value if self.merged_from_script else self.invocation_value self.changed = script_params is not None and self.value != self.script_value def __str__(self): return '%s=%s' % (self.name, shlex.quote(self.value)) class ParserParameters(object): def __init__(self, invocation_params, script=None): if script is not None: script_params = script.guards.bottom.refresh_command_params if script_params is None: if script.guards.present and not invocation_params['--no-auto-params']: raise DocoptError( 'Unable to auto-detect parser generation parameters. ' 'Re-run docopt.sh with `--no-auto-params`.' ) else: script_params = None params = OrderedDict([]) params['--prefix'] = ParserParameter('--prefix', invocation_params, script_params, default='') params['--line-length'] = ParserParameter('--line-length', invocation_params, script_params, default='80') params['--library'] = ParserParameter('--library', invocation_params, script_params, default=None) if params['--prefix'].changed: log.warning( 'The parameter variable prefix is changing from `%s` to `%s`, ' 'make sure to check the variable names used in your script.', params['--prefix'].script_value, params['--prefix'].invocation_value ) merged_from_script = list(filter(lambda p: p.merged_from_script, params.values())) if merged_from_script: log.info( 'Adding `%s` from parser generation parameters that were detected in the script. ' 'Use --no-auto-params to disable this behavior.', ' '.join(map(str, merged_from_script)) ) self.name_prefix = params['--prefix'].value self.max_line_length = int(params['--line-length'].value) self.library_path = params['--library'].value self.minify = self.max_line_length > 0 self.shorten_names = self.minify and self.library_path is not None command = ['docopt.sh'] command_short = ['docopt.sh'] if params['--prefix'].defined: command.append(str(params['--prefix'])) if params['--line-length'].defined: command.append(str(params['--line-length'])) if params['--library'].defined: command.append(str(params['--library'])) if script is not None and script.path: command.append(os.path.basename(script.path)) command_short.append(os.path.basename(script.path)) else: command.append(' 1: raise DocoptScriptValidationError( 'More than one variable containing usage doc found. ' 'Search your script for `DOC=`, there should be only one such declaration.', self.doc ) guard_help = ( 'Search your script for `# docopt parser below/above`, ' 'there should be exactly one with `below` and one with `above` (in that order). ' 'If in doubt, just delete anything that is not your code and try again.' ) if self.guards.top.count > 1: raise DocoptScriptValidationError( 'Multiple docopt parser top guards found. ' + guard_help, self.guards.top) if self.guards.bottom.count > 1: raise DocoptScriptValidationError( 'Multiple docopt parser bottom guards found.' + guard_help, self.guards.bottom) if self.guards.top.present and not self.guards.bottom.present: raise DocoptScriptValidationError( 'Parser top guard found, but no bottom guard detected. ' + guard_help, self.guards.top) if self.guards.bottom.present and not self.guards.top.present: raise DocoptScriptValidationError( 'Parser bottom guard found, but no top guard detected. ' + guard_help, self.guards.bottom) if self.invocation.count > 1: log.warning( '%s Multiple invocations of docopt found, check your script to make sure this is correct.', self.invocation ) if not self.invocation.present: log.warning( '%s No invocations of docopt found, check your script to make sure this is correct.\n' 'docopt.sh is invoked with `docopt "$@"`.', self.invocation ) for option in self.options: if option.present and option.start > self.invocation.last.end: log.warning( '%s $%s has no effect when specified after invoking docopt, ' 'make sure to place docopt options before calling `docopt "$@"`.', option, option.name ) def patch(self, parser): return Script( "{start}{guard_begin}\n{parser}{guard_end}\n{end}".format( start=self.contents[:self.guards.start], guard_begin=( "# docopt parser below, refresh this parser with `%s`" % parser.parameters.refresh_command_short), parser=parser.generate(self), guard_end=( "# docopt parser above, complete command for generating this parser is `%s`" % parser.parameters.refresh_command), end=self.contents[self.guards.end:], ) ) def __eq__(self, other): return self.contents == other.contents def __str__(self): return self.contents class ScriptLocation(object): def __init__(self, script, matches, offset): self.script = script self.matches = list(matches) self.match = self.matches[0] if self.matches else None self.offset = 0 if offset is None else offset self.present = self.match is not None self.count = len(self.matches) self.start = self.match.start(0) + self.offset if self.present else None self.end = self.match.end(0) + self.offset if self.present else None self.line = self.script.contents[:self.start].count('\n') + 1 self.all = [self] + [ScriptLocation(self.script, iter([match]), self.offset) for match in self.matches[1:]] if self.count == 0: self.last = None elif self.count == 1: self.last = self else: self.last = ScriptLocation(self.script, iter([self.matches[-1]]), self.offset) def __len__(self): return self.end - self.start if self.present else 0 def __str__(self): if not self.present: return '%s' % self.script.path if self.count > 1: return '%s:%s' % (self.script.path, ','.join(map(lambda l: str(l.line), self.all))) else: return '%s:%d' % (self.script.path, self.line) class Doc(ScriptLocation): def __init__(self, script): matches = re.finditer( r'DOC="(\s*)(.*?Usage:.+?)(\s*)"(\n|;)', script.contents, re.MULTILINE | re.IGNORECASE | re.DOTALL ) super(Doc, self).__init__(script, matches, 0) self.value = self.match.group(2) if self.present else None self.stripped_value_boundaries = ( self.match.start(2) - self.match.start(1), self.match.end(2) - self.match.start(1) ) if self.present else None class TopGuard(ScriptLocation): def __init__(self, script, doc): matches = re.finditer( r'# docopt parser below, refresh this parser with `([^`]+)`\n', script.contents[doc.end:], re.MULTILINE ) super(TopGuard, self).__init__(script, matches, doc.end) class BottomGuard(ScriptLocation): def __init__(self, script, top): matches = re.finditer( r'# docopt parser above, complete command for generating this parser is `([^`]+)`\n', script.contents[top.end:], re.MULTILINE ) super(BottomGuard, self).__init__(script, matches, top.end) self.refresh_command = self.match.group(1) if self.present else None self.refresh_command_params = None if self.refresh_command is not None: from .__main__ import __doc__ try: self.refresh_command_params = docopt.docopt(__doc__, shlex.split(self.refresh_command)[1:]) except (docopt.DocoptLanguageError, docopt.DocoptExit): pass class Guards(object): def __init__(self, script, doc): self.top = TopGuard(script, doc) if self.top.present: self.bottom = BottomGuard(script, self.top) else: self.bottom = BottomGuard(script, doc) self.present = self.top.present and self.bottom.present # The top.offset is to easily handle the absence of a parser self.start = self.top.start if self.present else self.top.offset self.end = self.bottom.end if self.present else self.top.offset def __len__(self): return self.end - self.start if self.present else 0 class Invocation(ScriptLocation): def __init__(self, script, parser): matches = re.finditer(r'docopt\s+"\$\@"', script.contents[parser.end:]) super(Invocation, self).__init__(script, matches, parser.end) class Option(ScriptLocation): def __init__(self, script, name): self.name = name matches = re.finditer(r'^%s=' % name, script.contents, re.MULTILINE) super(Option, self).__init__(script, 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(DocoptError): def __init__(self, message, script_location=None): # Exit code 74: input/output error (sysexits.h) super(DocoptScriptValidationError, self).__init__(message, exit_code=74) self.script_location = script_location def __str__(self): if self.script_location is not None: return '%s %s' % (self.script_location, self.message) return self.message PK!HY05*docopt_sh-0.9.1.dist-info/entry_points.txtN+I/N.,()JO/(+ΰ3s3@PK梬N(##!docopt_sh-0.9.1.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.9.1.dist-info/WHEEL HM K-*ϳR03rOK-J,/RH,szd&Y)r$[)T&UrPK!HCP`.cn "docopt_sh-0.9.1.dist-info/METADATAVmoG~bC"m*rU$FN[Ynߙ=c'3<3%: 'ohԪ~F:ѥm핧I=B]cѲIЇ0@Ƣ5*4iӊuq)8sv{-]Vݶ]J-*{.dB}EetRJjP*c"Km+h+iƓYR}h&F {Z9J;J޵»ToU˅r%bu)PRNIFE u`~Z{J'~{p"UeiS~97CҺRHtcEsh@3 Y|ʗX~-a#S3.~:02晴`mAw,w 2-i+mڒV *(Z.k#5Bk}gg"u#sD@A-D%C8 QJWCGy0孡|֞gp2q-b/q\\{L=A# a ^UNTe0XAPUv\Sg}<ϸreoT֡HAcq+]!3st_B,hEyhɹc, "E9y肰[IY E:.g| ;$  ΋ _">z&+HJ=$jLka]' IT /pu>&abQv[AB 4q>Q9]0&"'ڈº҆-?IOteڽ\|Lg -Sҋy}ۥ4vOx wb?3CJ=;:zhTxp:GdCpEWk]rX(+E/fm6]_GO73Mr* bXqTOLdջufwXeL.O8sy5a,/_M!FSx`lc@cV~ax6zwYh=hPK!HRi docopt_sh-0.9.1.dist-info/RECORD}9@ 8@A ȦpXeB6_?xN:wy/1nAC7!s)6 "Ne /DՒ TɊK<,{X4?L^ehs m7P?k|{z*8ZB"-GKgB͹4HĜe)E߸q^56;-h!խEKI%EM:J>B%%FPdocopt_sh/docopt.shPKcN a{udocopt_sh/node.pyPKNY?}docopt_sh/parser.pyPKN=)  Ddocopt_sh/script.pyPK!HY05*docopt_sh-0.9.1.dist-info/entry_points.txtPK梬N(##! docopt_sh-0.9.1.dist-info/LICENSEPK!HPOodocopt_sh-0.9.1.dist-info/WHEELPK!HCP`.cn "docopt_sh-0.9.1.dist-info/METADATAPK!HRi docopt_sh-0.9.1.dist-info/RECORDPK F