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, not user_id or user-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; 0 is 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, or null.
  • A default value is a scalar: string, int, float or bool.
  • help, h, verbose and v are reserved for every command: a command that declares them fails with an error.

6. Command-line syntax

You typeResult
bin/console send-email john@example.comArgument recipient = john@example.com
--subject=Hello, --subject="Hello you"Long option with its value, joined by =
-s HelloShortcut, value after a space
--dry-run, -dFlag set to true
-dfSeveral flags at once: -d and -f
bin/console c:cRuns cache:clear: each part of the name may be shortened
A long option takes its value with =. --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.

Read flags with 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.');
MethodShows
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_COLOR set in the environment: plain text, whatever the output.
  • Output with 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.

CodeConstantWhen
0CommandRunner::CLI_SUCCESSThe command ran, or the help was shown
1CommandRunner::CLI_ERRORThe command threw an exception
10CommandRunner::CLI_COMMAND_NOT_FOUNDNo command matches the name
11CommandRunner::CLI_INVALID_ARGUMENTSMissing argument, too many arguments, unknown option, option without its value
12CommandRunner::CLI_AMBIGUOUS_COMMANDA 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'].