Some jobs do not decompose into commands: branching on a condition, looping over hundreds of records, tying several steps into one operation. That is what wp eval-file is for.
eval and eval-file
One line goes through wp eval; a file goes through wp eval-file.
wp eval 'echo get_option( "home" );'
wp eval-file bin/report.php
Both run inside a fully loaded WordPress. Plugins are active, hooks are attached, and get_posts() and wp_insert_post() behave exactly as they do on the front end. Not having to write your own bootstrap is the whole value of the command.
Put a guard at the top of the script file.
if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
return;
}
Now if the file ever ends up inside the document root, a web request does nothing with it. For any script that writes data, this is not optional.
The trap: arguments are positional
Everybody hits this once. Anything beginning with -- is interpreted as a flag for WP-CLI itself. It never reaches your script, and an unrecognised one gets the whole command rejected.
# does not work — --dry is consumed by eval-file
wp eval-file bin/seed.php --dry
# works — pass it positionally
wp eval-file bin/seed.php dry s25
Inside the script they arrive in $args.
$args = (array) ( $args ?? [] );
$dry = in_array( 'dry', $args, true );
The restriction is more useful than it first appears, because it keeps a script’s interface down to a couple of short words. Once you want more than three options, the job has outgrown a script and it is time to promote it to a proper command — the subject of part six.
WordPress in the shell is not WordPress on the web
Same WordPress, different context. Three differences cause most of the surprises.
User 0 reaches furthest. With nobody logged in every current_user_can() is false, and the absence of unfiltered_html in particular means kses filters your content on save. The next part shows exactly what that looks like when it happens.
Hook timing differs too. eval-file executes after WordPress has finished loading, so early hooks such as init are long gone. People add add_action( 'init', … ) inside a script and then hunt for why it never fires; it is late rather than wrong, and the answer is simply to call the code directly.
The script that dies silently on an ABSPATH guard
Most WordPress files open with this.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
require such a file from a plain PHP script that never bootstrapped WordPress and the line executes as written, ending the run with zero bytes of output and exit code 0. No error, no warning — so you start by suspecting your own logic, and it takes a long while to find.
There are two clean responses. If you need WordPress, run through eval-file. If you do not, write a self-contained script that includes no WordPress files at all. The second is especially valuable in CI, where it finishes in seconds without a database or an install — we come back to it in part eight.
Examples of the kind of work worth scripting live in the development workflow archive, and the diagnostic plugins we built the same way are on the free tools page.
Next part
Once you write scripts, you will soon run the same script twice. Next up: idempotent seeds — making a second run create nothing new — and the several quiet traps hiding in that.