diff --git a/__tests__/lib/cli/command.js b/__tests__/lib/cli/command.js index 02f36bfe1..2a42541e5 100644 --- a/__tests__/lib/cli/command.js +++ b/__tests__/lib/cli/command.js @@ -41,6 +41,40 @@ describe( 'utils/cli/command', () => { } ); describe( 'subcommand dispatch', () => { + it( 'allows wp option-only invocations without separator', async () => { + const parentScript = path.join( process.cwd(), 'src/bin/vip.js' ); + const childScript = path.join( process.cwd(), 'src/bin/vip-wp.js' ); + + const cmd = command( { requiredArgs: 0 } ).command( 'wp', 'Run WP-CLI commands.' ); + + await cmd.argv( [ process.execPath, parentScript, 'wp', '--help' ] ); + + expect( spawnSync ).toHaveBeenCalledWith( process.execPath, [ childScript, '--help' ], { + stdio: 'inherit', + env: process.env, + } ); + expect( process.exit ).toHaveBeenCalledWith( 0 ); + } ); + + it( 'allows wp option-only invocations with option values without separator', async () => { + const parentScript = path.join( process.cwd(), 'src/bin/vip.js' ); + const childScript = path.join( process.cwd(), 'src/bin/vip-wp.js' ); + + const cmd = command( { requiredArgs: 0 } ).command( 'wp', 'Run WP-CLI commands.' ); + + await cmd.argv( [ process.execPath, parentScript, 'wp', '--path', '/tmp/wp' ] ); + + expect( spawnSync ).toHaveBeenCalledWith( + process.execPath, + [ childScript, '--path', '/tmp/wp' ], + { + stdio: 'inherit', + env: process.env, + } + ); + expect( process.exit ).toHaveBeenCalledWith( 0 ); + } ); + it( 'dispatches when child options appear before -- and subcommand follows separator', async () => { const parentScript = path.join( process.cwd(), 'src/bin/vip.js' ); const childScript = path.join( process.cwd(), 'src/bin/vip-wp.js' ); diff --git a/src/lib/cli/command.js b/src/lib/cli/command.js index 85cfbaf32..97a0a0ee3 100644 --- a/src/lib/cli/command.js +++ b/src/lib/cli/command.js @@ -394,6 +394,52 @@ class CommanderArgsCompat { return null; } + hasWpCommandPayload( subcommandArgs ) { + const wpFlagsWithoutValue = new Set( [ '--help', '-h', '--version', '-v', '--yes', '-y' ] ); + let expectOptionValue = false; + + for ( const arg of subcommandArgs ) { + if ( expectOptionValue ) { + if ( ! isOptionToken( arg ) ) { + expectOptionValue = false; + continue; + } + + expectOptionValue = false; + } + + if ( ! isOptionToken( arg ) ) { + return true; + } + + if ( arg.startsWith( '--' ) ) { + if ( arg.includes( '=' ) || wpFlagsWithoutValue.has( arg ) ) { + continue; + } + + expectOptionValue = true; + continue; + } + + if ( /^-[A-Za-z0-9]$/.test( arg ) ) { + if ( wpFlagsWithoutValue.has( arg ) ) { + continue; + } + + expectOptionValue = true; + continue; + } + + if ( /^-[A-Za-z0-9]+=/.test( arg ) ) { + continue; + } + + return true; + } + + return false; + } + async executeSubcommand( argv, parsedAlias, subcommand ) { const currentScript = argv[ 1 ]; const subcommandName = subcommand.name; @@ -407,8 +453,9 @@ class CommanderArgsCompat { const hasSeparatorBeforeSubcommand = rawArgsBeforeSubcommand.includes( '--' ); const argsBeforeSubcommand = rawArgsBeforeSubcommand.filter( arg => arg !== '--' ); const subcommandArgs = parsedAlias.argv.slice( subcommand.index + 1 ); + const hasWpCommandPayload = this.hasWpCommandPayload( subcommandArgs ); const hasSeparator = argv.includes( '--' ); - if ( subcommandName === 'wp' && subcommandArgs.length && ! hasSeparator ) { + if ( subcommandName === 'wp' && hasWpCommandPayload && ! hasSeparator ) { exit.withError( 'A double dash ("--") must separate the arguments of "vip" from those of "wp". Run "vip wp --help" for examples.' );