1616)
1717from collections import defaultdict
1818from functools import total_ordering
19+ from itertools import starmap
1920from string import Template
2021from typing import Any , Dict , List
2122from typing import Optional as Opt
@@ -452,9 +453,8 @@ def complete_zsh(parser, root_prefix=None, preamble="", choice_functions=None):
452453
453454 See `complete` for arguments.
454455 """
455- root_prefix = wordify ("_shtab_" + (root_prefix or parser .prog ))
456- root_arguments = []
457- subcommands = {} # {cmd: {"help": help, "arguments": [arguments]}}
456+ prog = parser .prog
457+ root_prefix = wordify ("_shtab_" + (root_prefix or prog ))
458458
459459 choice_type2fn = {k : v ["zsh" ] for k , v in CHOICE_FUNCTIONS .items ()}
460460 if choice_functions :
@@ -486,47 +486,123 @@ def format_positional(opt):
486486 "({})" .format (" " .join (map (str , opt .choices )))) if opt .choices else "" ,
487487 )
488488
489- for sub in parser ._get_positional_actions ():
490- if not sub .choices or not isinstance (sub .choices , dict ):
491- # positional argument
492- opt = sub
493- if opt .help != SUPPRESS :
494- root_arguments .append (format_positional (opt ))
495- else : # subparser
496- log .debug ("choices:{}:{}" .format (root_prefix , sorted (sub .choices )))
497- public_cmds = get_public_subcommands (sub )
498- for cmd , subparser in sub .choices .items ():
499- if cmd not in public_cmds :
500- log .debug ("skip:subcommand:%s" , cmd )
501- continue
502- log .debug ("subcommand:%s" , cmd )
503-
504- # optionals
505- arguments = [
506- format_optional (opt ) for opt in subparser ._get_optional_actions ()
507- if opt .help != SUPPRESS ]
508-
509- # subcommand positionals
510- subsubs = sum (
511- (list (opt .choices ) for opt in subparser ._get_positional_actions ()
512- if isinstance (opt .choices , dict )),
513- [],
514- )
515- if subsubs :
516- arguments .append ('"1:Sub command:({})"' .format (" " .join (subsubs )))
517-
518- # positionals
519- arguments .extend (
520- format_positional (opt ) for opt in subparser ._get_positional_actions ()
521- if not isinstance (opt .choices , dict ) if opt .help != SUPPRESS )
522-
523- subcommands [cmd ] = {
524- "help" : (subparser .description or "" ).strip ().split ("\n " )[0 ],
525- "arguments" : arguments }
526- log .debug ("subcommands:%s:%s" , cmd , subcommands [cmd ])
489+ # {cmd: {"help": help, "arguments": [arguments]}}
490+ all_commands = {
491+ root_prefix : {
492+ "cmd" : prog , "arguments" : [
493+ format_optional (opt ) for opt in parser ._get_optional_actions ()
494+ if opt .help != SUPPRESS ], "help" : (parser .description
495+ or "" ).strip ().split ("\n " )[0 ], "commands" : [],
496+ "paths" : []}}
497+
498+ def recurse (parser , prefix , paths = None ):
499+ paths = paths or []
500+ subcmds = []
501+ for sub in parser ._get_positional_actions ():
502+ if sub .help == SUPPRESS or not sub .choices :
503+ continue
504+ if not sub .choices or not isinstance (sub .choices , dict ):
505+ # positional argument
506+ all_commands [prefix ]["arguments" ].append (format_positional (sub ))
507+ else : # subparser
508+ log .debug ("choices:{}:{}" .format (prefix , sorted (sub .choices )))
509+ public_cmds = get_public_subcommands (sub )
510+ for cmd , subparser in sub .choices .items ():
511+ if cmd not in public_cmds :
512+ log .debug ("skip:subcommand:%s" , cmd )
513+ continue
514+ log .debug ("subcommand:%s" , cmd )
515+
516+ # optionals
517+ arguments = [
518+ format_optional (opt ) for opt in subparser ._get_optional_actions ()
519+ if opt .help != SUPPRESS ]
520+
521+ # positionals
522+ arguments .extend (
523+ format_positional (opt ) for opt in subparser ._get_positional_actions ()
524+ if not isinstance (opt .choices , dict ) if opt .help != SUPPRESS )
525+
526+ new_pref = prefix + "_" + wordify (cmd )
527+ options = all_commands [new_pref ] = {
528+ "cmd" : cmd , "help" : (subparser .description or "" ).strip ().split ("\n " )[0 ],
529+ "arguments" : arguments , "paths" : [* paths , cmd ]}
530+ new_subcmds = recurse (subparser , new_pref , [* paths , cmd ])
531+ options ["commands" ] = {
532+ all_commands [pref ]["cmd" ]: all_commands [pref ]
533+ for pref in new_subcmds if pref in all_commands }
534+ subcmds .extend ([* new_subcmds , new_pref ])
535+ log .debug ("subcommands:%s:%s" , cmd , options )
536+ return subcmds
537+
538+ recurse (parser , root_prefix )
539+ all_commands [root_prefix ]["commands" ] = {
540+ options ["cmd" ]: options
541+ for prefix , options in sorted (all_commands .items ())
542+ if len (options .get ("paths" , [])) < 2 and prefix != root_prefix }
543+ subcommands = {
544+ prefix : options
545+ for prefix , options in all_commands .items () if options .get ("commands" )}
546+ subcommands .setdefault (root_prefix , all_commands [root_prefix ])
547+ log .debug ("subcommands:%s:%s" , root_prefix , sorted (all_commands ))
548+
549+ def command_case (prefix , options ):
550+ name = options ["cmd" ]
551+ commands = options ["commands" ]
552+ case_fmt_on_no_sub = """{name}) _arguments -C ${prefix}_{name}_options ;;"""
553+ case_fmt_on_sub = """{name}) {prefix}_{name} ;;"""
554+
555+ cases = []
556+ for _ , options in sorted (commands .items ()):
557+ fmt = case_fmt_on_sub if options .get ("commands" ) else case_fmt_on_no_sub
558+ cases .append (fmt .format (name = options ["cmd" ], prefix = prefix ))
559+ cases = "\n \t " .expandtabs (8 ).join (cases )
560+
561+ return """\
562+ {prefix}() {{
563+ local context state line curcontext="$curcontext"
564+
565+ _arguments -C ${prefix}_options \\
566+ ': :{prefix}_commands' \\
567+ '*::: :->{name}'
568+
569+ case $state in
570+ {name})
571+ words=($line[1] "${{words[@]}}")
572+ (( CURRENT += 1 ))
573+ curcontext="${{curcontext%:*:*}}:{prefix}-$line[1]:"
574+ case $line[1] in
575+ {cases}
576+ esac
577+ esac
578+ }}
579+ """ .format (prefix = prefix , name = name , cases = cases )
580+
581+ def command_option (prefix , options ):
582+ return """\
583+ {prefix}_options=(
584+ {arguments}
585+ )
586+ """ .format (prefix = prefix , arguments = "\n " .join (options ["arguments" ]))
587+
588+ def command_list (prefix , options ):
589+ name = " " .join ([prog , * options ["paths" ]])
590+ commands = "\n " .join ('"{}:{}"' .format (cmd , escape_zsh (opt ["help" ]))
591+ for cmd , opt in sorted (options ["commands" ].items ()))
592+ return """
593+ {prefix}_commands() {{
594+ local _commands=(
595+ {commands}
596+ )
597+ _describe '{name} commands' _commands
598+ }}""" .format (prefix = prefix , name = name , commands = commands )
527599
528- log .debug ("subcommands:%s:%s" , root_prefix , sorted (subcommands ))
600+ preamble = """\
601+ # Custom Preamble
602+ {}
529603
604+ # End Custom Preamble
605+ """ .format (preamble .rstrip ()) if preamble else ""
530606 # References:
531607 # - https://github.com/zsh-users/zsh-completions
532608 # - http://zsh.sourceforge.net/Doc/Release/Completion-System.html
@@ -538,49 +614,21 @@ def format_positional(opt):
538614
539615# AUTOMATCALLY GENERATED by `shtab`
540616
541- ${root_prefix}_options_=(
542- ${root_options}
543- )
617+ ${command_commands}
544618
545- ${root_prefix}_commands_() {
546- local _commands=(
547- ${commands}
548- )
619+ ${command_options}
549620
550- _describe '${prog} commands' _commands
551- }
552- ${subcommands}
621+ ${command_cases}
553622${preamble}
554- typeset -A opt_args
555- local context state line curcontext="$curcontext"
556623
557- _arguments \\
558- $$${root_prefix}_options_ \\
559- ${root_arguments} \\
560- ': :${root_prefix}_commands_' \\
561- '*::args:->args'
562-
563- case $words[1] in
564- ${commands_case}
565- esac""" ).safe_substitute (
624+ typeset -A opt_args
625+ ${root_prefix} "$@\" """ ).safe_substitute (
626+ prog = prog ,
566627 root_prefix = root_prefix ,
567- prog = parser .prog ,
568- commands = "\n " .join ('"{}:{}"' .format (cmd , escape_zsh (subcommands [cmd ]["help" ]))
569- for cmd in sorted (subcommands )),
570- root_arguments = " \\ \n " .join (root_arguments ),
571- root_options = "\n " .join (
572- format_optional (opt ) for opt in parser ._get_optional_actions ()
573- if opt .help != SUPPRESS ),
574- commands_case = "\n " .join ("{cmd_orig}) _arguments ${root_prefix}_{cmd} ;;" .format (
575- cmd_orig = cmd , cmd = wordify (cmd ), root_prefix = root_prefix )
576- for cmd in sorted (subcommands )),
577- subcommands = "\n " .join ("""
578- {root_prefix}_{cmd}=(
579- {arguments}
580- )""" .format (root_prefix = root_prefix , cmd = wordify (cmd ), arguments = "\n " .join (
581- subcommands [cmd ]["arguments" ])) for cmd in sorted (subcommands )),
582- preamble = ("\n # Custom Preamble\n " + preamble +
583- "\n # End Custom Preamble\n " if preamble else "" ),
628+ command_cases = "\n " .join (starmap (command_case , sorted (subcommands .items ()))),
629+ command_commands = "\n " .join (starmap (command_list , sorted (subcommands .items ()))),
630+ command_options = "\n " .join (starmap (command_option , sorted (all_commands .items ()))),
631+ preamble = preamble ,
584632 )
585633
586634
0 commit comments