1- import * as zsh from './zsh' ;
2- import * as bash from './bash' ;
3- import * as fish from './fish' ;
4- import * as powershell from './powershell' ;
5- import type { Command as CommanderCommand , ParseOptions } from 'commander' ;
1+ import type { Command as CommanderCommand } from 'commander' ;
62import t , { type RootCommand } from './t' ;
7- import { assertDoubleDashes } from './shared' ;
3+
4+ // rawArgs is available on (just) the Commander root command, but is not included in the TypeScript types.
5+ interface CommandWithRawArgs extends CommanderCommand {
6+ rawArgs : string [ ] ;
7+ }
88
99const execPath = process . execPath ;
1010const processArgs = process . argv . slice ( 1 ) ;
@@ -18,7 +18,10 @@ function quoteIfNeeded(path: string): string {
1818 return path . includes ( ' ' ) ? `'${ path } '` : path ;
1919}
2020
21- export default function tab ( instance : CommanderCommand ) : RootCommand {
21+ export default function tab (
22+ instance : CommanderCommand ,
23+ completionConfig ?: { completionCommandName ?: string }
24+ ) : RootCommand {
2225 const programName = instance . name ( ) ;
2326
2427 // Process the root command
@@ -27,95 +30,76 @@ export default function tab(instance: CommanderCommand): RootCommand {
2730 // Process all subcommands
2831 processSubcommands ( instance ) ;
2932
30- // Add the complete command for normal shell script generation
31- instance
32- . command ( 'complete [shell]' )
33+ // Make a `completion` command with a required command-argument.
34+ const completionCommandName =
35+ completionConfig ?. completionCommandName ?? 'complete' ;
36+ const completionCommand = instance
37+ . createCommand ( completionCommandName )
3338 . description ( 'Generate shell completion scripts' )
34- . action ( async ( shell ) => {
35- switch ( shell ) {
36- case 'zsh' : {
37- const script = zsh . generate ( programName , x ) ;
38- console . log ( script ) ;
39- break ;
40- }
41- case 'bash' : {
42- const script = bash . generate ( programName , x ) ;
43- console . log ( script ) ;
44- break ;
45- }
46- case 'fish' : {
47- const script = fish . generate ( programName , x ) ;
48- console . log ( script ) ;
49- break ;
50- }
51- case 'powershell' : {
52- const script = powershell . generate ( programName , x ) ;
53- console . log ( script ) ;
54- break ;
55- }
56- case 'debug' : {
57- // Debug mode to print all collected commands
58- const commandMap = new Map < string , CommanderCommand > ( ) ;
59- collectCommands ( instance , '' , commandMap ) ;
60- console . log ( 'Collected commands:' ) ;
61- for ( const [ path , cmd ] of commandMap . entries ( ) ) {
62- console . log (
63- `- ${ path || '<root>' } : ${ cmd . description ( ) || 'No description' } `
64- ) ;
65- }
66- break ;
67- }
68- default : {
69- console . error ( `Unknown shell: ${ shell } ` ) ;
70- console . error ( 'Supported shells: zsh, bash, fish, powershell' ) ;
71- process . exit ( 1 ) ;
72- }
73- }
39+ . addArgument (
40+ instance
41+ . createArgument ( '<shell>' , 'Shell type for completion script' )
42+ . choices ( [ 'zsh' , 'bash' , 'fish' , 'powershell' ] )
43+ )
44+ . action ( ( shell ) => {
45+ t . setup ( programName , x , shell ) ;
7446 } ) ;
47+ completionCommand . copyInheritedSettings ( instance ) ;
48+
49+ // Make a `complete` command for generating tab-time complete suggestions.
50+ const completeCommand = instance
51+ . createCommand ( 'complete' )
52+ . description ( 'Generate completion suggestions' )
53+ . usage ( '-- [args...]' )
54+ . argument ( '[args...]' )
55+ . action ( ( args ) => {
56+ if ( completionCommandName !== 'complete' ) {
57+ // Check for user trying to generate shell completion script, since not using usual tab overloaded complete `command`.
58+ const rawArgs = ( instance as CommandWithRawArgs ) . rawArgs ;
59+ if ( args . length === 1 && ! rawArgs . includes ( '--' ) )
60+ instance . error (
61+ `error: completion requests are called like \`complete -- [args]\`.\n(Did you mean \`${ completionCommandName } ${ args [ 0 ] } \` to generate shell script?)`
62+ ) ;
63+ }
7564
76- const getCompletionArgs = ( argv ?: readonly string [ ] ) : string [ ] | null => {
77- const args = argv || process . argv ;
78- const completeIndex = args . findIndex ( ( arg ) => arg === 'complete' ) ;
79- const dashDashIndex = args . findIndex ( ( arg ) => arg === '--' ) ;
80-
81- if (
82- completeIndex !== - 1 &&
83- dashDashIndex !== - 1 &&
84- dashDashIndex > completeIndex
85- ) {
86- return args . slice ( dashDashIndex + 1 ) ;
87- }
88-
89- return null ;
90- } ;
91-
92- const handleCompletion = ( extra : string [ ] ) : void => {
93- assertDoubleDashes ( programName ) ;
94- t . parse ( extra ) ;
95- } ;
65+ t . parse ( args ) ;
66+ } ) ;
67+ completeCommand . copyInheritedSettings ( instance ) ;
9668
97- const originalParse = instance . parse . bind ( instance ) ;
98- instance . parse = function ( argv ?: readonly string [ ] , options ?: ParseOptions ) {
99- const extra = getCompletionArgs ( argv ) ;
100- if ( extra ) {
101- handleCompletion ( extra ) ;
102- return instance ;
103- }
104- return originalParse ( argv , options ) ;
105- } ;
106-
107- const originalParseAsync = instance . parseAsync . bind ( instance ) ;
108- instance . parseAsync = async function (
109- argv ?: readonly string [ ] ,
110- options ?: ParseOptions
111- ) {
112- const extra = getCompletionArgs ( argv ) ;
113- if ( extra ) {
114- handleCompletion ( extra ) ;
115- return instance ;
116- }
117- return originalParseAsync ( argv , options ) ;
118- } ;
69+ if ( completionCommandName !== 'complete' ) {
70+ // We have indepdendent commands so can hook them up directly.
71+ instance . addCommand ( completionCommand ) ;
72+ instance . addCommand ( completeCommand , { hidden : true } ) ;
73+ } else {
74+ // We need to add a dual-use command, work out calling pattern, and dispatch.
75+ instance
76+ . command ( 'complete' )
77+ . description ( 'Generate shell completion scripts' )
78+ . argument (
79+ '[shell]' ,
80+ 'shell type (choices: "zsh", "bash", "fish", "powershell")'
81+ )
82+ . allowExcessArguments ( )
83+ . action ( ( _shell , _options , cmd ) => {
84+ // Work out how we are being called, by user or by script as completion handler.
85+ const rawArgs = ( instance as CommandWithRawArgs ) . rawArgs ;
86+ const completeIndex = rawArgs . indexOf ( 'complete' ) ;
87+ const dashDashIndex = rawArgs . indexOf ( '--' ) ;
88+
89+ if (
90+ completeIndex !== - 1 &&
91+ dashDashIndex !== - 1 &&
92+ dashDashIndex === completeIndex + 1
93+ ) {
94+ // Commander stripped `--`, so put it back for reparse
95+ completeCommand . parse ( [ '--' , ...cmd . args ] , { from : 'user' } ) ;
96+ } else {
97+ completionCommand . parse ( cmd . args , {
98+ from : 'user' ,
99+ } ) ;
100+ }
101+ } ) ;
102+ }
119103
120104 return t ;
121105}
0 commit comments