From f97d982d341ba9608714dc4db2fd263826480fb4 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 29 Mar 2026 00:01:32 +0100 Subject: [PATCH 1/8] Try support PowerShell --- src/WP_CLI/Shell/REPL.php | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/WP_CLI/Shell/REPL.php b/src/WP_CLI/Shell/REPL.php index 5dcff93b..abc9162e 100644 --- a/src/WP_CLI/Shell/REPL.php +++ b/src/WP_CLI/Shell/REPL.php @@ -176,10 +176,12 @@ private function prompt() { } private static function create_prompt_cmd( $prompt, $history_path ) { - $prompt = escapeshellarg( $prompt ); - $history_path = escapeshellarg( $history_path ); + $is_windows = \WP_CLI\Utils\is_windows(); + if ( getenv( 'WP_CLI_CUSTOM_SHELL' ) ) { $shell_binary = (string) getenv( 'WP_CLI_CUSTOM_SHELL' ); + } elseif ( $is_windows ) { + $shell_binary = 'powershell.exe'; } elseif ( is_file( '/bin/bash' ) && is_readable( '/bin/bash' ) ) { // Prefer /bin/bash when available since we use bash-specific commands. $shell_binary = '/bin/bash'; @@ -191,10 +193,19 @@ private static function create_prompt_cmd( $prompt, $history_path ) { $shell_binary = 'bash'; } - if ( ! is_file( $shell_binary ) || ! is_readable( $shell_binary ) ) { - WP_CLI::error( "The shell binary '{$shell_binary}' is not valid. You can override the shell to be used through the WP_CLI_CUSTOM_SHELL environment variable." ); + $is_powershell = $is_windows && 'powershell.exe' === $shell_binary; + + if ( $is_powershell ) { + // PowerShell uses ` (backtick) for escaping but for strings single quotes are literal. + // If prompt contains single quotes, we double them in PowerShell. + $prompt_for_ps = str_replace( "'", "''", $prompt ); + $cmd = "\$line = Read-Host -Prompt '{$prompt_for_ps}'; Write-Output \$line;"; + return "powershell.exe -NoProfile -Command \"{$cmd}\""; } + $prompt = escapeshellarg( $prompt ); + $history_path = escapeshellarg( $history_path ); + $is_ksh = self::is_ksh_shell( $shell_binary ); $shell_binary = escapeshellarg( $shell_binary ); From 1801a737ba7fbeccced12c1425e4e3dbc74473b7 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 29 Mar 2026 00:03:26 +0100 Subject: [PATCH 2/8] Lint fix --- src/WP_CLI/Shell/REPL.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WP_CLI/Shell/REPL.php b/src/WP_CLI/Shell/REPL.php index abc9162e..998baa40 100644 --- a/src/WP_CLI/Shell/REPL.php +++ b/src/WP_CLI/Shell/REPL.php @@ -199,7 +199,7 @@ private static function create_prompt_cmd( $prompt, $history_path ) { // PowerShell uses ` (backtick) for escaping but for strings single quotes are literal. // If prompt contains single quotes, we double them in PowerShell. $prompt_for_ps = str_replace( "'", "''", $prompt ); - $cmd = "\$line = Read-Host -Prompt '{$prompt_for_ps}'; Write-Output \$line;"; + $cmd = "\$line = Read-Host -Prompt '{$prompt_for_ps}'; Write-Output \$line;"; return "powershell.exe -NoProfile -Command \"{$cmd}\""; } From 4a21ed1b2be0d90e1306e0009b28a0bea5e89118 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 29 Mar 2026 12:19:13 +0200 Subject: [PATCH 3/8] Update src/WP_CLI/Shell/REPL.php --- src/WP_CLI/Shell/REPL.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/WP_CLI/Shell/REPL.php b/src/WP_CLI/Shell/REPL.php index 998baa40..9ecba756 100644 --- a/src/WP_CLI/Shell/REPL.php +++ b/src/WP_CLI/Shell/REPL.php @@ -203,6 +203,10 @@ private static function create_prompt_cmd( $prompt, $history_path ) { return "powershell.exe -NoProfile -Command \"{$cmd}\""; } + if ( ! is_file( $shell_binary ) || ! is_readable( $shell_binary ) ) { + WP_CLI::error( "The shell binary '{$shell_binary}' is not valid. You can override the shell to be used through the WP_CLI_CUSTOM_SHELL environment variable." ); + } + $prompt = escapeshellarg( $prompt ); $history_path = escapeshellarg( $history_path ); From 23a8ee6a1b547c7f20548e0eeb035efd99003837 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 29 Mar 2026 13:03:49 +0200 Subject: [PATCH 4/8] make history --- src/WP_CLI/Shell/REPL.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/WP_CLI/Shell/REPL.php b/src/WP_CLI/Shell/REPL.php index 9ecba756..69bf04b5 100644 --- a/src/WP_CLI/Shell/REPL.php +++ b/src/WP_CLI/Shell/REPL.php @@ -198,9 +198,10 @@ private static function create_prompt_cmd( $prompt, $history_path ) { if ( $is_powershell ) { // PowerShell uses ` (backtick) for escaping but for strings single quotes are literal. // If prompt contains single quotes, we double them in PowerShell. - $prompt_for_ps = str_replace( "'", "''", $prompt ); - $cmd = "\$line = Read-Host -Prompt '{$prompt_for_ps}'; Write-Output \$line;"; - return "powershell.exe -NoProfile -Command \"{$cmd}\""; + $prompt_for_ps = str_replace( "'", "''", $prompt ); + $history_path_for_ps = str_replace( "'", "''", $history_path ); + $cmd = "\$line = Read-Host -Prompt '{$prompt_for_ps}'; if ( \$line ) { Add-Content -Path '{$history_path_for_ps}' -Value \$line; } Write-Output \$line;"; + return "powershell.exe -Command \"{$cmd}\""; } if ( ! is_file( $shell_binary ) || ! is_readable( $shell_binary ) ) { From c0c0bc6fe122d829e9a8a5b729a1662923f78d10 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 29 Mar 2026 14:01:33 +0200 Subject: [PATCH 5/8] no /dev/null --- features/shell.feature | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/features/shell.feature b/features/shell.feature index dfb63c44..a60c17d3 100644 --- a/features/shell.feature +++ b/features/shell.feature @@ -3,8 +3,12 @@ Feature: WordPress REPL Scenario: Blank session Given a WP install - When I run `wp shell < /dev/null` - And I run `wp shell --basic < /dev/null` + And an empty_session file: + """ + """ + + When I run `wp shell < empty_session` + And I run `wp shell --basic < empty_session` Then STDOUT should be empty Scenario: Persistent environment @@ -252,7 +256,11 @@ Feature: WordPress REPL Scenario: Shell with hook parameter for hook that hasn't fired Given a WP install - When I try `wp shell --basic --hook=shutdown < /dev/null` + And an empty_session file: + """ + """ + + When I try `wp shell --basic --hook=shutdown < empty_session` Then STDERR should contain: """ Error: The 'shutdown' hook has not fired yet From 892871cff175279bfa3594b0f0663e3b53c580bb Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 17 Apr 2026 10:30:08 +0200 Subject: [PATCH 6/8] tty check --- src/WP_CLI/Shell/REPL.php | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/WP_CLI/Shell/REPL.php b/src/WP_CLI/Shell/REPL.php index 69bf04b5..fa7e5e4e 100644 --- a/src/WP_CLI/Shell/REPL.php +++ b/src/WP_CLI/Shell/REPL.php @@ -144,19 +144,24 @@ private function prompt() { // @phpstan-ignore booleanNot.alwaysTrue $prompt = ( ! $done && false !== $full_line ) ? '--> ' : $this->prompt; - $fp = popen( self::create_prompt_cmd( $prompt, $this->history_file ), 'r' ); - - $line = $fp ? fgets( $fp ) : ''; - - if ( $fp ) { - pclose( $fp ); + if ( ! self::is_tty() ) { + if ( getenv( 'WP_CLI_CUSTOM_SHELL' ) ) { + self::create_prompt_cmd( $prompt, $this->history_file ); + } + $line = fgets( STDIN ); + } else { + $fp = popen( self::create_prompt_cmd( $prompt, $this->history_file ), 'r' ); + $line = $fp ? fgets( $fp ) : ''; + if ( $fp ) { + pclose( $fp ); + } } if ( ! $line ) { break; } - $line = rtrim( $line, "\n" ); + $line = rtrim( $line, "\r\n" ); if ( $line && '\\' === $line[ strlen( $line ) - 1 ] ) { $line = substr( $line, 0, -1 ); @@ -347,4 +352,19 @@ private function get_recursive_mtime( $path ) { return $mtime; } + + /** + * Detect if STDIN is an interactive terminal. + * + * @return bool True if interactive, false otherwise. + */ + private static function is_tty() { + if ( function_exists( 'stream_isatty' ) ) { + return stream_isatty( STDIN ); + } + if ( function_exists( 'posix_isatty' ) ) { + return posix_isatty( STDIN ); + } + return true; + } } From 343061f81a80e9a0adb2d8f2bfe34de2072c72c2 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 17 Apr 2026 11:12:42 +0200 Subject: [PATCH 7/8] fixes --- features/shell.feature | 2 +- src/WP_CLI/Shell/REPL.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/shell.feature b/features/shell.feature index a60c17d3..be8be6ae 100644 --- a/features/shell.feature +++ b/features/shell.feature @@ -51,7 +51,7 @@ Feature: WordPress REPL return true; """ - When I try `WP_CLI_CUSTOM_SHELL=/nonsense/path wp shell --basic < session` + When I try `MSYS_NO_PATHCONV=1 WP_CLI_CUSTOM_SHELL=/nonsense/path wp shell --basic < session` Then STDOUT should be empty And STDERR should contain: """ diff --git a/src/WP_CLI/Shell/REPL.php b/src/WP_CLI/Shell/REPL.php index fa7e5e4e..f86a6d4d 100644 --- a/src/WP_CLI/Shell/REPL.php +++ b/src/WP_CLI/Shell/REPL.php @@ -144,7 +144,7 @@ private function prompt() { // @phpstan-ignore booleanNot.alwaysTrue $prompt = ( ! $done && false !== $full_line ) ? '--> ' : $this->prompt; - if ( ! self::is_tty() ) { + if ( \WP_CLI\Utils\is_windows() && ! self::is_tty() ) { if ( getenv( 'WP_CLI_CUSTOM_SHELL' ) ) { self::create_prompt_cmd( $prompt, $this->history_file ); } From 0a4a9baa6806b0bd13046b2891d787332878e33c Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 17 Apr 2026 23:07:01 +0200 Subject: [PATCH 8/8] fixes --- features/shell.feature | 1 + src/WP_CLI/Shell/REPL.php | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/features/shell.feature b/features/shell.feature index be8be6ae..2272719e 100644 --- a/features/shell.feature +++ b/features/shell.feature @@ -43,6 +43,7 @@ Feature: WordPress REPL bool(true) """ + @skip-windows Scenario: Use custom shell path Given a WP install diff --git a/src/WP_CLI/Shell/REPL.php b/src/WP_CLI/Shell/REPL.php index f86a6d4d..63e4d5bc 100644 --- a/src/WP_CLI/Shell/REPL.php +++ b/src/WP_CLI/Shell/REPL.php @@ -145,9 +145,6 @@ private function prompt() { $prompt = ( ! $done && false !== $full_line ) ? '--> ' : $this->prompt; if ( \WP_CLI\Utils\is_windows() && ! self::is_tty() ) { - if ( getenv( 'WP_CLI_CUSTOM_SHELL' ) ) { - self::create_prompt_cmd( $prompt, $this->history_file ); - } $line = fgets( STDIN ); } else { $fp = popen( self::create_prompt_cmd( $prompt, $this->history_file ), 'r' );