Skip to content

Commit 688d397

Browse files
committed
Merge branch 'main' into copilot/add-suppress-output-option
2 parents 36e50f9 + 393bb10 commit 688d397

File tree

7 files changed

+419
-20
lines changed

7 files changed

+419
-20
lines changed

.gitattributes

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/.actrc export-ignore
2+
/.distignore export-ignore
3+
/.editorconfig export-ignore
4+
/.github export-ignore
5+
/.gitignore export-ignore
6+
/.typos.toml export-ignore
7+
/AGENTS.md export-ignore
8+
/behat.yml export-ignore
9+
/features export-ignore
10+
/phpcs.xml.dist export-ignore
11+
/phpstan.neon.dist export-ignore
12+
/phpunit.xml.dist export-ignore
13+
/tests export-ignore
14+
/wp-cli.yml export-ignore

.github/workflows/copilot-setup-steps.yml

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,11 @@ jobs:
2121

2222
- name: Check existence of composer.json file
2323
id: check_composer_file
24-
uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3
25-
with:
26-
files: "composer.json"
24+
run: echo "files_exists=$(test -f composer.json && echo true || echo false)" >> "$GITHUB_OUTPUT"
2725

2826
- name: Set up PHP environment
2927
if: steps.check_composer_file.outputs.files_exists == 'true'
30-
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2
28+
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2
3129
with:
3230
php-version: 'latest'
3331
ini-values: zend.assertions=1, error_reporting=-1, display_errors=On
@@ -38,7 +36,7 @@ jobs:
3836

