diff --git a/.github/workflows/comment-bot.yml b/.github/workflows/comment-bot.yml index 698b9fa..8ab1f09 100644 --- a/.github/workflows/comment-bot.yml +++ b/.github/workflows/comment-bot.yml @@ -24,12 +24,13 @@ jobs: }) - name: Tag Commit run: | - git config user.name tag-bot - git config user.email casperdcl@noreply.github.com - git tag $(echo "$BODY" | awk '{print $2" "$3}') - git push --tags + git clone https://${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY} repo + git -C repo tag $(echo "$BODY" | awk '{print $2" "$3}') + git -C repo push --tags + rm -rf repo env: BODY: ${{ github.event.comment.body }} + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - name: React Success uses: actions/github-script@v2 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0dac1a8..074d6c5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,7 +44,7 @@ jobs: run: | echo "::set-output name=asset_path::$(ls dist/*.whl)" echo "::set-output name=asset_name::$(basename dist/*.whl)" - git fetch --tags + git fetch --unshallow changes="$(git log --format='format:%d %B %N' $(git tag --sort=v:refname | tail -n2 | xargs | awk '{print $1".."$2}'))" changes="${changes//'%'/'%25'}" changes="${changes//$'\n'/'%0A'}" diff --git a/README.rst b/README.rst index bb6e8d4..a0ffdf0 100644 --- a/README.rst +++ b/README.rst @@ -223,28 +223,26 @@ Add direct support to scripts for a little more configurability: def get_main_parser(): parser = argparse.ArgumentParser(prog="pathcomplete") parser.add_argument( - "-s", "--print-completion-shell", choices=["bash", "zsh"] - ) - parser.add_argument( - "--file", - choices=shtab.Optional.FILE, # file tab completion, can be blank - ) - parser.add_argument( - "--dir", - choices=shtab.Required.DIRECTORY, # directory tab completion - default=".", + "-s", + "--print-completion-shell", + choices=["bash", "zsh"], + help="prints completion script", ) + # file & directory tab complete + parser.add_argument("file", nargs="?").complete = shtab.FILE + parser.add_argument("--dir", default=".").complete = shtab.DIRECTORY return parser if __name__ == "__main__": parser = get_main_parser() args = parser.parse_args() - print("received --file='%s' --dir='%s'" % (args.file, args.dir)) # completion magic shell = args.print_completion_shell if shell: print(shtab.complete(parser, shell=shell)) + else: + print("received =%r --dir=%r" % (args.file, args.dir)) docopt ~~~~~~ diff --git a/examples/customcomplete.py b/examples/customcomplete.py index d67e669..e57cda2 100755 --- a/examples/customcomplete.py +++ b/examples/customcomplete.py @@ -8,9 +8,9 @@ import shtab # for completion magic -CHOICE_FUNCTIONS = { - "bash": {"*.txt": "_shtab_greeter_compgen_TXTFiles"}, - "zsh": {"*.txt": "_files -g '(*.txt|*.TXT)'"}, +TXT_FILE = { + "bash": "_shtab_greeter_compgen_TXTFiles", + "zsh": "_files -g '(*.txt|*.TXT)'", } PREAMBLE = { "bash": """ @@ -25,27 +25,26 @@ } -class Optional(shtab.Required): - TXT_FILE = [shtab.Choice("*.txt", required=False)] - - -class Required(shtab.Required): - TXT_FILE = [shtab.Choice("*.txt", required=True)] - - def get_main_parser(): parser = argparse.ArgumentParser(prog="customcomplete") parser.add_argument( - "-s", "--print-completion-shell", choices=["bash", "zsh"] + "-s", + "--print-completion-shell", + choices=["bash", "zsh"], + help="prints completion script", ) + # `*.txt` file tab completion + parser.add_argument("input_txt", nargs="?").complete = TXT_FILE + # file tab completion builtin shortcut + parser.add_argument("-i", "--input-file").complete = shtab.FILE parser.add_argument( "-o", - "--output-txt", - choices=Optional.TXT_FILE, # *.txt file tab completion, can be blank - ) - parser.add_argument( - "input_txt", choices=Required.TXT_FILE, # cannot be blank - ) + "--output-name", + help=( + "output file name. Completes directory names to avoid users" + " accidentally overwriting existing files." + ), + ).complete = shtab.DIRECTORY # directory tab completion builtin shortcut return parser @@ -56,15 +55,10 @@ def get_main_parser(): # completion magic shell = args.print_completion_shell if shell: - script = shtab.complete( - parser, - shell=shell, - preamble=PREAMBLE[shell], - choice_functions=CHOICE_FUNCTIONS[shell], - ) + script = shtab.complete(parser, shell=shell, preamble=PREAMBLE) print(script) else: print( - "received input_txt='%s' --output-txt='%s'" - % (args.input_txt, args.output_txt) + "received =%r --output-dir=%r --output-name=%r" + % (args.input_txt, args.output_dir, args.output_name) ) diff --git a/examples/pathcomplete.py b/examples/pathcomplete.py index b5c61ad..749aaad 100755 --- a/examples/pathcomplete.py +++ b/examples/pathcomplete.py @@ -1,8 +1,7 @@ #!/usr/bin/env python """ `argparse`-based CLI app using -`add_argument(choices=shtab.(Optional|Required).(FILE|DIR)` -for file/directory completion. +`add_argument().complete = shtab.(FILE|DIR)` for file/dir tab completion. See `customcomplete.py` for a more advanced version. """ @@ -14,17 +13,14 @@ def get_main_parser(): parser = argparse.ArgumentParser(prog="pathcomplete") parser.add_argument( - "-s", "--print-completion-shell", choices=["bash", "zsh"] - ) - parser.add_argument( - "--file", - choices=shtab.Optional.FILE, # file tab completion, can be blank - ) - parser.add_argument( - "--dir", - choices=shtab.Required.DIRECTORY, # directory tab completion - default=".", + "-s", + "--print-completion-shell", + choices=["bash", "zsh"], + help="prints completion script", ) + # file & directory tab complete + parser.add_argument("file", nargs="?").complete = shtab.FILE + parser.add_argument("--dir", default=".").complete = shtab.DIRECTORY return parser @@ -37,4 +33,4 @@ def get_main_parser(): if shell: print(shtab.complete(parser, shell=shell)) else: - print("received --file='%s' --dir='%s'" % (args.file, args.dir)) + print("received =%r --dir=%r" % (args.file, args.dir)) diff --git a/shtab/__init__.py b/shtab/__init__.py index 7a10454..4543999 100644 --- a/shtab/__init__.py +++ b/shtab/__init__.py @@ -42,14 +42,12 @@ def get_version_dist(name=__name__): __all__ = ["Optional", "Required", "Choice", "complete"] log = logging.getLogger(__name__) -CHOICE_FUNCTIONS_BASH = { - "file": "_shtab_compgen_files", - "directory": "_shtab_compgen_dirs", -} -CHOICE_FUNCTIONS_ZSH = { - "file": "_files", - "directory": "_files -/", +CHOICE_FUNCTIONS = { + "file": {"bash": "_shtab_compgen_files", "zsh": "_files"}, + "directory": {"bash": "_shtab_compgen_dirs", "zsh": "_files -/"}, } +FILE = CHOICE_FUNCTIONS["file"] +DIRECTORY = DIR = CHOICE_FUNCTIONS["directory"] FLAG_OPTION = ( _StoreConstAction, _HelpAction, @@ -109,6 +107,14 @@ class Required(object): DIR = DIRECTORY = [Choice("directory", True)] +def complete2pattern(opt_complete, shell, choice_type2fn): + return ( + opt_complete.get(shell, "") + if isinstance(opt_complete, dict) + else choice_type2fn[opt_complete] + ) + + def replace_format(string, **fmt): """Similar to `string.format(**fmt)` but ignores unknown `{key}`s.""" for k, v in fmt.items(): @@ -116,18 +122,6 @@ def replace_format(string, **fmt): return string -def get_optional_actions(parser): - """Flattened list of all `parser`'s optional actions.""" - return sum( - ( - opt.option_strings - for opt in parser._get_optional_actions() - if opt.help != SUPPRESS - ), - [], - ) - - def get_bash_commands(root_parser, root_prefix, choice_functions=None): """ Recursive subcommand parser traversal, printing bash helper syntax. @@ -145,13 +139,24 @@ def get_bash_commands(root_parser, root_prefix, choice_functions=None): # `add_argument('subcommand', choices=shtab.Required.FILE)`) _{root_parser.prog}_{subcommand}_COMPGEN=_shtab_compgen_files """ - choice_type2fn = dict(CHOICE_FUNCTIONS_BASH) + choice_type2fn = {k: v["bash"] for k, v in CHOICE_FUNCTIONS.items()} if choice_functions: choice_type2fn.update(choice_functions) fd = io.StringIO() root_options = [] + def get_optional_actions(parser): + """Flattened list of all `parser`'s optional actions.""" + return sum( + ( + opt.option_strings + for opt in parser._get_optional_actions() + if opt.help != SUPPRESS + ), + [], + ) + def recurse(parser, prefix): positionals = parser._get_positional_actions() commands = [] @@ -176,6 +181,16 @@ def recurse(parser, prefix): for sub in positionals: if sub.choices: log.debug("choices:{}:{}".format(prefix, sorted(sub.choices))) + if hasattr(sub, "complete"): + print( + "{}_COMPGEN={}".format( + prefix, + complete2pattern( + sub.complete, "bash", choice_type2fn + ), + ), + file=fd, + ) for cmd in sorted(sub.choices): if isinstance(cmd, Choice): log.debug( @@ -342,7 +357,7 @@ def complete_zsh(parser, root_prefix=None, preamble="", choice_functions=None): root_arguments = [] subcommands = {} # {cmd: {"help": help, "arguments": [arguments]}} - choice_type2fn = dict(CHOICE_FUNCTIONS_ZSH) + choice_type2fn = {k: v["zsh"] for k, v in CHOICE_FUNCTIONS.items()} if choice_functions: choice_type2fn.update(choice_functions) @@ -368,7 +383,9 @@ def format_optional(opt): ), help=escape_zsh(opt.help or ""), dest=opt.dest, - pattern=( + pattern=complete2pattern(opt.complete, "zsh", choice_type2fn) + if hasattr(opt, "complete") + else ( choice_type2fn[opt.choices[0].type] if isinstance(opt.choices[0], Choice) else "({})".format(" ".join(opt.choices)) @@ -380,10 +397,12 @@ def format_optional(opt): ) def format_positional(opt): - return '"{nargs}:{help}:{choices}"'.format( + return '"{nargs}:{help}:{pattern}"'.format( nargs={"+": "*", "*": "*"}.get(opt.nargs, ""), help=escape_zsh((opt.help or opt.dest).strip().split("\n")[0]), - choices=( + pattern=complete2pattern(opt.complete, "zsh", choice_type2fn) + if hasattr(opt, "complete") + else ( choice_type2fn[opt.choices[0].type] if isinstance(opt.choices[0], Choice) else "({})".format(" ".join(opt.choices)) @@ -529,10 +548,15 @@ def complete( shell : str (bash/zsh) root_prefix : str, prefix for shell functions to avoid clashes (default: "_{parser.prog}") - preamble : str, prepended to generated script - choice_functions : dict, maps custom `shtab.Choice.type`s to - completion functions (possibly defined in `preamble`) + preamble : dict, mapping shell to text to prepend to generated script + (e.g. `{"bash": "_myprog_custom_function(){ echo hello }"}`) + choice_functions : deprecated. + + N.B. `parser.add_argument().complete = ...` can be used to define custom + completions (e.g. filenames). See <../examples/pathcomplete.py>. """ + if isinstance(preamble, dict): + preamble = preamble.get(shell, "") if shell == "bash": return complete_bash( parser,