Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions features/shell.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -39,6 +43,7 @@ Feature: WordPress REPL
bool(true)
"""

@skip-windows
Scenario: Use custom shell path
Given a WP install

Expand All @@ -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:
"""
Expand Down Expand Up @@ -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
Expand Down
51 changes: 42 additions & 9 deletions src/WP_CLI/Shell/REPL.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand All @@ -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';
Expand All @@ -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." );
}
Comment on lines +200 to 207
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The new PowerShell implementation provides basic REPL functionality, but it's missing command history support, which is a crucial feature for an interactive shell. The use of Read-Host with the -NoProfile switch on powershell.exe prevents the use of PSReadLine for history. This is a significant usability regression compared to the bash/ksh implementation which explicitly handles a history file.

Please consider implementing history support for PowerShell. This could involve:

  • Manually managing a history file with PowerShell commands (e.g., Get-Content/Add-Content).
  • Interacting with the PSReadLine module's history functions if it's available.

Without history, the shell is much less productive.


$prompt = escapeshellarg( $prompt );
$history_path = escapeshellarg( $history_path );
Comment thread
swissspidy marked this conversation as resolved.

$is_ksh = self::is_ksh_shell( $shell_binary );
$shell_binary = escapeshellarg( $shell_binary );

Expand Down Expand Up @@ -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;
}
}