3937
- name: Install Composer dependencies & cache dependencies
4038
if: steps.check_composer_file.outputs.files_exists == 'true'
41-
uses: ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520 # v3
39+
uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # v3
4240
env:
4341
COMPOSER_ROOT_VERSION: dev-${{ github.event.repository.default_branch }}
4442
with:

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Quick links: [Using](#using) | [Installing](#installing) | [Contributing](#contr
1010
## Using
1111

1212
~~~
13-
wp shell [--basic] [--hook=<hook>]
13+
wp shell [--basic] [--watch=<path>] [--hook=<hook>]
1414
~~~
1515

1616
`wp shell` allows you to evaluate PHP statements and expressions
@@ -19,12 +19,21 @@ hit enter, and see the code execute right before you. Because WordPress
1919
is loaded, you have access to all the functions, classes and globals
2020
that you can use within a WordPress plugin, for example.
2121

22+
The `restart` command reloads the shell by spawning a new PHP process,
23+
allowing modified code to be fully reloaded. Note that this requires
24+
the `pcntl_exec()` function. If not available, the shell restarts
25+
in-process, which resets variables but doesn't reload PHP files.
26+
2227
**OPTIONS**
2328

2429
[--basic]
2530
Force the use of WP-CLI's built-in PHP REPL, even if the Boris or
2631
PsySH PHP REPLs are available.
2732

33+
[--watch=<path>]
34+
Watch a file or directory for changes and automatically restart the shell.
35+
Only works with the built-in REPL (--basic).
36+
2837
[--hook=<hook>]
2938
Ensure that a specific WordPress action hook has fired before starting the shell.
3039
This validates that the preconditions associated with that hook are met.
@@ -40,6 +49,18 @@ that you can use within a WordPress plugin, for example.
4049
wp> get_bloginfo( 'name' );
4150
=> string(6) "WP-CLI"
4251

52+
# Restart the shell to reload code changes.
53+
$ wp shell
54+
wp> restart
55+
Restarting shell in new process...
56+
wp>
57+
58+
# Watch a directory for changes and auto-restart.
59+
$ wp shell --watch=wp-content/plugins/my-plugin
60+
wp> // Make changes to files in the plugin directory
61+
Detected changes in wp-content/plugins/my-plugin, restarting shell...
62+
wp>
63+
4364
# Start a shell, ensuring the 'init' hook has already fired.
4465
$ wp shell --hook=init
4566

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
}
1313
],
1414
"require": {
15-
"wp-cli/wp-cli": "^2.12"
15+
"wp-cli/wp-cli": "^2.13"
1616
},
1717
"require-dev": {
1818
"wp-cli/wp-cli-tests": "^5"

features/shell.feature

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,75 @@ Feature: WordPress REPL
6161
"""
6262
And STDERR should be empty
6363
64+
Scenario: Restart shell
65+
Given a WP install
66+
And a session file:
67+
"""
68+
$a = 1;
69+
restart
70+
$b = 2;
71+
"""
72+
73+
When I run `wp shell --basic < session`
74+
Then STDOUT should contain:
75+
"""
76+
Restarting shell...
77+
"""
78+
And STDOUT should contain:
79+
"""
80+
=> int(2)
81+
"""
82+
83+
Scenario: Exit shell
84+
Given a WP install
85+
And a session file:
86+
"""
87+
$a = 1;
88+
exit
89+
"""
90+
91+
When I run `wp shell --basic < session`
92+
Then STDOUT should contain:
93+
"""
94+
=> int(1)
95+
"""
96+
And STDOUT should not contain:
97+
"""
98+
exit
99+
"""
100+
101+
Scenario: Use SHELL environment variable as fallback for bash
102+
Given a WP install
103+
104+
And a session file:
105+
"""
106+
return true;
107+
"""
108+
109+
# SHELL pointing to bash should work (when bash is available).
110+
When I try `SHELL=/bin/bash wp shell --basic < session`
111+
Then STDOUT should contain:
112+
"""
113+
bool(true)
114+
"""
115+
And STDERR should be empty
116+
117+
# SHELL pointing to non-bash binary should be ignored and fall back to /bin/bash.
118+
When I try `SHELL=/bin/sh wp shell --basic < session`
119+
Then STDOUT should contain:
120+
"""
121+
bool(true)
122+
"""
123+
And STDERR should be empty
124+
125+
# SHELL pointing to invalid path should be ignored and fall back to /bin/bash.
126+
When I try `SHELL=/nonsense/path wp shell --basic < session`
127+
Then STDOUT should contain:
128+
"""
129+
bool(true)
130+
"""
131+
And STDERR should be empty
132+
64133
Scenario: Input starting with dash
65134
Given a WP install
66135
And a session file:

src/Shell_Command.php

Lines changed: 149 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,21 @@ class Shell_Command extends WP_CLI_Command {
1313
* is loaded, you have access to all the functions, classes and globals
1414
* that you can use within a WordPress plugin, for example.
1515
*
16+
* The `restart` command reloads the shell by spawning a new PHP process,
17+
* allowing modified code to be fully reloaded. Note that this requires
18+
* the `pcntl_exec()` function. If not available, the shell restarts
19+
* in-process, which resets variables but doesn't reload PHP files.
20+
*
1621
* ## OPTIONS
1722
*
1823
* [--basic]
1924
* : Force the use of WP-CLI's built-in PHP REPL, even if the Boris or
2025
* PsySH PHP REPLs are available.
2126
*
27+
* [--watch=<path>]
28+
* : Watch a file or directory for changes and automatically restart the shell.
29+
* Only works with the built-in REPL (--basic).
30+
*
2231
* [--hook=<hook>]
2332
* : Ensure that a specific WordPress action hook has fired before starting the shell.
2433
* This validates that the preconditions associated with that hook are met.
@@ -34,15 +43,37 @@ class Shell_Command extends WP_CLI_Command {
3443
* wp> get_bloginfo( 'name' );
3544
* => string(6) "WP-CLI"
3645
*
46+
* # Restart the shell to reload code changes.
47+
* $ wp shell
48+
* wp> restart
49+
* Restarting shell in new process...
50+
* wp>
51+
*
52+
* # Watch a directory for changes and auto-restart.
53+
* $ wp shell --watch=wp-content/plugins/my-plugin
54+
* wp> // Make changes to files in the plugin directory
55+
* Detected changes in wp-content/plugins/my-plugin, restarting shell...
56+
* wp>
57+
*
3758
* # Start a shell, ensuring the 'init' hook has already fired.
3859
* $ wp shell --hook=init
3960
*
4061
* # Start a shell in quiet mode, suppressing return value output.
4162
* $ wp shell --quiet
4263
* wp> $a = "hello";
4364
* wp>
65+
*
66+
* @param string[] $_ Positional arguments. Unused.
67+
* @param array{basic?: bool, watch?: string} $assoc_args Associative arguments.
4468
*/
4569
public function __invoke( $_, $assoc_args ) {
70+
$watch_path = Utils\get_flag_value( $assoc_args, 'watch', false );
71+
72+
if ( $watch_path && ! Utils\get_flag_value( $assoc_args, 'basic' ) ) {
73+
WP_CLI::warning( 'The --watch option only works with the built-in REPL. Enabling --basic mode.' );
74+
$assoc_args['basic'] = true;
75+
}
76+
4677
$hook = Utils\get_flag_value( $assoc_args, 'hook', '' );
4778

4879
// No hook specified, start immediately.
@@ -72,9 +103,16 @@ public function __invoke( $_, $assoc_args ) {
72103
/**
73104
* Start the shell REPL.
74105
*
75-
* @param array<string,bool|string> $assoc_args Associative arguments.
106+
* @param array{basic?: bool, watch?: string} $assoc_args Associative arguments.
76107
*/
77108
private function start_shell( $assoc_args ) {
109+
$watch_path = Utils\get_flag_value( $assoc_args, 'watch', '' );
110+
111+
if ( $watch_path && ! Utils\get_flag_value( $assoc_args, 'basic' ) ) {
112+
WP_CLI::warning( 'The --watch option only works with the built-in REPL. Enabling --basic mode.' );
113+
$assoc_args['basic'] = true;
114+
}
115+
78116
$class = WP_CLI\Shell\REPL::class;
79117
$quiet = (bool) WP_CLI::get_config( 'quiet' );
80118

@@ -107,8 +145,116 @@ private function start_shell( $assoc_args ) {
107145
/**
108146
* @var class-string<WP_CLI\Shell\REPL> $class
109147
*/
110-
$repl = new $class( 'wp> ', $quiet );
111-
$repl->start();
148+
if ( $watch_path ) {
149+
$watch_path = $this->resolve_watch_path( $watch_path );
150+
}
151+
152+
do {
153+
$repl = new $class( 'wp> ', $quiet );
154+
if ( $watch_path ) {
155+
$repl->set_watch_path( $watch_path );
156+
}
157+
$exit_code = $repl->start();
158+
159+
// If restart requested, exec a new PHP process to reload all code
160+
if ( WP_CLI\Shell\REPL::EXIT_CODE_RESTART === $exit_code ) {
161+
$this->restart_process( $assoc_args );
162+
// If restart_process() returns, pcntl_exec is not available, continue in-process
163+
}
164+
} while ( WP_CLI\Shell\REPL::EXIT_CODE_RESTART === $exit_code );
165+
}
166+
}
167+
168+
/**
169+
* Resolve and validate the watch path.
170+
*
171+
* @param string $path Path to watch.
172+
* @return string|never Absolute path to watch.
173+
*/
174+
private function resolve_watch_path( $path ) {
175+
if ( ! file_exists( $path ) ) {
176+
WP_CLI::error( "Watch path does not exist: {$path}" );
177+
}
178+
179+
$realpath = realpath( $path );
180+
if ( false === $realpath ) {
181+
WP_CLI::error( "Could not resolve watch path: {$path}" );
182+
}
183+
184+
return $realpath;
185+
}
186+
187+
/**
188+
* Restart the shell by spawning a new PHP process.
189+
*
190+
* This replaces the current process with a new one to fully reload all code.
191+
* Falls back to in-process restart if pcntl_exec is not available.
192+
*
193+
* @param array{basic?: bool, watch?: string} $assoc_args Command arguments to preserve.
194+
*/
195+
private function restart_process( $assoc_args ) {
196+
/**
197+
* @var array{0?: string} $argv
198+
*/
199+
global $argv;
200+
201+
// Check if pcntl_exec is available
202+
if ( ! function_exists( 'pcntl_exec' ) ) {
203+
WP_CLI::debug( 'pcntl_exec not available, falling back to in-process restart', 'shell' );
204+
return;
205+
}
206+
207+
// Build the command to restart wp shell with the same arguments
208+
$php_binary = Utils\get_php_binary();
209+
210+
/**
211+
* @var array{argv: array{0?: string}} $_SERVER
212+
*/
213+
214+
// Get the WP-CLI script path
215+
$wp_cli_script = null;
216+
if ( isset( $argv[0] ) ) {
217+
$wp_cli_script = $argv[0];
218+
} elseif ( isset( $_SERVER['argv'][0] ) ) {
219+
$wp_cli_script = $_SERVER['argv'][0];
112220
}
221+
222+
if ( ! $wp_cli_script ) {
223+
WP_CLI::debug( 'Could not determine WP-CLI script path, falling back to in-process restart', 'shell' );
224+
return;
225+
}
226+
227+
// Build arguments array
228+
$args = array( $php_binary, $wp_cli_script, 'shell' );
229+
230+
if ( Utils\get_flag_value( $assoc_args, 'basic' ) ) {
231+
$args[] = '--basic';
232+
}
233+
234+
$watch_path = Utils\get_flag_value( $assoc_args, 'watch', false );
235+
if ( $watch_path ) {
236+
$args[] = '--watch=' . $watch_path;
237+
}
238+
239+
// Add global config values to preserve the environment after restart
240+
$config = WP_CLI::get_runner()->config;
241+
if ( isset( $config['path'] ) ) {
242+
$args[] = '--path=' . $config['path'];
243+
}
244+
if ( isset( $config['user'] ) ) {
245+
$args[] = '--user=' . $config['user'];
246+
}
247+
if ( isset( $config['url'] ) ) {
248+
$args[] = '--url=' . $config['url'];
249+
}
250+
251+
WP_CLI::log( 'Restarting shell in new process...' );
252+
253+
// Replace the current process with a new one
254+
// Note: pcntl_exec does not return on success
255+
pcntl_exec( $php_binary, array_slice( $args, 1 ) );
256+
257+
// If we reach here, exec failed
258+
WP_CLI::warning( 'Failed to restart process, falling back to in-process restart' );
113259
}
114260
}

0 commit comments

Comments
 (0)