stubbles/console

Stubbles package to build command line apps.

v7.1.0 2016-08-30 20:43 UTC

README

Support for command line applications.

Build status

Build Status Coverage Status

Latest Stable Version Latest Unstable Version

Installation

stubbles/console is distributed as Composer package. To install it as a dependency of your package use the following command:

composer require "stubbles/console": "^7.0"

Requirements

stubbles/console requires at least PHP 7.0.

Console app class

The main point for command line applications are their Console app classes. They must extend stubbles\console\ConsoleApp, and have two purposes:

  1. Provide a list of binding modules for the Stubbles IoC mechanism that tells it how to wire the object graph for the whole application.
  2. Get the main entry of the logic injected and run it within its run() method. Depending on the outcome of the logic it should return a proper exit code, which means 0 for a successful execution and any other exit code for in case an error happened.

For details about the IoC part check the docs about Apps in stubbles/ioc. Generally, understanding the Inversion of Control functionality in stubbles/ioc will help a lot in regard on how to design the further classes in your command line app.

For an example of how a console app class might look like check ConsoleAppCreator, the Console app class behind the createConsoleApp script explained below.

Exit codes

The Console app class' run() method should return a proper exit code. It should be 0 if the run was successful, and a non-zero exit code in case an error occurred.

It is recommended to use exit code between 21 and 255 for errors. Exit codes 1 to 20 are reserved by Stubbles Console and should not be used, whereas the upper limit of 255 is a restriction by some operating systems.

Reserved exit codes

  • 10 Returned when one of the command line options or arguments for the app contains an error.
  • 20 Returned when an uncatched exception occurred during the run of the console app class.

How to create a command line app

In order to ease creation of console apps stubbles/console provides a helper script that can create the skeleton of a Console app class, the fitting command line script to call this Console app, and a unit test skeleton for this console app class.

When in the root directory of your project and stubbles/console is installed, simply type

vendor/bin/createConsoleApp

First it will ask you for the full qualified name of the console app class to create. Type it into the prompt and confirm with enter. There is no need to escape the namespace separator. Second, it will ask you for the name of the command line script which should be created. Type it's name into the prompt and confirm with enter.

Once the script finishes you will find three new files in your application:

  • a class in src/main/php with the class name you entered, extending the stubbles\console\ConsoleApp class.
  • a script in the bin folder with the name you typed in second.
  • a unit test for the app class in src/test/php with the class name you entered.

At this point the app class, the script and the unit test are already fully functional - they can be run but of course will do nothing.

From this point you can start and extend the generated class and unit test with the functionality you want to implement.

FAQ

What happens if the entered class already exists?

The creating script will check if a class with the given name already exists within the project. This includes all classes that can be loaded via the autoload functionality. If it does exist, creation of the app class is skipped.

What happens if the script to be created already exists?

Creation of the script will be skipped.

What happens if a unit test in src/test/php with this class name already exists?

Creation of the unit test will be skipped.

Can I use the createConsoleApp script to generate a script or a unit test for an already existing app?

Yes, this is possible. Just enter the name of the existing app class. As the class already exists, it's creation will be skipped, but the script and unit test will still be created if they don't exist yet.

Provided binding modules

When compiling the list of binding modules in the __bindings() method of your console app class, you can make use of binding modules provided by stubbles/console. The stubbles\console\ConsoleApp class which your own console app class should extend provides static helper methods to create those binding modules:

argumentParser()

Creates a binding module that parses any arguments passed via command line and adds them as bindings to become available for injection. See below for more details about parsing command line arguments.

Parsing command line arguments

Most times a command line app needs arguments to be passed by the caller of the script. When the argument binding module is added to the list of binding modules (see above) stubbles/console will make those arguments available for injection into the app's classes.

Without special parsing

By default all arguments given via command line are made available as an array and as single values. Suppose the command line call was ./exampleScript foo bar baz then the following values will be available for injection:

  • @Named('argv'): the whole array of input values: array('foo', 'bar', 'baz').
  • @Named('argv.0'): the first value that is not the name of the called script, in this case foo.
  • @Named('argv.1'): the second value that is not the name of the called script, in this case bar.
  • @Named('argv.2'): the third value that is not the name of the called script, in this case baz.

Requesting a value that was not passed, e.g. @Named('argv.3') in this example, will result in a stubbles\ioc\BindingException.

With special parsing

In some cases you need more sophisticated argument parsing. This is also possible:

self::argumentParser()
        ->withOptions($options)
        ->withLongOptions(array $options)

Using this we can parse arguments like this:

./exampleScript -n foo -d bar --other baz -f --verbose

To get a proper parsing for this example the arguments binding module must be configured as follows:

self::argumentParser()
        ->withOptions('fn:d:')
        ->withLongOptions(array('other:', 'verbose'))

For more details about the grammar for the options check PHP's manual on getopt().

If arguments are parsed like this they become available for injection with the following names:

  • @Named('argv'): the whole array of input values: array('n' => 'foo', 'd'=> 'bar', 'other' => 'baz', 'f' => false, 'verbose' => false).
  • @Named('argv.n'): value of the option -n, in this case foo.
  • @Named('argv.d'): value of the option -d, in this case bar.
  • @Named('argv.other'): value of the option --other, in this case baz.
  • @Named('argv.f'): value of the option -f, in this case false.
  • @Named('argv.verbose'): value of the option --verbose, in this case false.

Any value being false is due to the fact that PHP's getopt() function sets the values for arguments without values to false.

Requesting a value that was not passed, e.g. @Named('argv.invalid') in this example, will result in a stubbles\ioc\BindingException.

Console app runner script

