PKMN@docopt_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.10' class DocoptError(Exception): def __init__(self, message, exit_code=1): self.message = message self.exit_code = exit_code PK,MNN״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 [options] (- | SCRIPT) docopt.sh generate-library Options: --line-length -n 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 -P Disable auto-detection parser generation parameters --parser -p Output the parser instead of inserting it in the script --help -h Show this help screen --version Show the docopt.sh version Note: When reading the script from stdin (`-` instead of SCRIPT) docopt.sh will output the modified script to stdout. Parameters: You can set the global variables before invoking docopt with `eval "$(docopt "$@")"` to change the behavior of the parser. Consult the readme for advanced options. $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): # `generate-library` will never be true because it is specified after [SCRIPT] # which matches it first. We want it in that order though, but there's a simple workaround: if params['SCRIPT'] == 'generate-library': parser_parameters = ParserParameters(params) parser = Parser(parser_parameters) sys.stdout.write('#!/usr/bin/env bash\n\n' + str(parser.generate_library(add_version_check=True))) else: try: if params['-']: 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['-']: 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() PKNl'W W docopt_sh/bash.pyimport re from shlex import quote from collections import OrderedDict from collections.abc import Iterable from itertools import chain class Code(object): def __init__(self, code): self.code = self._get_iter(code) def _get_iter(self, code): if isinstance(code, str): return iter([code]) else: return iter(code) def minify(self, max_line_length): return Code(minify(str(self), max_line_length)) def replace_literal(self, replacements): def gen_replace(): for part in self.code: code = str(part) for placeholder, replacement in replacements.items(): code = code.replace(placeholder, str(replacement)) yield code return Code(gen_replace()) def __iter__(self): return self.code def __add__(self, other): return Code(chain(self.code, self._get_iter(other))) def __str__(self): return '\n'.join(map(str, self)) def indent(script, level=1): indentation = ' ' * level return '\n'.join(map(lambda l: indentation + l, script.split('\n'))) def bash_variable_name(name): return 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 PKNOL-L-docopt_sh/doc_ast.pyimport re from collections import OrderedDict from itertools import chain class DocAst(object): def __init__(self, doc): 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))) # Enumerate leaf nodes first so that their function index & index in the params array match for idx, param in enumerate(sorted_params): node_map[param] = LeafNode(param, idx) offset = len(node_map) for idx, pattern in enumerate(iter(root)): if isinstance(pattern, BranchPattern): node_map[pattern] = BranchNode(pattern, offset + idx, node_map) self.root_node = node_map[root] self.nodes = node_map.values() 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:]) + ' )' PKLN+?((docopt_sh/docopt.sh#!/usr/bin/env bash docopt() { docopt_run() { "LIBRARY SOURCE" docopt_doc="DOC VALUE" docopt_usage="DOC USAGE" docopt_digest="DOC DIGEST" docopt_shorts=("SHORTS") docopt_longs=("LONGS") docopt_argcount=("ARGCOUNT") "NODES" docopt_parse "ROOT NODE IDX" "$@" || exit $? # shellcheck disable=2016 cat <<<' docopt_exit() { [[ -n $1 ]] && printf "%s\n" "$1" >&2 printf "%s\n" ""DOC USAGE"" >&2 exit 1 }' "OUTPUT TEARDOWN" # shellcheck disable=2157,2140 "HAS VARS" || return 0 # shellcheck disable=2034 local docopt_prefix=${DOCOPT_PREFIX:-''} # Workaround for bash-4.3 bug # The following script will not work in bash 4.3.0 (and only that version) # #!tests/bash-versions/bash-4.3/bash # fn() { # decl=$(X=(A B); declare -p X) # eval "$decl" # declare -p X # } # fn local docopt_loops=1 [[ $BASH_VERSION =~ ^4.3 ]] && docopt_loops=2 # Adding "declare X" before "eval" fixes the issue, but we don't know the # variable names, so instead we just output the `declare`s twice # in bash-4.3. # Unset exported variables from parent shell unset "VAR NAMES" "DEFAULTS" local docopt_i=0 for ((docopt_i=0;docopt_i&1); then printf -- "%s\n" "$out" else local ret=$? if [ $ret -eq 85 ]; then printf -- "cat <<'EOM'\n%s\nEOM\nexit 0\n" "$out" else printf -- "cat <<'EOM' >&2\n%s\nEOM\nexit %d\n" "$out" "$ret" fi fi } lib_version_check() { if [[ $1 != '"LIBRARY 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" \ '"LIBRARY VERSION"' "$1" >&2 exit 70 fi } 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 remaining=${token#-} while [[ -n $remaining ]]; do local short="-${remaining:0:1}" remaining="${remaining: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 [[ $remaining = '' ]]; 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=$remaining remaining='' 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" >&2 printf "%s\n" "${docopt_usage}" >&2 exit 1 } docopt_do_teardown() { unset -f docopt_run docopt_parse docopt_either docopt_oneormore \ docopt_optional docopt_required docopt_command docopt_switch docopt_value \ docopt_parse_long docopt_parse_shorts docopt_error docopt_do_teardown } 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)\nRun \`docopt.sh\` to refresh the parser.\n" \ "${doc_hash:0:5}" "$docopt_digest" exit 70 fi fi local root_idx=$1 shift 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 85 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 85 fi done fi local i=0 while [[ $i -lt ${#docopt_parsed_params[@]} ]]; do docopt_left+=("$i") ((i++)) done if ! docopt_required "$root_idx" || [ ${#docopt_left[@]} -gt 0 ]; then docopt_error fi return 0 } PK N'M docopt_sh/node.pyfrom .doc_ast import Option, Command, Required, Optional, OptionsShortcut, OneOrMore, Either from .bash import Code, bash_variable_name, bash_variable_value, bash_ifs_value helper_map = { Required: 'docopt_required', Optional: 'docopt_optional', OptionsShortcut: 'docopt_optional', OneOrMore: 'docopt_oneormore', Either: 'docopt_either', } class Node(Code): def __init__(self, pattern, body, idx): self.pattern = pattern self.idx = idx code = '{name}(){{\n{body}\n}}\n'.format( name='docopt_node_' + str(idx), body=body, ) super(Node, self).__init__(code) class BranchNode(Node): def __init__(self, pattern, idx, node_map): # minify arg list by only specifying node idx child_indexes = map(lambda child: node_map[child].idx, pattern.children) body = ' {helper} {args}'.format( helper=helper_map[type(pattern)], args=' '.join(list(map(str, child_indexes))), ) super(BranchNode, self).__init__(pattern, body, idx) class LeafNode(Node): def __init__(self, pattern, idx): default_value = pattern.value if type(pattern) is Option: helper_name = 'docopt_switch' if type(default_value) in [bool, int] else 'docopt_value' needle = idx elif type(pattern) is Command: helper_name = 'docopt_command' needle = pattern.name else: helper_name = 'docopt_value' needle = 'a' self.variable_name = bash_variable_name(pattern.name) args = [self.variable_name, bash_ifs_value(needle)] if type(default_value) in [list, int]: args.append(bash_ifs_value(True)) elif helper_name == 'docopt_command' and args[0] == args[1]: args = [args[0]] body = ' {helper} {args}'.format( helper=helper_name, args=' '.join(args), ) if type(default_value) is list: default_tpl = ( 'if declare -p {docopt_name} >/dev/null 2>&1; then\n' ' eval "${{docopt_prefix}}"\'{name}=("${{{docopt_name}[@]}}")\'\n' 'else\n' ' eval "${{docopt_prefix}}"\'{name}={default}\'\n' 'fi' ) else: default_tpl = ( 'eval "${{docopt_prefix}}"\'{name}=${{{docopt_name}:-{default}}}\'' ) self.default_assignment = default_tpl.format( name=self.variable_name, docopt_name='docopt_var_' + self.variable_name, default=bash_variable_value(default_value) ) self.prefixed_variable_name = '${{docopt_prefix}}{name}'.format(name=self.variable_name) super(LeafNode, self).__init__(pattern, body, idx) PKJNq$Odocopt_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, indent, bash_ifs_value, minify from .node import LeafNode log = logging.getLogger(__name__) class Parser(object): def __init__(self, parser_parameters): self.parameters = parser_parameters self.library = Library() self.shellcheck_ignores = [ '2016', # Ignore unexpanded variables in single quotes (used for docopt_exit generation) ] if self.parameters.library_path: self.shellcheck_ignores.extend([ '1091', # Ignore library sourcing '2034', # Ignore unused vars (they refer to things in the library) ]) def generate(self, script): generated = self.generate_main(script) if not self.parameters.library_path: generated = generated + self.generate_library() if self.parameters.minify: generated = generated.minify(self.parameters.max_line_length) 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) 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), ) leaf_nodes = [n for n in doc_ast.nodes if type(n) is LeafNode] option_nodes = [node for node in leaf_nodes if type(node.pattern) is Option] replacements = { '"LIBRARY SOURCE"': library_source, ' "OUTPUT TEARDOWN"\n': '' if library_source else " printf 'docopt_do_teardown\\n'\n", '"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]), ' "NODES"': indent('\n'.join(map(str, list(doc_ast.nodes))), level=2), ' "DEFAULTS"': indent('\n'.join([node.default_assignment for node in leaf_nodes]), level=2), '"VAR NAMES"': ' \\\n '.join(['"%s"' % node.prefixed_variable_name for node in leaf_nodes]), '"HAS VARS"': bash_ifs_value(True if leaf_nodes else False), '"MAX NODE IDX"': max([n.idx for n in doc_ast.nodes]), '"ROOT NODE IDX"': doc_ast.root_node.idx, } return self.library.main.replace_literal(replacements) def generate_library(self, add_version_check=False): functions = self.library.functions if add_version_check: functions['lib_version_check'] = functions['lib_version_check'].replace_literal( {'"LIBRARY VERSION"': __version__}) else: del functions['lib_version_check'] return Code(functions.values()) class Library(object): def __init__(self): function_re = re.compile(( r'^(?P[a-z_][a-z0-9_]*)\(\)\s*\{' r'\n+' r'(?P.*?)' r'\n+\}\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') if name == 'docopt': self.main = Code(match.group(0)) elif name == 'lib_version_check': self.functions[name] = Code(match.group('body')) else: self.functions[name] = Code(match.group(0)) 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['--line-length'] = ParserParameter('--line-length', invocation_params, script_params, default='80') params['--library'] = ParserParameter('--library', invocation_params, script_params, default=None) 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.max_line_length = int(params['--line-length'].value) self.library_path = params['--library'].value self.minify = self.max_line_length > 0 command = ['docopt.sh'] command_short = ['docopt.sh'] 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('-') 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 `eval "$(docopt "$@")"`.', self.invocation ) for option in self.options: if self.invocation.present and 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 `eval "$(docopt "$@")"`.', option, option.name ) def patch(self, parser): return Script( "{start}{guard_begin}\n{shellcheck_ignores}\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), shellcheck_ignores='# shellcheck disable=%s' % ','.join(parser.shellcheck_ignores), 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 ) # re.IGNORECASE causes doc=, Doc= etc. to be matched, remove those matches matches = filter(lambda m: m.group(0).startswith('DOC='), matches) 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_params = None if self.present and self.match.group(2) is not None: from .__main__ import __doc__ try: self.refresh_command_params = docopt.docopt(__doc__, shlex.split(self.match.group(2))[1:]) except (docopt.DocoptLanguageError, docopt.DocoptExit) as e: 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'eval "\$\(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 = self.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.10.dist-info/entry_points.txtN+I/N.,()JO/(+ΰ3s3@PKN(##"docopt_sh-0.9.10.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!HPO docopt_sh-0.9.10.dist-info/WHEEL HM K-*ϳR03rOK-J,/RH,szd&Y)r$[)T&UrPK!Hݭi2#docopt_sh-0.9.10.dist-info/METADATA[{s6uӍJNR'֛gvH IH mk~Cx&̝YdLySe)?,={np"PYêzr'. .\ $43#i5Wi)rYUJU!ˬ7\s9YYp{{Y5l-بl:VxϓGU9ˊCqGS5ixɡyZ6l< Oڕ )!$u7t \vv56$6nNﻥ4 XUR$a"ݰe.!%IkCV`XPS=MS`w')M XF]ںJӔS EMWwnq0Ma݅,NItg )<2]0=_6K. |S<%WI>)h@|@Ɋ[}%ZCԷaS+ g'KX6ۨKKnfF'G^a Jd"0ܐp4#`k3f-lkxn9<>AQ/V MSd{UDexʺs;d)9SLm: E8$AM rJlMJPo <%`!=EC$eޙzl)XK5^X@-䂨w &)[1#v9N{4kJ.H ~:z+aqP_ӿ ʛl \_Ŀoj(Ȓľ-..<#2#ѐT ʗ5Vqfx9y^mH8=8ؑ@Cdnasrxv>%Zuqr%5u6$ Ņ| 5+"ԉiiWFV8%6Rn)㰝zh2vM]>aÏ: ½CSYol~͌Ns̉?o~gV/Vn"8 ;yks*7sCI\Ppd4tyvfyLd%68AVCRxk@dĶ&\O BHRb@02He@1vwM3j)k쟁]yʦxʰ:]VVWDm |]䒳?kzTR+wP Z)VAk)-O`B2U0z\,1QQܓM>̛뱂J6̅Og7^R SRTVѻO/OO?]-\g6`ՊGmZJy4a*Rj'BC˗7ޞ5FCVHH؞fD5[)m*??Q^ 8>drJ}a kL긺~ҏa=>3'Tts}hJ 1]}śW/Y.hB܅ "_,?BL;]W߃o.oV vS4xߝͬ$m>.8u-UDA QוiT0*[|VQ2C*ǵ4,&kxJׯ*-iO dz=K_jƒR cM;ےnwՍfd绣.'Ymy/e:n1Pni !#-Fjw W>dM博~6Q.(K'NNKYLLc&^!#ӭ 1uJGvu7xeJk:} %.^yÅ9"\R2P!ZY#:Gzxצ \ #o.PV [1& V#pce~&-ڍ$'f?w Vb$ڐhFFVA}w{Ğq1D]ST%ZO²6|R.NGFpC:-3ϋ騄>q|FzJijpZp8.EB`:mw:yk\>!maqI| d_$˱_vJf|J]oWm*P3چ&L!112 |c?2\u|U OBdap]vY<ܖƀmw}xN'{q^;WMǖDu /xnO v4j\R"'WhS0-ΰ 9{oۥejR~*4$E2sMܘdt7i)7vR36ATCB/D[HfgGX4)\;u(ꢡM\歡{YS[GNxqLuK@ZIt 4{|3.'x;;6~|b`8>vdvvbɣmK",o ?PK!H-jl!docopt_sh-0.9.10.dist-info/RECORD}һ@|{piFEPQH(ֈ$n1U;?U݅$ C\. H,bxt L{Y{"u= t z-_a(S$|3*5x䜜ISǫ9|JvǝuNS@3l&2Ctw$j;5;4лs:db9Ϥ*H7ä=YE\#2!ffb xvw$cWDlDcFL~"teo])_9ae'\՚XU\! Fp!kǰpّe7t0G "FTt8KD5R@G_v(v"Ao9_a8ŅFOB$lt^(<[s>GѧB79"BnB#?n#ugkPKMN@docopt_sh/__init__.pyPK,MNN״docopt_sh/__main__.pyPKNl'W W cdocopt_sh/bash.pyPKNOL-L-docopt_sh/doc_ast.pyPKLN+?((gKdocopt_sh/docopt.shPK N'M tdocopt_sh/node.pyPKJNq$O~docopt_sh/parser.pyPKJNqPPdocopt_sh/script.pyPK!HY05+docopt_sh-0.9.10.dist-info/entry_points.txtPKN(##"docopt_sh-0.9.10.dist-info/LICENSEPK!HPO docopt_sh-0.9.10.dist-info/WHEELPK!Hݭi2#pdocopt_sh-0.9.10.dist-info/METADATAPK!H-jl!docopt_sh-0.9.10.dist-info/RECORDPK