Laravel/Symfony Console Commands & Stderr

When we use Symfony's Console component to write CLI commands in PHP (and you should!), we're almost always writing any output to "stdout".

There's a few ways to get general output from a CLI command using Console:

// Run the command (Laravelish)
public function fire()
{
    echo "This is sent to stdout";  // Just text

    $this->info('Some info'); // Regular Text
    $this->error('An Error'); // Red Text
}

// Run the command (Symonfyish)
public function execute(InputInterface $input, OutputInterface $output)
{
    echo "This is sent to stdout";  // Just text

    $output->writeln("<info>$string</info>");   // Regular Text
    $output->writeln("<error>$string</error>"); // Red Text
}

Console commands in Laravel use Symfony's Console component under the hood. While I'll write this (mostly) in context of Laravel, this is definitely applicable to Symfony users and those not using Laravel (collectively, "the haters") as well.

All of this, even the "error" output, writes to "Stdout". This isn't necessarily good. In fact, the default behavior can easily make for some surprises to other developers calling these commands over a CLI.

Convention

The following are well established *nix conventions to follow for any CLI tool.

  • Only write pertinent, needed information to Stdout
  • Write info messages (non-pertinent) to Stderr, even if it's not an error message
  • Only attempt to detect if the command was successful based on the result the command returns (exit status code). That will be with 0 (success) or 1 (failure).
    • Do not attempt to guess if a command failed simply because output was sent to Stderr
  • Usually if everything works, then nothing is output. Simply returning a 0 exit code is generally enough.

Writing important information to Stdout lets administrators send important data to log files. Writing non-important to Stderr lets administrators ignore it or send it to a log file specifically for errors or other information.

Perhaps more importantly is that Stdout output might get piped to another process to handle (think about anytime you do cat /some/file | grep 'search-term'). You don't want non-important output sent to Stdout in those cases. Sending those to Stderr makes the most sense then.

Lastly, because of these conventions, it's important that your commands return a 0 or 1 if they are successful or if the exit with an error. This is The Way™ that should be used to detect if there's truly an error or if the command operated successfully.

In Practice

Here's how I setup Laravel commands:

<?php namespace Foo\Bar;

use Illuminate\Console\Command;
use Symfony\Component\Console\Output\ConsoleOutputInterface;

class MyCommand extends Command {

    # Some boiler plate omitted

    public function run()
    {
        // Default $stdErr variable to output
        $stdErr = $this->getOutput();

        if( $this->getOutput() instanceof ConsoleOutputInterface )
        {
            // If it's available, get stdErr output
            $stdErr = $this->getOutput()->getErrorOutput();
        }

        try {
            // Some operations

            // Non-critical information message
            // Since we have the Symfony output object, use writeln function
            $stdErr->writeln('<info>Status: Working...</info>')
        } catch( \Exception $e )
        {
            // Since we have the Symfony output object, use writeln function
            $stdErr->writeln('<error>'.$e->getMessage().'</error>');
            return 1;
        }

        // Important output
        $this->info('Your new API key is: aaabbbcccddd');
        return 0;
    }
}

And the same in a Symfony command:

<?php namespace Foo\Bar;

use Symfony\Component\Console\Command\Command
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;

class MyCommand extends Command {

    # Some boiler plate omitted

    public function execute(InputInterface $input, OutputInterface $output)
    {
        // Default $stdErr variable to output
        $stdErr = $output;

        if( $output instanceof ConsoleOutputInterface )
        {
            // If it's available, get stdErr output
            $stdErr = $output->getErrorOutput();
        }

        try {
            // Some operations

            // Non-critical information message
            $stdErr->writeln('<info>Status: Working...</info>')
        } catch( \Exception $e )
        {
            $stdErr->writeln('<error>'.$e->getMessage().'</error>');
            return 1;
        }

        // Important output
        $output->writeln('<info>Your new API key is: aaabbbcccddd</info>');
        return 0;
    }
}

These classes mirror each other. The Laravel version uses some of its syntactic sugar.

Let's go over what's going on.

First, I assign a variable $stdErr. This gets assigned a fallback of the Output object. I'm going to use this variable later for error output regardless of whether it's used for Stdout (the default) or Stderr.

If the Output object happens to be an instance of ConsoleOutputInterface, I'll know it has the getErrorOutput method available. Not all Output implementations do, so this check is important. Stderr can then be assigned the Error Output object, which will write to Stderr. I can then easily differentiate output between Stderr and Stdout.

The rest of this is implementation of the above conventions. I write non-essential information to Stderr, but use the "info" formatters, as they don't need the red error styling.

Actual errors are also output to Stderr, but with the red output styling.

Important information is output to Stdout, again with the "info" styling.

Note that I return 0 or 1 (0 for success). The return value is taken by the Symfony Console component and returned as the exit code of the command. If you define nothing, then 0 is returned, even if you have output an error!

If this command didn't need the resulting output (for example, the new API key), I would return nothing. If I had a "success" message, I would actually return that in Stderr, but with the "info" formatting.

comments powered by Disqus