For each console app there should be a runner script that can be used to execute the console app. When using the createConsoleApp script (see above) such a script will be created automatically.

Testing Console apps

Having all code required in an app in an app class has a huge advantage: you can create a unit test that makes sure that the whole application with all dependencies can be created. This means you can have a unit test like this:

    /**
     * @test
     */
    public function canCreateInstance()
    {
        $this->assertInstanceOf(
                MyConsoleApp::class,
                MyConsoleApp::create(new Rootpath())
        );
    }

This test makes sure that all dependencies are bound and that an instance of the app can be created. If you also have unit tests for all the logic you created and you run those tests you can be pretty sure that the application will work.

Tests for apps created with createConsoleApp

The unit test created with createConsoleApp will already provide two tests:

  • A test that makes sure that the run() method returns with exit code 0 after a successful run.
  • And finally a test that makes sure that an instance of the app can be created (see above for how this looks like).

From this point on it should be fairly easy to extend this unit test with tests for the logic you implement in your app class.

Reading from command line

In order to read user input from the command line one can use the stubbles\console\ConsoleInputStream. It is a normal input stream from which can be read.

If you want to get such an input stream injected in your class it is recommended to typehint against stubbles\streams\InputStream and add a @Named('stdin') annotation for this parameter:

    /**
     * receive input stream to read from command line
     *
     * @param  InputStream  $in
     * @Named('stdin')
     */
    public function __construct(InputStream $in)
    {
        $this->in = $in;
    }

Writing to command line

To write to the command line there are two possibilities: either write directly to standard out, or write to the error out. Both ways are implemented as an output stream.

If you want to get such an output stream injected in your class it is recommended to typehint against stubbles\streams\OutputStream and add a @Named('stout') or @Named('sterr') respectively annotation for these parameters:

    /**
     * receive streams for standard and error out
     *
     * @param  OutputStream  $out
     * @param  OutputStream  $err
     * @Named{out}('stdout')
     * @Named{err}('stderr')
     */
    public function __construct(OutputStream $out, OutputStream $err)
    {
        $this->out = $out;
        $this->err = $err;
    }

Reading from and writing to command line

Sometimes there are situations when you need to read from and to write to command line at the same time. That's where stubbles\console\Console comes into play. It provides a facade to stdin input stream, stdout and stderr output streams so you have a direct dependency to one class only instead of three. The class provides methods to read and write:

prompt(string $message, ParamErrors $paramErrors = null): ValueReader

Available since release 2.1.0.

Writes a message to stdout and returns a value reader similar to reading request parameters. In case you need access to error messages that may happen during value validation you need to supply stubbles\input\ParamErrors, errors will be accumulated therein under the param name stdin.

readValue(ParamErrors $paramErrors = null): ValueReader

Available since release 2.1.0.

Similar to prompt(), but without a message

confirm(string $message, string $default = null): bool

Available since release 2.1.0.

Asks the user to confirm something. Repeats the message until user enters y or n (case insensitive). In case a default is given and the users enters nothing this default will be used - if the default is y it will return true, and false otherwise.

read(int $length = 8192): string

Reads input from stdin.

readLine(int $length = 8192): string

Reads input from stdin with line break stripped.

write(string|InputStream $bytes): Console

Write message to stdout, in case of an input stream the contents of that stream are copied to stdout.

writeLine(string $bytes): Console

Write a line to stdout.

writeEmptyLine(): Console

Available since release 2.6.0. Write an empty line to stdout.

writeError(string|InputStream $bytes): Console

Write error message to stderr, in case of an input stream the contents of that stream are copied to stderr.

writeErrorLine(string $bytes): Console

Write an error message line to stderr.

writeEmptyErrorLine(): Console

Available since release 2.6.0. Write an empty error message line to stderr.

Command line executor

From time to time it is necessary to run another command line program from within your application. stubbles/console provides a convenient way to do this via the stubbles\console\Executor class.

It provides three different ways to run a command line program:

  1. execute(string $command, callable $out = null): This will simply execute the given command. If the executor receives an callable the callable will be executed for each single line of the command's output.
  2. executeAsync(string $command): InputStream: This will execute the command, but reading the output of the command can be done later via the returned InputStream.
  3. outputOf(string $command): \Generator: This will execute the given command and return a Generator which yields each single line from the command's output as it occurs. (Available since release 6.0.0.)

If the executed command returns an exit code other than 0 this is considered as failure, resulting in a \RuntimeException.

Redirecting output

If you want to redirect the output of the command to execute you can provide a redirect as an optional last argument for each of the methods listed above. By default the error output of a command is redirected to the standard output using 2>&1.

Examples

Running a command, discarding its output:

$executor->execute('git clone git://github.com/stubbles/stubbles-console.git');

Running a command and retrieve the output:

$executor->execute('git clone git://github.com/stubbles/stubbles-console.git', [$myOutputStream, 'writeLine']);

Running a command asynchronously:

$inputStream = $executor->executeAsync('git clone git://github.com/stubbles/stubbles-console.git');
// ... do some other work here ...
while (!$inputStream->eof()) {
    echo $inputStream->readLine();
}

Directly receive command output:

foreach ($executor->outputOf('git clone git://github.com/stubbles/stubbles-console.git') as $line) {
    echo $line;
}

Collect output in a variable

Sometimes it's sufficient to collect the output of a command in a separate variable. This can be done using the stubbles\console\collect() function:

$out = '';
$executor->execute('git clone git://github.com/stubbles/stubbles-console.git', collect($out));

Afterwards, $out contains all output from the command, separated by PHP_EOL. Alternatively, an array can be used, each element in the array will be a line from the command output then.