Skip to content
77 changes: 77 additions & 0 deletions features/command.feature
Original file line number Diff line number Diff line change
Expand Up @@ -1751,6 +1751,83 @@ Feature: WP-CLI Commands
"""
And the return code should be 0

Scenario: Space-separated numeric arguments should be split on Windows
# This test simulates Windows PowerShell behavior using WP_CLI_TEST_IS_WINDOWS=1
# which activates the argument splitting logic on Unix-like test environments.
# The actual behavior on Windows systems will be identical.
Given an empty directory
And a custom-cmd.php file:
"""
<?php
/**
* Test command for space-separated arguments
*
* <ids>...
* : One or more IDs
*
* @when before_wp_load
*/
function test_ids_command( $args ) {
WP_CLI::log( 'Number of arguments: ' . count( $args ) );
foreach ( $args as $id ) {
WP_CLI::log( 'ID: ' . $id );
}
}
WP_CLI::add_command( 'test-ids', 'test_ids_command' );
"""

# Test on Windows with space-separated IDs as a single argument
When I run `WP_CLI_TEST_IS_WINDOWS=1 wp --require=custom-cmd.php test-ids "123 456 789"`
Then STDOUT should contain:
"""
Number of arguments: 3
"""
And STDOUT should contain:
"""
ID: 123
"""
And STDOUT should contain:
"""
ID: 456
"""
And STDOUT should contain:
"""
ID: 789
"""

# Test on Windows with single ID (should not be split)
When I run `WP_CLI_TEST_IS_WINDOWS=1 wp --require=custom-cmd.php test-ids 123`
Then STDOUT should contain:
"""
Number of arguments: 1
"""
And STDOUT should contain:
"""
ID: 123
"""

# Test on non-Windows (should pass through as-is)
When I run `WP_CLI_TEST_IS_WINDOWS=0 wp --require=custom-cmd.php test-ids "123 456"`
Then STDOUT should contain:
"""
Number of arguments: 1
"""
And STDOUT should contain:
"""
ID: 123 456
"""

# Test on Windows with mixed arguments (non-numeric strings should not be split)
When I run `WP_CLI_TEST_IS_WINDOWS=1 wp --require=custom-cmd.php test-ids "hello world"`
Then STDOUT should contain:
"""
Number of arguments: 1
"""
And STDOUT should contain:
"""
ID: hello world
"""

Scenario: Warn when command overrides global argument
Given an empty directory
And a custom-cmd.php file:
Expand Down
21 changes: 21 additions & 0 deletions php/WP_CLI/Runner.php
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,27 @@ public function get_wp_config_code( $wp_config_path = '' ) {
* @return array
*/
private static function back_compat_conversions( $args, $assoc_args ) {
// On Windows (PowerShell), command substitution like $(wp post list --format=ids)
// returns space-separated values as a single string argument instead of separate arguments.
// Split such arguments to maintain compatibility with Unix-like behavior.
if ( Utils\is_windows() ) {
$split_args = [];
foreach ( $args as $arg ) {
// Check if the argument contains space-separated numeric IDs
// We only split if the entire argument matches the pattern of space-separated numbers
if ( is_string( $arg ) && preg_match( '/^\d+(\s+\d+)+$/', $arg ) ) {
// Split on whitespace and add each ID as a separate argument
$ids = preg_split( '/\s+/', $arg, -1, PREG_SPLIT_NO_EMPTY );
if ( false !== $ids ) {
array_push( $split_args, ...$ids );
}
} else {
$split_args[] = $arg;
}
}
$args = $split_args;
}

$top_level_aliases = [
'sql' => 'db',
'blog' => 'site',
Expand Down
103 changes: 103 additions & 0 deletions tests/WindowsArgsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

use PHPUnit\Framework\TestCase;
use WP_CLI\Runner;

/**
* Test Windows PowerShell argument splitting
*/
class WindowsArgsTest extends TestCase {

/**
* Test that space-separated numeric IDs are split on Windows
*
* @dataProvider provideWindowsArguments
*/
#[PHPUnit\Framework\Attributes\DataProvider( 'provideWindowsArguments' )] // phpcs:ignore PHPCompatibility.Attributes.NewAttributes.PHPUnitAttributeFound
public function testWindowsArgumentSplitting( $is_windows, $input_args, $expected_count, $expected_values ) {
// Set the Windows environment variable
putenv( $is_windows ? 'WP_CLI_TEST_IS_WINDOWS=1' : 'WP_CLI_TEST_IS_WINDOWS=0' );

$reflection = new ReflectionClass( Runner::class );
$method = $reflection->getMethod( 'back_compat_conversions' );
if ( PHP_VERSION_ID < 80100 ) {
// @phpstan-ignore method.deprecated
$method->setAccessible( true );
}

/**
* @var array{0: array<string>, 1: array<string, string>} $result
*/
$result = $method->invoke( null, $input_args, [] );
[ $result_args, $_ ] = $result;

// Verify the results
$this->assertCount( $expected_count, $result_args, 'Unexpected number of arguments' );

foreach ( $expected_values as $index => $expected_value ) {
$this->assertEquals( $expected_value, $result_args[ $index ], "Argument at index $index doesn't match" );
}
}

public static function provideWindowsArguments() {
return [
// is_windows, input_args, expected_count, expected_values
'Windows: space-separated IDs should be split' => [
true,
[ 'post', 'delete', '123 456 789' ],
5,
[ 'post', 'delete', '123', '456', '789' ],
],
'Windows: single ID should not be split' => [
true,
[ 'post', 'delete', '123' ],
3,
[ 'post', 'delete', '123' ],
],
'Windows: non-numeric strings should not be split' => [
true,
[ 'post', 'delete', 'hello world' ],
3,
[ 'post', 'delete', 'hello world' ],
],
'Windows: mixed args (numeric at start)' => [
true,
[ 'post', 'delete', '123 456', 'some-slug' ],
5,
[ 'post', 'delete', '123', '456', 'some-slug' ],
],
'Non-Windows: space-separated IDs should not split' => [
false,
[ 'post', 'delete', '123 456' ],
3,
[ 'post', 'delete', '123 456' ],
],
'Windows: IDs with tabs and spaces' => [
true,
[ 'post', 'delete', "123\t456 789" ],
5,
[ 'post', 'delete', '123', '456', '789' ],
],
'Windows: normal case without leading/trailing spaces' => [
true,
[ 'post', 'delete', '123 456' ],
4,
[ 'post', 'delete', '123', '456' ],
],
'Windows: leading/trailing spaces prevent splitting' => [
true,
[ 'post', 'delete', ' 123 456 ' ],
3,
[ 'post', 'delete', ' 123 456 ' ],
],
];
}

/**
* Cleanup after each test
*/
public function tearDown(): void {
putenv( 'WP_CLI_TEST_IS_WINDOWS' );
parent::tearDown();
}
}
Loading