diff --git a/features/shell.feature b/features/shell.feature index dfb63c44..2272719e 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 @@ -39,6 +43,7 @@ Feature: WordPress REPL bool(true) """ + @skip-windows Scenario: Use custom shell path Given a WP install @@ -47,7 +52,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: """ @@ -252,7 +257,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 diff --git a/src/WP_CLI/Shell/REPL.php b/src/WP_CLI/Shell/REPL.php index 5dcff93b..63e4d5bc 100644 --- a/src/WP_CLI/Shell/REPL.php +++ b/src/WP_CLI/Shell/REPL.php @@ -144,19 +144,21 @@ 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 ( \WP_CLI\Utils\is_windows() && ! self::is_tty() ) { + $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 ); @@ -176,10 +178,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 +195,24 @@ private static function create_prompt_cmd( $prompt, $history_path ) { $shell_binary = 'bash'; } + $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 ); + $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 ) ) { 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 ); + $is_ksh = self::is_ksh_shell( $shell_binary ); $shell_binary = escapeshellarg( $shell_binary ); @@ -331,4 +349,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; + } }