Documentation
One interface to implement, one runner to call. CommandRunner finds the command, checks its arguments and options, and calls execute().
1. Install
$ composer require michel/console
PHP 7.4 to 8.4, with the mbstring, json and ctype extensions. No other package.
2. Quick start
Create bin/console at the root of your project:
#!/usr/bin/env php <?php use Michel\Console\CommandParser; use Michel\Console\CommandRunner; use Michel\Console\Output; require dirname(__DIR__) . '/vendor/autoload.php'; $runner = new CommandRunner([ new SendEmailCommand(), ]); exit($runner->run(new CommandParser(), new Output()));
$ chmod +x bin/console
$ bin/console # lists the commands
With a framework, build the commands from your container after booting it, and pass them to CommandRunner the same way.
3. Writing a command
use Michel\Console\Argument\CommandArgument; use Michel\Console\Command\CommandInterface; use Michel\Console\InputInterface; use Michel\Console\Option\CommandOption; use Michel\Console\OutputInterface; final class SendEmailCommand implements CommandInterface { public function getName(): string { return 'send-email'; } public function getDescription(): string { return 'Sends an email to a recipient.'; } public function getArguments(): array { return [ CommandArgument::required('recipient', 'Email address of the recipient'), ]; } public function getOptions(): array { return [ CommandOption::withValue('subject', 's', 'Subject of the email', 'No subject'), CommandOption::flag('dry-run', 'd', 'Do not send anything'), ]; } public function execute(InputInterface $input, OutputInterface $output): void { $recipient = $input->getArgumentValue('recipient'); $subject = $input->getOptionValue('subject'); if ($input->getOptionValue('dry-run')) { $output->writeln("Would send to $recipient: $subject"); return; } // send it $output->writeln("Email sent to $recipient: $subject"); } }
Command names may contain : to group them: cache:clear, user:create. To report a failure, throw an exception from execute() (see Errors and exit codes).
4. Arguments
Arguments are positional: their order in getArguments() is their order on the command line.
CommandArgument::required('recipient', 'Email address'); CommandArgument::optional('format', 'html', 'html or text'); // default: 'html' new CommandArgument('format', false, 'html', 'html or text'); // same, with the constructor
- A name contains letters only:
recipient, notuser_idoruser-id. It is stored in lower case. - A required argument cannot have a default value.
- A required argument given as an empty string (
"") is missing;0is a value. - More arguments than declared is an error.
5. Options
CommandOption::withValue('subject', 's', 'Subject of the email', 'No subject'); // takes a value, with a default CommandOption::withValue('limit', 'l', 'Maximum number of rows'); // takes a value, no default CommandOption::flag('force', 'f', 'Skip the confirmation'); // present or not
- A name contains letters and dashes:
dry-run. A shortcut is one letter, ornull. - A default value is a scalar: string, int, float or bool.
help,h,verboseandvare reserved for every command: a command that declares them fails with an error.
6. Command-line syntax
| You type | Result |
|---|---|
bin/console send-email john@example.com | Argument recipient = john@example.com |
--subject=Hello, --subject="Hello you" | Long option with its value, joined by = |
-s Hello | Shortcut, value after a space |
--dry-run, -d | Flag set to true |
-df | Several flags at once: -d and -f |
bin/console c:c | Runs cache:clear: each part of the name may be shortened |
=. --subject Hello reads --subject without a value and Hello as an argument; the command then fails with Option "subject" requires a value. Write --subject=Hello or -s Hello.A shortened name must match a single command: with user:create and user:list, u:c works and u lists both and stops. A name typed in full always wins, even if it is the beginning of another one.
7. Reading the input
$input->getArgumentValue('recipient'); // the value, or the default of an optional argument $input->getOptionValue('subject'); // the value, the default, or null if neither $input->getOptionValue('dry-run'); // true if given, false otherwise
Values typed on the command line are strings: cast them ((int) $input->getOptionValue('limit')). Default values keep the type you declared.
getOptionValue(), not hasOption(). Every flag of the command is in the input, set to false when absent, so hasOption('dry-run') is always true.8. Output
Output writes. By default, to STDOUT, and to STDERR for errors:
$output = new Output();
Pass a callable to send the text anywhere else, a log, a buffer, a test:
$output = new Output(function (string $message, bool $error = false) { file_put_contents('/var/log/cli.log', $message, FILE_APPEND); });
OutputInterface has five methods: write(), writeln(), error(), setVerbose(), isVerbose().
9. ConsoleOutput
Wrap the output in ConsoleOutput for formatted messages, lists, tables and prompts:
use Michel\Console\Output\ConsoleOutput; $io = new ConsoleOutput($output); $io->title('User report'); $io->table(['ID', 'Name'], [[1, 'John'], [2, 'Jane']]); $io->success('Report generated.');
| Method | Shows |
|---|---|
success(), info(), warning() | A colored block: [OK], [INFO], [WARNING] |
error() | A red [ERROR] block, written to STDERR |
debug() | A [DEBUG] block, only in verbose mode |
title($text) | A title, underlined with = |
boxed($text, '*', 1) | Text in a box, wrapped to the terminal width |
list(), numberedList(), indentedList() | A dash list, a numbered list, a nested list |
listKeyValues($items, true) | key : value lines, keys aligned when the second argument is true |
table($headers, $rows) | A table; long cells are cut to fit the terminal |
json($data) | Pretty-printed JSON |
progressBar($total, $current) | A 50-character bar and a percentage, redrawn on the same line |
spinner($seconds) | A spinner for the given number of seconds |
confirm($question) | Asks [y/n], returns true for y or yes |
ask($question, $hidden, $required) | Reads an answer; hidden input (passwords) is not supported on Windows |
writeColor($text, 'green', 'black') | Text with a foreground and an optional background color |
10. Colors
Colors are ANSI codes. They are written only when they will be seen:
- Output to a terminal: colors.
- Output redirected to a file or a pipe (
bin/console report > report.txt,| grep): plain text. NO_COLORset in the environment: plain text, whatever the output.Outputwith your own callable: colors, as written.
Foreground colors: black, dark_gray, red, light_red, green, light_green, yellow, light_yellow, blue, light_blue, purple, light_purple, cyan, light_cyan, light_gray, white. Backgrounds: black, red, green, yellow, blue, magenta, cyan, light_gray. An unknown color name is ignored.
11. Help and verbose mode
$ bin/console # list of the commands $ bin/console --help # same, also -h $ bin/console send-email --help # description, arguments and options of one command, also -h $ bin/console send-email john@example.com -v
-v or --verbose works with every command, without declaring it. It shows the debug() messages and, after the command, its execution time and peak memory. Inside a command, $output->isVerbose() tells you if it is on.
12. Errors and exit codes
run() returns the exit code: pass it to exit(). Any exception thrown by a command is caught, its message is shown as an [ERROR] block on STDERR.
| Code | Constant | When |
|---|---|---|
0 | CommandRunner::CLI_SUCCESS | The command ran, or the help was shown |
1 | CommandRunner::CLI_ERROR | The command threw an exception |
10 | CommandRunner::CLI_COMMAND_NOT_FOUND | No command matches the name |
11 | CommandRunner::CLI_INVALID_ARGUMENTS | Missing argument, too many arguments, unknown option, option without its value |
12 | CommandRunner::CLI_AMBIGUOUS_COMMAND | A shortened name matches several commands; they are listed |
13. Testing a command
Give the runner your own argv and an Output that keeps the text:
$text = ''; $output = new Output(function (string $message) use (&$text) { $text .= $message; }); $runner = new CommandRunner([new SendEmailCommand()]); $code = $runner->run(new CommandParser(['bin/console', 'send-email', 'john@example.com', '-d']), $output); // $code === 0 // $text === "Would send to john@example.com: No subject\n"
The first item of the array is the script name, as in $_SERVER['argv'].