627627"""
628628
629629
630+ TCSH_PREAMBLE_TMPL = r"""
631+ # Dynamic completion helpers for tcsh
632+
633+ alias _borg_complete_timestamp 'date +"%Y-%m-%dT%H:%M:%S"'
634+
635+
636+ alias _borg_complete_sortby "echo {SORT_KEYS}"
637+ alias _borg_complete_filescachemode "echo {FCM_KEYS}"
638+ alias _borg_help_topics "echo {HELP_CHOICES}"
639+ alias _borg_complete_compression_spec "echo {COMP_SPEC_CHOICES}"
640+ alias _borg_complete_chunker_params "echo {CHUNKER_PARAMS_CHOICES}"
641+ alias _borg_complete_relative_time "echo {RELATIVE_TIME_CHOICES}"
642+ alias _borg_complete_file_size "echo {FILE_SIZE_CHOICES}"
643+ """
644+
645+
646+ def _monkeypatch_shtab ():
647+ """
648+ Monkeypatches shtab's tcsh completion logic to fix severe parsing issues and add missing features.
649+
650+ 1. Subcommand Positional Completion: shtab lacks native support for auto-completing positional
651+ arguments that belong to subcommands in tcsh (e.g., `borg help <topic>`). This builds a
652+ conditional evaluation structure (`if ( $#cmd >= X && ... )`) to support them.
653+ 2. Subshell Array Indexing Fix: `tcsh` aggressively evaluates array indices like `$cmd[2]` even
654+ if the array is smaller than the requested index, causing "if: Empty if." errors. Added
655+ explicit bounds checking (`$#cmd >= max_idx`).
656+ 3. Nested Subshell Safety: Standard shtab nests subshells using backticks which causes recursive
657+ parsing crashes in tcsh. Replaced with safe `eval` usage.
658+ """
659+ import shtab
660+ from shtab import CHOICE_FUNCTIONS , complete2pattern
661+ from collections import defaultdict
662+ from argparse import SUPPRESS
663+ from string import Template
664+
665+ def patched_complete_tcsh (parser , root_prefix = None , preamble = "" , choice_functions = None ):
666+ optionals_single = set ()
667+ optionals_double = set ()
668+ specials = []
669+ index_choices = defaultdict (dict )
670+
671+ choice_type2fn = {k : v ["tcsh" ] for k , v in CHOICE_FUNCTIONS .items ()}
672+
673+ if choice_functions :
674+ choice_type2fn .update (choice_functions )
675+
676+ def get_specials (arg , arg_type , arg_sel ):
677+ if arg .choices :
678+ choice_strs = " " .join (map (str , arg .choices ))
679+ yield f"'{ arg_type } /{ arg_sel } /({ choice_strs } )/'"
680+ elif hasattr (arg , "complete" ):
681+ complete_fn = complete2pattern (arg .complete , "tcsh" , choice_type2fn )
682+ if complete_fn :
683+ yield f"'{ arg_type } /{ arg_sel } /{ complete_fn } /'"
684+
685+ def recurse_parser (cparser , positional_idx , requirements = None ):
686+ if requirements is None :
687+ requirements = []
688+
689+ for optional in cparser ._get_optional_actions ():
690+ if optional .help != SUPPRESS :
691+ for optional_str in optional .option_strings :
692+ if optional_str .startswith ("--" ):
693+ optionals_double .add (optional_str [2 :])
694+ elif optional_str .startswith ("-" ):
695+ optionals_single .add (optional_str [1 :])
696+ specials .extend (get_specials (optional , "n" , optional_str ))
697+ if optional .nargs != 0 :
698+ specials .extend (get_specials (optional , "c" , optional_str + "=" ))
699+
700+ for positional in cparser ._get_positional_actions ():
701+ if positional .help != SUPPRESS :
702+ positional_idx += 1
703+ index_choices [positional_idx ][tuple (requirements )] = positional
704+ if isinstance (positional .choices , dict ):
705+ for subcmd , subparser in positional .choices .items ():
706+ recurse_parser (subparser , positional_idx , requirements + [subcmd ])
707+
708+ recurse_parser (parser , 0 )
709+
710+ for idx , ndict in index_choices .items ():
711+ if len (ndict ) == 1 :
712+ arg = list (ndict .values ())[0 ]
713+ specials .extend (get_specials (arg , "p" , str (idx )))
714+ else :
715+ nlist = []
716+ for nn , arg in ndict .items ():
717+ max_idx = len (nn ) + 1
718+ checks = [f'("$cmd[{ iidx } ]" == "{ n } ")' for iidx , n in enumerate (nn , start = 2 )]
719+ condition = f"$#cmd >= { max_idx } && " + " && " .join (checks )
720+ if arg .choices :
721+ choices_str = " " .join (map (str , arg .choices ))
722+ nlist .append (f"if ( { condition } ) echo { choices_str } " )
723+ elif hasattr (arg , "complete" ):
724+ complete_fn = complete2pattern (arg .complete , "tcsh" , choice_type2fn )
725+ if complete_fn :
726+ if complete_fn .startswith ("`" ) and complete_fn .endswith ("`" ):
727+ func_name = complete_fn .strip ("`" )
728+ nlist .append (f"if ( { condition } ) eval { func_name } " )
729+ else :
730+ nlist .append (f"if ( { condition } ) { complete_fn } " )
731+ if nlist :
732+ nlist_str = "; " .join (nlist )
733+ padding = '"" "" "" "" "" "" "" "" ""'
734+ specials .append (f"'p@{ str (idx )} @`set cmd=(\" $COMMAND_LINE\" { padding } ); { nlist_str } `@'" )
735+
736+ if optionals_double :
737+ if optionals_single :
738+ optionals_single .add ("-" )
739+ else :
740+ optionals_single = ("-" , "-" )
741+
742+ specials = list (dict .fromkeys (specials ))
743+
744+ return Template (
745+ """\
746+ # AUTOMATICALLY GENERATED by `shtab`
747+
748+ ${preamble}
749+
750+ complete ${prog} \\
751+ 'c/--/(${optionals_double_str})/' \\
752+ 'c/-/(${optionals_single_str})/' \\
753+ ${optionals_special_str} \\
754+ 'p/*/()/'"""
755+ ).safe_substitute (
756+ preamble = ("\n # Custom Preamble\n " + preamble + "\n # End Custom Preamble\n " if preamble else "" ),
757+ root_prefix = root_prefix ,
758+ prog = parser .prog ,
759+ optionals_double_str = " " .join (sorted (optionals_double )),
760+ optionals_single_str = " " .join (sorted (optionals_single )),
761+ optionals_special_str = " \\ \n " .join (specials ),
762+ )
763+
764+ shtab .complete_tcsh = patched_complete_tcsh
765+ shtab ._SUPPORTED_COMPLETERS ["tcsh" ] = patched_complete_tcsh
766+
767+
630768def _attach_completion (parser : ArgumentParser , type_class , completion_dict : dict ):
631769 """Tag all arguments with type `type_class` with completion choices from `completion_dict`."""
632770
@@ -659,32 +797,72 @@ def do_completion(self, args):
659797 # adds dynamic completion for archive IDs with the aid: prefix for all ARCHIVE
660798 # arguments (identified by archivename_validator). It reuses `borg repo-list`
661799 # to enumerate archives and does not introduce any new commands or caching.
800+ _monkeypatch_shtab ()
662801 parser = self .build_parser ()
663802 _attach_completion (
664803 parser , archivename_validator , {"bash" : "_borg_complete_archive" , "zsh" : "_borg_complete_archive" }
665804 )
666- _attach_completion ( parser , SortBySpec , { "bash" : "_borg_complete_sortby" , "zsh" : "_borg_complete_sortby" })
805+
667806 _attach_completion (
668- parser , FilesCacheMode , {"bash" : "_borg_complete_filescachemode" , "zsh" : "_borg_complete_filescachemode" }
807+ parser ,
808+ SortBySpec ,
809+ {"bash" : "_borg_complete_sortby" , "zsh" : "_borg_complete_sortby" , "tcsh" : "`_borg_complete_sortby`" },
810+ )
811+ _attach_completion (
812+ parser ,
813+ FilesCacheMode ,
814+ {
815+ "bash" : "_borg_complete_filescachemode" ,
816+ "zsh" : "_borg_complete_filescachemode" ,
817+ "tcsh" : "`_borg_complete_filescachemode`" ,
818+ },
669819 )
670820 _attach_completion (
671821 parser ,
672822 CompressionSpec ,
673- {"bash" : "_borg_complete_compression_spec" , "zsh" : "_borg_complete_compression_spec" },
823+ {
824+ "bash" : "_borg_complete_compression_spec" ,
825+ "zsh" : "_borg_complete_compression_spec" ,
826+ "tcsh" : "`_borg_complete_compression_spec`" ,
827+ },
674828 )
675829 _attach_completion (parser , PathSpec , shtab .DIRECTORY )
676830 _attach_completion (
677- parser , ChunkerParams , {"bash" : "_borg_complete_chunker_params" , "zsh" : "_borg_complete_chunker_params" }
831+ parser ,
832+ ChunkerParams ,
833+ {
834+ "bash" : "_borg_complete_chunker_params" ,
835+ "zsh" : "_borg_complete_chunker_params" ,
836+ "tcsh" : "`_borg_complete_chunker_params`" ,
837+ },
678838 )
679839 _attach_completion (parser , tag_validator , {"bash" : "_borg_complete_tags" , "zsh" : "_borg_complete_tags" })
680840 _attach_completion (
681841 parser ,
682842 relative_time_marker_validator ,
683- {"bash" : "_borg_complete_relative_time" , "zsh" : "_borg_complete_relative_time" },
843+ {
844+ "bash" : "_borg_complete_relative_time" ,
845+ "zsh" : "_borg_complete_relative_time" ,
846+ "tcsh" : "`_borg_complete_relative_time`" ,
847+ },
684848 )
685- _attach_completion (parser , timestamp , {"bash" : "_borg_complete_timestamp" , "zsh" : "_borg_complete_timestamp" })
686849 _attach_completion (
687- parser , parse_file_size , {"bash" : "_borg_complete_file_size" , "zsh" : "_borg_complete_file_size" }
850+ parser ,
851+ timestamp ,
852+ {
853+ "bash" : "_borg_complete_timestamp" ,
854+ "zsh" : "_borg_complete_timestamp" ,
855+ "tcsh" : "`_borg_complete_timestamp`" ,
856+ },
857+ )
858+ _attach_completion (
859+ parser ,
860+ parse_file_size ,
861+ {
862+ "bash" : "_borg_complete_file_size" ,
863+ "zsh" : "_borg_complete_file_size" ,
864+ "tcsh" : "`_borg_complete_file_size`" ,
865+ },
688866 )
689867
690868 # Collect all commands and help topics for "borg help" completion
@@ -694,7 +872,9 @@ def do_completion(self, args):
694872 help_choices .extend (action .choices .keys ())
695873
696874 help_completion_fn = "_borg_help_topics"
697- _attach_help_completion (parser , {"bash" : help_completion_fn , "zsh" : help_completion_fn })
875+ _attach_help_completion (
876+ parser , {"bash" : help_completion_fn , "zsh" : help_completion_fn , "tcsh" : "`_borg_help_topics`" }
877+ )
698878
699879 # Build preambles using partial_format to avoid escaping braces etc.
700880 sort_keys = " " .join (AI_HUMAN_SORT_KEYS )
@@ -730,11 +910,14 @@ def do_completion(self, args):
730910 }
731911 bash_preamble = partial_format (BASH_PREAMBLE_TMPL , mapping )
732912 zsh_preamble = partial_format (ZSH_PREAMBLE_TMPL , mapping )
913+ tcsh_preamble = partial_format (TCSH_PREAMBLE_TMPL , mapping )
733914
734915 if args .shell == "bash" :
735916 preambles = [bash_preamble ]
736917 elif args .shell == "zsh" :
737918 preambles = [zsh_preamble ]
919+ elif args .shell == "tcsh" :
920+ preambles = [tcsh_preamble ]
738921 else :
739922 preambles = []
740923 script = parser .get_completion_script (f"shtab-{ args .shell } " , preambles = preambles )
0 commit comments