PKUN8docopt_sh/__init__.py""" docopt.sh - Bash argument parser generator. This program looks for a docopt usage string in a script and appends a matching parser to it. """ __all__ = ['docopt_sh'] __version__ = '0.8.0' PKNUWhWdocopt_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, Library import logging log = logging.getLogger(__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] --parser Output parser instead of inserting it in the script --library -l PATH Generates only the dynamic part of the parser and includes the static parts from a file located at PATH, use `generate-library` to create that file. -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): if params['generate-library']: parser = Parser(params) sys.stdout.write('#!/usr/bin/env bash\n\n' + str(parser.generate_library())) else: 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']) script.validate_script_locations() parser = Parser(params) 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)) except DocoptScriptValidationError as e: print(str(e)) # Exit code 74: input/output error (sysexits.h) sys.exit(74) def main(): params = docopt(__doc__) docopt_sh(params) if __name__ == '__main__': main() PKN|Ydocopt_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=''): 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 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 PKNV bd-d-docopt_sh/doc_ast.pyimport re from collections import OrderedDict from itertools import chain class DocAst(object): def __init__(self, settings, doc): from .node import BranchNode, LeafNode doc = doc root, usage_match = parse_doc(doc) node_map = OrderedDict([]) param_sort_order = [Option, Argument, Command] unique_params = list(OrderedDict.fromkeys(root.flat(*param_sort_order))) sorted_params = sorted(unique_params, key=lambda p: param_sort_order.index(type(p))) for idx, param in enumerate(sorted_params): node_map[param] = LeafNode(settings, param, idx) offset = len(node_map) for idx, pattern in enumerate(iter(root)): if isinstance(pattern, BranchPattern): node_map[pattern] = BranchNode(settings, pattern, offset + idx, node_map) node_map[root].name = 'docopt_node_root' self.root_node = node_map[root] self.node_map = node_map self.usage_match = usage_match.start(0), usage_match.end(0) @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] 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:]) + ' )' PKN>]$]$docopt_sh/docopt.sh#!/usr/bin/env bash docopt() { "LIBRARY SOURCE" docopt_usage="DOC VALUE" docopt_digest="DOC DIGEST" docopt_short_usage="${docopt_usage:"SHORT USAGE START":"SHORT USAGE LENGTH"}" 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" } 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_usage docopt_digest docopt_short_usage 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_error \ 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_short_usage}" exit 1 } docopt_parse() { if ${docopt_doc_check:-true}; then local doc_hash doc_hash=$(printf "%s" "$docopt_usage" | 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_usage" exit 0 fi done fi if [[ ${docopt_add_version:-true} = 'true' && -n $version ]]; then for idx in "${docopt_parsed_params[@]}"; do [[ $idx = 'a' ]] && continue if [[ ${docopt_longs[$idx]} = "--version" ]]; then printf "%s\n" "$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 } PKiN˹docopt_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, settings, pattern, idx): self.settings = settings self.type = type(pattern) self.idx = idx super(Node, self).__init__('docopt_node_' + str(idx)) class BranchNode(Node): def __init__(self, settings, pattern, idx, function_map): super(BranchNode, self).__init__(settings, pattern, idx) self.helper_name = helper_map[self.type] self.child_indexes = map(lambda child: function_map[child].idx, pattern.children) @property def body(self): # 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, settings, pattern, idx): super(LeafNode, self).__init__(settings, pattern, idx) self.default_value = pattern.value self.pattern = pattern if self.type is Option: self.helper_name = '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, settings.name_prefix) @property def body(self): args = [self.variable_name, bash_ifs_value(self.needle)] if self.multiple: args.append(bash_ifs_value(self.multiple)) if self.helper_name == 'docopt_command' and args[0] == args[1] and len(args) == 2: args = [args[0]] body = ' '.join([self.helper_name] + args) return ' ' + body PKܣNuudocopt_sh/parser.pyimport os.path import re import hashlib from shlex import quote from collections import OrderedDict from .doc_ast import DocAst, Option from .bash import Code, HelperTemplate, Helper, indent, bash_variable_value, bash_ifs_value, minify class Parser(object): def __init__(self, params): self.settings = ParserSettings(params) self.library = Library() def patched_script(self): return self.script.insert_parser(str(self), self.settings.refresh_command) def generate(self, script): generated = self.generate_main(script) if not self.settings.library_path: generated = generated + self.generate_library() if self.settings.minify: return generated.minify(self.settings.max_line_length) else: return str(generated) def generate_main(self, script): library_source = 'source %s' % quote(self.settings.library_path) if self.settings.library_path else '' doc_value_start, doc_value_end = script.doc.in_string_value_match doc_name = '${{{docname}:{start}:{end}}}'.format( docname=script.doc.name, start=doc_value_start, end=doc_value_end, ) doc_ast = DocAst(self.settings, script.doc.value) usage_start, usage_end = doc_ast.usage_match 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"': doc_name, '"DOC DIGEST"': hashlib.sha256(script.doc.value.encode('utf-8')).hexdigest()[0:5], '"SHORT USAGE START"': str(usage_start), '"SHORT USAGE LENGTH"': str(usage_end - usage_start), '"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): functions = OrderedDict([]) for name, tpl in self.library.functions.items(): functions[name] = tpl.render() 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 ParserSettings(object): def __init__(self, docopt_params): self.docopt_params = docopt_params @property def name_prefix(self): return self.docopt_params['--prefix'] @property def minify(self): return self.max_line_length > 0 @property def max_line_length(self): return int(self.docopt_params['--line-length']) @property def library_path(self): return self.docopt_params['--library'] @property def refresh_command(self): command = 'docopt.sh' if self.docopt_params['--prefix'] != '': command += ' --prefix=' + quote(self.docopt_params['--prefix']) if self.docopt_params['--line-length'] != '': command += ' --line-length=' + self.docopt_params['--line-length'] if self.docopt_params['--library'] != '': command += ' --library=' + quote(self.docopt_params['--library']) if self.docopt_params['SCRIPT'] is not None: command += ' ' + os.path.basename(self.docopt_params['SCRIPT']) return command PKN zdocopt_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) 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 patch(self, parser): return Script( "{start}{guard_begin}\n{parser}{guard_end}\n{end}".format( start=self.contents[:self.parser.start], guard_begin="# docopt parser below, refresh this parser with `%s`" % parser.settings.refresh_command, parser=parser.generate(self), guard_end="# docopt parser above, refresh this parser with `%s`" % parser.settings.refresh_command, 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]*)="(\s*)(.*?Usage:.+?)(\s*)"(\n|;)', script, re.MULTILINE | re.IGNORECASE | re.DOTALL ) 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(3) @property def in_string_value_match(self): return self.match.start(3) - self.match.start(2), self.match.end(3) - self.match.end(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!HY05*docopt_sh-0.8.0.dist-info/entry_points.txtN+I/N.,()JO/(+ΰ3s3@PK梬N(##!docopt_sh-0.8.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.8.0.dist-info/WHEEL HM K-*ϳR03rOK-J,/RH,szd&Y)r$[)T&UrPK!HC"docopt_sh-0.8.0.dist-info/METADATATMo6W =JNhH48FHcX#;"i8|3o#/FOJ?[Cȍy\<)Osӵ-> + Ӫk3DLSB4CKEĕ71G妻/Ўי|="d1kj;nBù/Ǻ1`V˚5}l\̶),~>{S`R,ĊY.(x$ ?aͅÜҒԞh+KSleK,U¶+_o^/tǏOv sԥ ǘšjˇ!~[?'!+|ZP)7Wm-tAlW3olD"0- 0 ː@]C4fXwB SqJpUIDtag5u'`$n`\5PX>*X$gm]z2s )JQDKwMyf(K5&Kۚ9QﬗWT_h!bJ*oNȯ‡=̄5~Rb䆆a|aP5 79 a}:瞾#EZnief.TRJ$!gRNJ$!! *ĄEC.zz]ʇQN'tP&9MRF%k5j 96zX;tgj`=IL ۿ7,nV|#Pt1BlfЪ&D㣮: @Fvr/PK!Hi docopt_sh-0.8.0.dist-info/RECORD}ҹP༟m, CYE~:qN諪T^ò)QE?+!*h[LW| $9sN8DZ`dQڞ)5wElqa)uc$iPB#X,!.a3Gm,?#=&.][KGb$ 'D L1Êqk,Xrc#"IG8bְxc.jΨ.c~&]AT>IR:k'$86Ik5wv5[ N}GpI+FuhC`yc0֖B𺸯c4IIӭZjx6֢$ ^[%k YJ]$]$큼Idocopt_sh/docopt.shPKiN˹Jndocopt_sh/node.pyPKܣNuuYvdocopt_sh/parser.pyPKN zdocopt_sh/script.pyPK!HY05*ߝdocopt_sh-0.8.0.dist-info/entry_points.txtPK梬N(##!Wdocopt_sh-0.8.0.dist-info/LICENSEPK!HPOdocopt_sh-0.8.0.dist-info/WHEELPK!HC"Fdocopt_sh-0.8.0.dist-info/METADATAPK!Hi ɦdocopt_sh-0.8.0.dist-info/RECORDPK p