Skip to content

Commit ebe2608

Browse files
swissspidyCopilot
andauthored
Windows: improve PowerShell support (#93)
Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 303277f commit ebe2608

2 files changed

Lines changed: 57 additions & 13 deletions

File tree

features/shell.feature

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@ Feature: WordPress REPL
33
Scenario: Blank session
44
Given a WP install
55

6-
When I run `wp shell < /dev/null`
7-
And I run `wp shell --basic < /dev/null`
6+
And an empty_session file:
7+
"""
8+
"""
9+
10+
When I run `wp shell < empty_session`
11+
And I run `wp shell --basic < empty_session`
812
Then STDOUT should be empty
913
1014
Scenario: Persistent environment
@@ -39,6 +43,7 @@ Feature: WordPress REPL
3943
bool(true)
4044
"""
4145
46+
@skip-windows
4247
Scenario: Use custom shell path
4348
Given a WP install
4449
@@ -47,7 +52,7 @@ Feature: WordPress REPL
4752
return true;
4853
"""
4954
50-
When I try `WP_CLI_CUSTOM_SHELL=/nonsense/path wp shell --basic < session`
55+
When I try `MSYS_NO_PATHCONV=1 WP_CLI_CUSTOM_SHELL=/nonsense/path wp shell --basic < session`
5156
Then STDOUT should be empty
5257
And STDERR should contain:
5358
"""
@@ -252,7 +257,11 @@ Feature: WordPress REPL
252257
Scenario: Shell with hook parameter for hook that hasn't fired
253258
Given a WP install
254259
255-
When I try `wp shell --basic --hook=shutdown < /dev/null`
260+
And an empty_session file:
261+
"""
262+
"""
263+
264+
When I try `wp shell --basic --hook=shutdown < empty_session`
256265
Then STDERR should contain:
257266
"""
258267
Error: The 'shutdown' hook has not fired yet

src/WP_CLI/Shell/REPL.php

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -144,19 +144,21 @@ private function prompt() {
144144
// @phpstan-ignore booleanNot.alwaysTrue
145145
$prompt = ( ! $done && false !== $full_line ) ? '--> ' : $this->prompt;
146146

147-
$fp = popen( self::create_prompt_cmd( $prompt, $this->history_file ), 'r' );
148-
149-
$line = $fp ? fgets( $fp ) : '';
150-
151-
if ( $fp ) {
152-
pclose( $fp );
147+
if ( \WP_CLI\Utils\is_windows() && ! self::is_tty() ) {
148+
$line = fgets( STDIN );
149+
} else {
150+
$fp = popen( self::create_prompt_cmd( $prompt, $this->history_file ), 'r' );
151+
$line = $fp ? fgets( $fp ) : '';
152+
if ( $fp ) {
153+
pclose( $fp );
154+
}
153155
}
154156

155157
if ( ! $line ) {
156158
break;
157159
}
158160

159-
$line = rtrim( $line, "\n" );
161+
$line = rtrim( $line, "\r\n" );
160162

161163
if ( $line && '\\' === $line[ strlen( $line ) - 1 ] ) {
162164
$line = substr( $line, 0, -1 );
@@ -176,10 +178,12 @@ private function prompt() {
176178
}
177179

178180
private static function create_prompt_cmd( $prompt, $history_path ) {
179-
$prompt = escapeshellarg( $prompt );
180-
$history_path = escapeshellarg( $history_path );
181+
$is_windows = \WP_CLI\Utils\is_windows();
182+
181183
if ( getenv( 'WP_CLI_CUSTOM_SHELL' ) ) {
182184
$shell_binary = (string) getenv( 'WP_CLI_CUSTOM_SHELL' );
185+
} elseif ( $is_windows ) {
186+
$shell_binary = 'powershell.exe';
183187
} elseif ( is_file( '/bin/bash' ) && is_readable( '/bin/bash' ) ) {
184188
// Prefer /bin/bash when available since we use bash-specific commands.
185189
$shell_binary = '/bin/bash';
@@ -191,10 +195,26 @@ private static function create_prompt_cmd( $prompt, $history_path ) {
191195
$shell_binary = 'bash';
192196
}
193197

198+
$shell_basename = strtolower( basename( $shell_binary ) );
199+
$is_powershell = $is_windows && in_array( $shell_basename, array( 'powershell.exe', 'pwsh.exe' ), true );
200+
201+
if ( $is_powershell ) {
202+
// PowerShell uses ` (backtick) for escaping but for strings single quotes are literal.
203+
// If prompt contains single quotes, we double them in PowerShell.
204+
$prompt_for_ps = str_replace( "'", "''", $prompt );
205+
$history_path_for_ps = str_replace( "'", "''", $history_path );
206+
$cmd = "\$line = Read-Host -Prompt '{$prompt_for_ps}'; if ( \$line ) { Add-Content -Path '{$history_path_for_ps}' -Value \$line; } Write-Output \$line;";
207+
$shell_quoted = escapeshellarg( $shell_binary );
208+
return "{$shell_quoted} -Command \"{$cmd}\"";
209+
}
210+
194211
if ( ! is_file( $shell_binary ) || ! is_readable( $shell_binary ) ) {
195212
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." );
196213
}
197214

215+
$prompt = escapeshellarg( $prompt );
216+
$history_path = escapeshellarg( $history_path );
217+
198218
$is_ksh = self::is_ksh_shell( $shell_binary );
199219
$shell_binary = escapeshellarg( $shell_binary );
200220

@@ -331,4 +351,19 @@ private function get_recursive_mtime( $path ) {
331351

332352
return $mtime;
333353
}
354+
355+
/**
356+
* Detect if STDIN is an interactive terminal.
357+
*
358+
* @return bool True if interactive, false otherwise.
359+
*/
360+
private static function is_tty() {
361+
if ( function_exists( 'stream_isatty' ) ) {
362+
return stream_isatty( STDIN );
363+
}
364+
if ( function_exists( 'posix_isatty' ) ) {
365+
return posix_isatty( STDIN );
366+
}
367+
return true;
368+
}
334369
}

0 commit comments

Comments
 (0)