16
16
)
17
17
from collections import defaultdict
18
18
from functools import total_ordering
19
+ from itertools import starmap
19
20
from string import Template
20
21
from typing import Any , Dict , List
21
22
from typing import Optional as Opt
@@ -452,9 +453,8 @@ def complete_zsh(parser, root_prefix=None, preamble="", choice_functions=None):
452
453
453
454
See `complete` for arguments.
454
455
"""
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 ))
458
458
459
459
choice_type2fn = {k : v ["zsh" ] for k , v in CHOICE_FUNCTIONS .items ()}
460
460
if choice_functions :
@@ -486,47 +486,123 @@ def format_positional(opt):
486
486
"({})" .format (" " .join (map (str , opt .choices )))) if opt .choices else "" ,
487
487
)
488
488
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 )
527
599
528
- log .debug ("subcommands:%s:%s" , root_prefix , sorted (subcommands ))
600
+ preamble = """\
601
+ # Custom Preamble
602
+ {}
529
603
604
+ # End Custom Preamble
605
+ """ .format (preamble .rstrip ()) if preamble else ""
530
606
# References:
531
607
# - https://github.com/zsh-users/zsh-completions
532
608
# - http://zsh.sourceforge.net/Doc/Release/Completion-System.html
@@ -538,49 +614,21 @@ def format_positional(opt):
538
614
539
615
# AUTOMATCALLY GENERATED by `shtab`
540
616
541
- ${root_prefix}_options_=(
542
- ${root_options}
543
- )
617
+ ${command_commands}
544
618
545
- ${root_prefix}_commands_() {
546
- local _commands=(
547
- ${commands}
548
- )
619
+ ${command_options}
549
620
550
- _describe '${prog} commands' _commands
551
- }
552
- ${subcommands}
621
+ ${command_cases}
553
622
${preamble}
554
- typeset -A opt_args
555
- local context state line curcontext="$curcontext"
556
623
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 ,
566
627
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 ,
584
632
)
585
633
586
634
0 commit comments