Running scripts on the command line

The PHP CLI (command-line interface) is the php program you run in a terminal. It executes a PHP file from top to bottom and exits, just like python3 or node. It is the same language as on a web server, with a few differences that suit scripts: there is no time limit, output goes to the terminal, and nothing is sent as HTTP headers.

Like every command, php average.php looks for the file in the terminal's working directory, so first go to the folder that contains it with cd. From the wrong folder PHP answers Could not open input file: average.php.

cd ~/projects/php         # the folder that contains average.php
php average.php           # run a script
php --version             # which PHP version is this?
php -r 'echo 7 * 6, PHP_EOL;'   # run one line of code without a file
Days measured: 5
Average per day: 11.4 kWh
PHP 8.4.26 (cli) (built: …) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.4.26, Copyright (c) Zend Technologies
    with Zend OPcache v8.4.26, Copyright (c), by Zend Technologies
42

If the command is not found, PHP is not installed, or not on the PATH. On Debian and Ubuntu the package is php-cli (or a versioned one such as php8.4-cli); see the apt workflow. On Windows and macOS, the PHP downloads page and package managers such as winget and Homebrew provide it.

Arguments and exit codes

Words typed after the script name arrive in the array $argv; $argv[0] is the script name itself and $argc is the number of entries. A script reports success or failure to the shell through its exit status: exit(0) (or reaching the end) means success, any other number means failure. Error messages belong on standard error, written with fwrite(STDERR, …), so they do not mix with the real output.

<?php
// PHP fills $argv itself; here we set it to what this command would give:
//   php usage.php 2026-10 north
$argv = ['usage.php', '2026-10', 'north'];
$argc = count($argv);

if ($argc !== 3) {
    fwrite(STDERR, "Usage: php usage.php MONTH REGION\n");
    exit(2);
}
[, $month, $region] = $argv;
echo "Usage report for $region, $month\n";
Usage report for north, 2026-10

Scripts can also read standard input, so they fit into pipelines. STDIN is an open file handle; fgets() reads one line and returns false at the end:

<?php
// Reads one meter reading per line until the input ends
$total = 0.0;
$count = 0;
while (($line = fgets(STDIN)) !== false) {
    $total += (float) trim($line);
    $count++;
}
printf("%d readings, total %.1f kWh\n", $count, $total);
3 readings, total 33.5 kWh
cut -d';' -f3 readings.csv | php total.php

On Linux a script can start with a shebang line, #!/usr/bin/env php, above <?php; after chmod +x you can run it as ./total.php.

Useful command-line options

Command What it does
php -v / php --version Show the version
php -l file.php Check the syntax (lint) without running the file
php -r 'code' Run code given on the command line, without the tags
php -a Start the interactive shell
php -S host:port Start the built-in development web server
php --ini Show which php.ini and extra .ini files are loaded
php -i Print all configuration (the command-line version of phpinfo())
php -m List the loaded extensions
php -d name=value Override one php.ini setting for this run
php -n Run without any php.ini (built-in defaults only)

Checking syntax with php -l

php -l ("lint") parses a file and reports syntax errors without running any of it, so it is safe on code that would send emails or change a database. Run it before you upload a file that was edited by hand: a single missing semicolon makes every page that includes the file fail.

php -l public/index.php
php -l bad.php; echo "exit status: $?"
No syntax errors detected in public/index.php
PHP Parse error:  syntax error, unexpected token "echo", expecting "," or ";" in bad.php on line 3
Errors parsing bad.php
exit status: 255

The line number points to where PHP noticed the problem; the mistake (here a missing ;) is often at the end of the line before. Since PHP 8.3, php -l accepts several files at once; on older versions, lint one file per command. The site's code examples are checked the same way.

The interactive shell

The PHP interactive shell, started with php -a, reads one statement at a time, runs it and waits for the next. It is handy for trying a function or checking how PHP treats a value. Two differences from Python's REPL and the Node.js console: every statement needs its semicolon, and a bare expression shows nothing — you must echo or var_dump() it.

php -a
Interactive shell

php > $price = 0.32;
php > echo $price * 350;
112
php > var_dump("5" + "5");
int(10)
php > exit

The shell needs the readline extension. Without it, current PHP versions stop with "Interactive shell (-a) requires the readline extension"; on Debian and Ubuntu install the php8.x-readline package for your version. For longer experiments many developers prefer a small file and php test.php, or a richer shell such as PsySH (a Composer package).

The built-in web server

The built-in web server is a small development server inside the PHP CLI, started with php -S. It serves the files in a folder and runs .php files through PHP, so you can try a site in your browser without installing Apache or Nginx:

php -S localhost:8000 -t public
[Fri Oct  2 09:14:03 2026] PHP 8.4.26 Development Server (http://localhost:8000) started
[Fri Oct  2 09:14:09 2026] [::1]:40968 Accepted
[Fri Oct  2 09:14:09 2026] [::1]:40968 [200]: GET /
[Fri Oct  2 09:14:09 2026] [::1]:40968 Closing
[Fri Oct  2 09:14:09 2026] [::1]:40982 [404]: GET /missing.css - No such file or directory

-t public sets the document root (the folder that URLs map to); without it, the current folder is used. A request for a folder runs its index.php or serves index.html. Every request is logged in the terminal, which makes it easy to see a 404 for a missing stylesheet. Stop the server with Ctrl+C. Inside a script on this server, PHP_SAPI is "cli-server".

Router scripts

Frameworks send every URL to one front controller, index.php, which decides what to show. The built-in server does this with a router script given after the address. The router runs for every request; if it returns false, the server delivers the requested file as it is:

<?php
// router.php — started with: php -S localhost:8000 router.php
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

if (is_file(__DIR__ . $path)) {
    return false;   // an existing file (CSS, image): serve it unchanged
}
require __DIR__ . '/index.php';   // everything else goes to the front controller

Frameworks ship their own command for this, such as php artisan serve in Laravel, which starts PHP's built-in server for you.

Configuration: php.ini

php.ini is PHP's main configuration file. It is read when PHP starts and sets limits (memory, upload size, execution time), error handling, session behavior and which extensions to load. The format is simple: name = value lines, with ; starting a comment.

; Maximum amount of memory a script may consume
memory_limit = 128M

display_errors = Off
log_errors = On
error_reporting = E_ALL

upload_max_filesize = 2M
post_max_size = 8M
date.timezone = Europe/Amsterdam

Finding the php.ini that is used

There is not one php.ini but one per way of running PHP. On Debian and Ubuntu each SAPI has its own folder: /etc/php/8.4/cli/ for the command line, /etc/php/8.4/fpm/ for PHP-FPM and /etc/php/8.4/apache2/ for the Apache module. Next to each php.ini is a conf.d/ folder whose .ini files are read next, in alphabetical order — that is where packages switch their extensions on. Ask PHP itself instead of guessing:

php --ini
Configuration File (php.ini) Path: /etc/php/8.4/cli
Loaded Configuration File:         /etc/php/8.4/cli/php.ini
Scan for additional .ini files in: /etc/php/8.4/cli/conf.d
Additional .ini files parsed:      /etc/php/8.4/cli/conf.d/10-opcache.ini,
/etc/php/8.4/cli/conf.d/10-pdo.ini,
/etc/php/8.4/cli/conf.d/20-mbstring.ini,
…

This shows the command line's files only. For the web server, a page that calls phpinfo() prints the same information ("Loaded Configuration File") as an HTML table, along with every setting and extension. PHP ships two templates to start from: php.ini-development (shows errors) and php.ini-production (hides errors from visitors and logs them).

Common settings

Setting Built-in default What it controls
memory_limit 128M Most memory one script may use; -1 means no limit (Debian's CLI php.ini uses that)
max_execution_time 30 (seconds) How long a web request may run; the CLI always uses 0, no limit
display_errors on Show errors in the output; must be Off in production (see Errors and Exceptions)
error_reporting E_ALL Which kinds of errors are reported at all
upload_max_filesize 2M Largest single uploaded file
post_max_size 8M Largest whole request body; must be at least upload_max_filesize
date.timezone empty (UTC) Default time zone for date functions

"Built-in default" is what PHP uses when php.ini does not set the value; the php.ini files that distributions install often change some of them. A typical ticket — "uploads over 2 MB fail" — is usually upload_max_filesize or post_max_size on the web server's php.ini, plus a similar limit in the web server itself.

Changing settings

Settings are applied in layers; a later layer wins:

  1. Built-in defaults.
  2. php.ini, then the files in conf.d/.
  3. Per-folder settings: .user.ini files with PHP-FPM, or php_value lines in .htaccess with mod_php — common on shared hosting.
  4. -d name=value on the command line.
  5. ini_set() in the script itself, for settings that may be changed at runtime.
<?php
echo "memory_limit: ", ini_get('memory_limit'), "\n";
echo "max_execution_time: ", ini_get('max_execution_time'), "\n";

$old = ini_set('memory_limit', '256M');
echo "memory_limit changed from $old to ", ini_get('memory_limit'), "\n";

var_dump(ini_set('upload_max_filesize', '50M'));   // not allowed at runtime
var_dump(ini_get('no_such_setting'));
memory_limit: 128M
max_execution_time: 0
memory_limit changed from 128M to 256M
bool(false)
bool(false)

ini_get() returns settings as strings. ini_set() returns the old value, or false when it is not allowed: the upload limit has to be known before the script starts, because PHP has already received the upload by then. These examples run without a php.ini, so they show the built-in defaults. After editing php.ini for a website, reload or restart PHP-FPM (for example php8.4-fpm) or Apache; until then the old values stay in effect.

Extensions

A PHP extension is a module, usually written in C, that adds functions, classes and constants to PHP. Some are compiled into every PHP (json, pcre for regular expressions, date); others are separate files that php.ini loads with an extension= line: database drivers such as pdo_mysql and mysqli, mbstring for multibyte text, intl, curl, gd for images. Applications list the extensions they need; WordPress, for example, needs mysqli.

<?php
foreach (['json', 'pcre', 'session', 'xdebug'] as $name) {
    echo str_pad($name, 8), extension_loaded($name) ? "loaded" : "not loaded", "\n";
}
var_dump(function_exists('json_encode'));
json    loaded
pcre    loaded
session loaded
xdebug  not loaded
bool(true)

A missing extension shows up as an error such as Call to undefined function mb_strlen() or Class "PDO" not found. On Debian and Ubuntu, each extension is a package named after the PHP version:

php -m | grep -i mbstring          # is it loaded on the command line?
sudo apt install php8.4-mbstring   # install it for PHP 8.4
sudo systemctl restart php8.4-fpm  # make the web side load it

The package drops an .ini file into the conf.d/ folders, so the extension is enabled for every SAPI. Debian's phpenmod and phpdismod commands switch an installed extension on or off. Some extensions, such as OPcache and Xdebug, are Zend extensions that hook into the engine itself; they are loaded with zend_extension= instead.