Close search
Hoa

Hack book of Hoa\Console

The terminal is a very powerful interface that is based on multiple concepts. Hoa\Console allows to write tools that are adapted to this kind of environment.

Table of contents

  1. Introduction
  2. Window
    1. Size and position
    2. Title and label
    3. Interact with the content
  3. Cursor
    1. Moving
    2. Content
    3. Style
    4. Sound
  4. Readline
    1. Basic usage
    2. Shortcuts
    3. Auto-completion
  5. Reading options
    1. Analyzing options
    2. Read options and inputs
    3. Special or ambiguous options
    4. Integrate a router and a dispatcher
  6. Processus
    1. Very basic execution
    2. Reading and writing
    3. Detect the type of pipes
    4. Execution conditions
    5. Miscellaneous
    6. Interactive processus and pseudo-terminals
  7. Conclusion

Introduction

Nowadays, we have two kinds of interface: textual and graphical. The textual interface exist since the origin of computers, then called terminal. This interface, despite its “raw” aspect, is functionally very powerful thanks to several concepts such as the readline or pipes. Today, it is even more used because it is often faster than a graphical interface when executing complex tasks. It can also easily be used through networks or on a machine with low performances. In short, this interface is still indispensable.

From the user point of view, there is three levels to consider:

The Hoa\Console library provides tools to answer to these three levels. Thus, it is based on standards, such as ECMA-48 that specifies the communication with the system through sequences of ASCII characters and control codes (also called escaping sequences), all of that, in order to manipulate the window, the cursor or other devices of the machine. Other features are also standard such as the way we read options from a program, very inspired of systems like Linux, FreeBSD or System V. By the way, if you are familiar with several C libraries, you will not be lost. And a contrario, if you learn to use Hoa\Console, you will not be lost when using low-level languages such as C.

Before starting, we would like to add a little note only about the window and cursor support. Today, we have the choice among many terminals per system and some of them are more comprehensive than others. For example, Windows and its default terminal, MS-DOS, does not respect any standard. In this case, forget the ECMA-48 standard and take a look at the Wincon library. It is often recommended to use a virtual Unix machine or a terminal emulator, such as TeraTerm, very comprehensive. Even on systems that are closed to the BSD family, the embeded terminals do not support all the standards. This is the case of Mac OS X where we recommend to use iTerm2 instead of Terminal. Finally, on other systems of the Linux or BSD family, we recommend urxvt. For other features, such as the readline, the options reading, the processus etc., Hoa\Console is perfectly compatible and suitable.

Window

The window of a terminal must be understood as a canvas of columns and lines. The Hoa\Console\Window allows to manipulate the window of a terminal and its content through static methods.

Size and position

The first elementary operations are about the size and the position of the window, thanks to the setSize, getSize, moveTo and getPosition methods. The size is defined with the column × line unit and the position is defined in pixels. Thus:

Hoa\Console\Window::setSize(80, 50);
print_r(Hoa\Console\Window::getSize());
print_r(Hoa\Console\Window::getPosition());

/**
 * Will output:
 *     Array
 *     (
 *         [x] => 80
 *         [y] => 50
 *     )
 *     Array
 *     (
 *         [x] => 104
 *         [y] => 175
 *     )
 */

We will notice that the window is resized itself. Nor the size or the position of the window is stored in memory, they are computed at each call of the getSize and getPosition method. Attention, the y axe of the window position is computed from the bottom of the screen and not from the top of the screen as we might expect!

It is also possible to listen the hoa://Event/Console/Window:resize event that is fired each time the window is resized: either manually or with the setSize method. We need two things in order to see this event working:

  1. the pcntl extension must be activated,
  2. we have to use the declare structure to enable the the pcntl_signal function.

To put the program in a passive waiting state, we will use the stream_select function, this is a detail, only present to test our code, else the program will terminate directly. Thus:

Consistency\Autoloader::load('Hoa\Console\Window'); // make sure it is loaded.

declare(ticks = 1);

Hoa\Event\Event::getEvent('hoa://Event/Console/Window:resize')
    ->attach(function (Hoa\Event\Bucket $bucket) {
        $data = $bucket->getData();
        $size = $data['size'];

        echo 'New size (', $size['x'], ', ', $size['y'], ')', "\n";
    });

// Passive loop.
while (true) {
    $r = [STDIN];
    @stream_select($r, $w, $e, 3600);
}

When we modify the size of the window, we will see for example: New size (45, 67), and this, for each resize. This event is interesting if we would like to re-layout our view.

Finally, we can minimize or restore the window thanks to the Hoa\Console\Window::minimize and Hoa\Console\Window::restore static methods. Moreover, we can put the window in the background (behind all other windows) thanks to the Hoa\Console\Window::lower static method, just like we can place it in the foreground with Hoa\Console\Window::raise. For example:

Hoa\Console\Window::minimize();
sleep(2);
Hoa\Console\Window::restore();
sleep(2);
Hoa\Console\Window::lower();
sleep(2);
Hoa\Console\Window::raise();

echo 'Back!', "\n";

Title and label

The title of a window is the text displayed in the top bar in which the controls of the window are often placed, such as the maximization, the minimization etc. The label is the name associated to the current processus. We have the setTitle, getTitle and getLabel methods, it is not possible to modify the label. To define the title of the processus (what we see with the top or ps command for example), we must take a look at Hoa\Console\Processus::setTitle and also at Hoa\Console\Processus::getTitle to get it. Thus:

Hoa\Console\Window::setTitle('Foobar');
var_dump(Hoa\Console\Window::getTitle());
var_dump(Hoa\Console\Window::getLabel());

/**
 * Will output:
 *     string(6) "Foobar"
 *     string(3) "php"
 */

Once again, the title and the label are not stored in memory, they are computed at each call of the methods.

Interact with the content

Hoa\Console\Window also allows to control the content of the window, or at least the viewport, it means the visible content of the window. Only one method is available: scroll, that allows to move the content to the top or the bottom. The arguments of this method are very simple: up or to move up of one line and down or to move down of one line. We are able to concatenate those directions by a space or to repeat one direction:

Hoa\Console\Window::scroll('↑', 10);

In reality, this method will move the content in order to get x new lines respectively bellow or above the cursor. Attention, the cursor does not move!

Even if it is most of the time useless, it is possible to refresh the window, it means to redo a full render. We can use the refresh method, still on Hoa\Console\Window.

Finally, it is possible to put a text inside the clipboard of the user thanks to the copy method:

Hoa\Console\Window::copy('Foobar');

Then, if the user pastes what is in the clipboard, she will see Foobar.

Cursor

Inside a window, we have a cursor that can be seen as a tip of pen. The Hoa\Console\Cursor class allows to manipulate the cursor of the terminal through static methods.

Moving

We will start to move the cursor. It can move everywhere inside the viewport, it means the visible content of the terminal window, but we will write a piece of text and move around at first. The move method on Hoa\Console\Cursor allows to move the cursor in several directions. First of all, relatively:

We also have semi-absolute moving:

We are able to concatenate those directions by a space or to repeat one direction.

echo
    'abcdef', "\n",
    'ghijkl', "\n",
    'mnopqr', "\n",
    'stuvwx';

sleep(1);
Hoa\Console\Cursor::move('↑');
sleep(1);
Hoa\Console\Cursor::move('↑ ←');
sleep(1);
Hoa\Console\Cursor::move('←', 3);
sleep(1);
Hoa\Console\Cursor::move('DOWN');
sleep(1);
Hoa\Console\Cursor::move('→', 4);

During the execution, we will see the cursor moving by itself from a letter to another one each second.

To actually move the cursor in an absolute way, we will use the moveTo method that takes column × line coordinates (starting from 1 and not 0) as argument. Similarly, there is the getPosition method that returns the position of the cursor. Thus, if we would like to move the cursor from the column 12 and line 7, and then print its coordinates, we will write:

Hoa\Console\Cursor::moveTo(12, 7);
print_r(Hoa\Console\Cursor::getPosition());

/**
 * Will output:
 *     Array(
 *         [x] => 12
 *         [y] => 7
 *     )
 */

Finally, we often would like to move the cursor temporary for some operations. In this case, it is useless to get its current position, to move it and then re-position it; we can benefit from the save and restore methods. As their names suggest, they respectively save the position of the cursor and then restore the cursor as the previously saved position. These functions do not manipulate a stack, it is impossible to save more than one position at a time (the new record will erase the previous one). Thus, we will write a text, save the position of the cursor, go back to rewrite, and then go back to our previous position:

echo 'hello world';

// Save cursor position.
Hoa\Console\Cursor::save();
sleep(1);

// Go to the begining of the line.
Hoa\Console\Cursor::move('LEFT');
sleep(1);

// Replace “h” by “H”.
echo 'H';
sleep(1);

// Go to “w”.
Hoa\Console\Cursor::move('→', 5);
sleep(1);

// Replace “w” by “W”.
echo 'W';
sleep(1);

// Back to the saved position.
Hoa\Console\Cursor::restore();
sleep(1);

echo '!';

The final result will be Hello World!. We can notice that each time a character is written, the cursor moves.

Content

Now we know how to move, we will see how to clean some lines and/or columns. We will use the clean method that takes the following symbols (concatenated by a space) as argument:

Thus, to clean a line:

Hoa\Console\Cursor::clear('↔');

The cursor can act as a brush and thus write in different colors or different styles thanks to the colorize method (we can mix everything by separating each “command” by spaces). We start by enumerating all the styles:

Those styles are very classical. Now, let's see the colors. Before all, we have to say if we apply a color on the foreground of the text (the text itself) or on the background. Then, we will respectively use the f[ore]g[round](color) or b[ack]g[round](color) syntax. The value of color can be:

Terminals manipulate one of two palettes: 8 colors or 256 colors. Each color is indexed starting from 0. The names of colors are transformed into their respective index. When a color is given in hexadecimal, the closest color in the 256 colors palette is used.

Thus, if we would like to write Hello in yellow on a red-like background (#932e2e) and underlined, then world but not underlined:

Hoa\Console\Cursor::colorize('fg(yellow) bg(#932e2e) underlined');
echo 'Hello';
Hoa\Console\Cursor::colorize('!underlined');
echo ' world';

Finally, it is possible to modify the palette of colors thanks to the changeColor method, but use it with caution, this can disturb the user. This method takes as first argument the index of the color and as second argument its hexadecimal value. For example, fg(yellow) matches the 33 index, and we would like to see it blue:

Hoa\Console\Cursor::changeColor(33, 0xf00);

However, the 256 colors palette is wide enough to not need to redefine colors.

Style

The cursor is not necessary visible. During some operations, we can hide it, make some moves and then make it visible again. The hide and show methods, still on Hoa\Console\Cursor, are here for that:

echo 'Visible', "\n";
sleep(5);

echo 'Invisible', "\n";
Hoa\Console\Cursor::hide();
sleep(5);

echo 'Visible', "\n";
Hoa\Console\Cursor::show();
sleep(5);

There is three types of cursors, that we will choose with the setStyle method:

This method takes as a second argument a boolean which indicates if the cursor must blink (default value) or not. Thus, we will try all the styles:

echo 'Block/steady: ';
Hoa\Console\Cursor::setStyle('▋', false);
sleep(3);

echo "\n", 'Vertical/blink: ';
Hoa\Console\Cursor::setStyle('|', true);
sleep(3);

// etc.

The cursor often indicates area or interactive elements, just like the pointer of a mouse.

Sound

The cursor is also able to emit a little “bip”, most of the time to bring the attention of the user. We will use the eponym bip method:

Hoa\Console\Cursor::bip();

There is only one available tonality.

Readline

A way to interact with users is to read the STDIN stream, namely the input stream. This reading is by default very basic: unable to erase, unable to use arrows, unable to use shortcuts etc. That's why there is the readline, that is still a reading on the STDIN stream, but more sophisticated. The Hoa\Console\Readline\Readline library provides several features that we will describe.

Basic usage

To read a line (it means an input from the user), we will instanciate the Hoa\Console\Readline\Readline class and call the readLine method. Each call of this method will wait that the user inputs a data and then hits . At this moment, the method will returns the input from the user (or false if there is nothing to read). This method also takes a prefix as argument, it means a data to print before the line. Sometimes, the term prompt could be used in the literature, both notions are identical.

Thus, we will write a program that will read the inputs from the user and make an echo. The program will finish if the user inputs quit:

$rl = new Hoa\Console\Readline\Readline();

do {
    $line = $rl->readLine('> ');
    echo '< ', $line, "\n\n";
} while (false !== $line && 'quit' !== $line);

Now, let's detail the features offered by Hoa\Console\Readline\Readline.

We are able to move (understand, move the cursor) in the line with the help of the and keys. We can erase a character back at any moment with the key or all the characters until the beginning of the word with Ctrl + W (where W stands for word). We are also able to move the cursor by using shortcuts that a shared by many softwares:

We also have access to the history when we hit the and keys, respectively to search back and forth in the history. The key fires the auto-completion if set. And finally, the key returns the input.

There is also the Hoa\Console\Readline\Password class that allows to have a readline with the same features but the characters are not printed to the screen, very useful to read a password:

$rl  = new Hoa\Console\Readline\Password();
$pwd = $rl->readLine('Password: ');

echo 'Your password is: ', $pwd, "\n";

Shortcuts

To understand how to create a shortcut, we have to understand a little bit how the Hoa\Console\Readline\Readline class works internally, and this is very simple. Each time we hit one or many keys, a string representing this combination is received by our readline. It looks for an associated action to this string: if one exists, then this one will be executed, else the readline will use a default action consisting to print the string verbatim. Each action returns the state of the readline (which are constants on Hoa\Console\Readline\Readline):

Thus, if an action returns STATE_CONTINUE | STATE_NO_ECHO, the reading will continue but the string that has been received will not be printed. Another example, the action associated to the key returns the STATE_BREAK state.

To add actions, we use the addMapping method. This latter eases the add thanks to a dedicated syntax:

For example, if we would like to print z instead of a, we will write:

$rl->addMapping('a', 'z');

More sophisticated this time, we can use a callable as the second argument of the addMapping method. This callable will receive the instance of Hoa\Console\Readline\Readline as the only argument. Several methods will help to manipulate the reading (to manage the history, the line etc.). For example, each time we will hit Ctrl + R, we will reverse the case of the line:

$rl = new Hoa\Console\Readline\Readline();

// Add mapping.
$rl->addMapping('\C-R', function (Hoa\Console\Readline\Readline $self) {
    // Clear the line.
    Hoa\Console\Cursor::clear('↔');
    echo $self->getPrefix();

    // Get the line text.
    $line = $self->getLine();

    // New line.
    $new  = null;

    // Loop over all characters.
    for ($i = 0, $max = $self->getLineLength(); $i < $max; ++$i) {
        $char = mb_substr($line, $i, 1);

        if ($char === $lower = mb_strtolower($char)) {
            $new .= mb_strtoupper($char);
        } else {
            $new .= $lower;
        }
    }

    // Set the new line.
    $self->setLine($new);

    // Set the buffer (and let the readline echoes or not).
    $self->setBuffer($new);

    // The readline will continue to read.
    return $self::STATE_CONTINUE;
});

// Try!
var_dump($rl->readLine('> '));

Do not hesitate to take a look at how previous listed shortcuts have been implemented to get ideas.

Auto-completion

Another very useful tool when we are writing a readline is the auto-completion. It is fired when hiting the key if the auto-completer has been defined with the setAutocompleter method.

All the auto-completers must implement the Hoa\Console\Readline\Autocompleter\Autocompleter interface. Some of them are already distributed to help us in our development, such as Hoa\Console\Readline\Autocompleter\Word that will auto-complete the input based on a list of words. For example:

$rl = new Hoa\Console\Readline\Readline();
$rl->setAutocompleter(new Hoa\Console\Readline\Autocompleter\Word([
    'hoa',
    'console',
    'readline',
    'autocompleter',
    'autocompletion',
    'password',
    'awesome'
]));
var_dump($rl->readLine('> '));

Let's try to write something, then where we would like, we hit . If the text at the left of the cursor starts by h, then we will see hoa printed in the line because the auto-completer has no choice (it returns a string). If the auto-completer does not find any appropriated word, nothing will happen (it will return null). And finally, if it finds several words (it will return an array), then a menu will appear. Let's try to simply auto-complete a: the menu will propose autocompleter, autocompletion and awesome. Either we continue to hit and the menu will disappear, or we can move the cursor inside the menu with the , , , and keys, then to select a word. This behavior is quite natural.

In addition to the auto-completer on words, we find the auto-completer on paths with the Hoa\Console\Readline\Autocompleter\Path class. Based on a root and a file iterator, this auto-completer is able to auto-complete paths. If the root is undefined, it will be set to the current working directory. At each auto-completion, a new instance of the file iterator is created by a factory. This latter receives the path to iterate as a unique argument. By default, the factory is defined by the getDefaultIteratorFactory static method on Hoa\Console\Readline\Autocompleter\Path. The factory builds a file iterator of type DirectoryIterator. Each value computed by the iterator must be an object of type SplFileInfo. Thus, to auto-complete all the files and directories from the hoa://Library/Console root, we will write:

$rl->setAutocompleter(
    new Hoa\Console\Readline\Autocompleter\Path(
        resolve('hoa://Library/Console')
    )
);

Using a factory offers a lot of flexibility and allows us to use any file iterator, such as Hoa\File\Finder (please, see the Hoa\File library). Thus, to only auto-complete not hidden files and directories that have been modified the last 6 months and sorted by their size, we will write:

$rl->setAutocompleter(
    new Hoa\Console\Readline\Autocompleter\Path(
        resolve('hoa://Library/Console'),
        function ($path) {
            $finder = new Hoa\File\Finder();
            $finder->in($path)
                   ->files()
                   ->directories()
                   ->maxDepth(1)
                   ->name('#^(?!\.).#')
                   ->modified('since 6 months')
                   ->sortBySize();

            return $finder;
        }
    )
);

We can replace the local file iterator by another totally different iterator: on files that are stored in another machine, a third party service or even resources that are not files but have path-like URI.

Finally, we can aggregate several auto-completers together thanks to the Hoa\Console\Readline\Autocompleter\Aggregate class. The declaration order of auto-completers is important: the first one that matches a word to auto-complete will break the loop over all the auto-completers. Thus, to auto-complete paths and words, we will write:

$rl->setAutocompleter(
    new Hoa\Console\Readline\Autocompleter\Aggregate([
        new Hoa\Console\Readline\Autocompleter\Path(),
        new Hoa\Console\Readline\Autocompleter\Word($words)
    ])
);

The getAutocompleters method from Hoa\Console\Readline\Autocompleter\Aggregate returns an ArrayObject object for more flexibility. We can consequently add or delete auto-completers after having declared them in the constructor.

Example of an aggregation of the Hoa\Console\Readline\Autocompleter\Path auto-completer with Hoa\Console\Readline\Autocompleter\Word.

Reading options

A big benefit of programs with a command line interface is their flexibility. They are dedicated to one (little) task and we are able to parameterize them thanks to options that they exposed. The reading of those options must be very simple and straightforward because this is a repetitive and subtle task. The Hoa\Console\Parser and Hoa\Console\GetOption classes act as a duo to solve this problem.

Analyzing options

We start with Hoa\Console\Parser that analyzes options given to a program. Whatever the options we would like to have, we just analyze them for now. Let's start by using the parse method:

$parser = new Hoa\Console\Parser();
$parser->parse('-s --long=value input');

print_r($parser->getSwitches());
print_r($parser->getInputs());

/**
 * Will output:
 *     Array
 *     (
 *         [s] => 1
 *         [long] => value
 *     )
 *     Array
 *     (
 *         [0] => input
 *     )
 */

Let's see of what a command line is composed. We have two categories: the options (switches) and the inputs. The inputs are everything that is not an option. An option can have two forms: short if it has only one character or long if it has many.

Thus, -s is a short option and --long is a long option. However, we have to consider the number of hyphen in front of the option: with two hyphens, it will always be a long option, with one hyphen, it depends. There is two schools that differenciate themselves with one parameter: long only. Let's take an example: -abc is considered as -a -b -c if the long only parameter is set to false, else it will be equivalent to a long option, such as --abc. Most of the time, this parameter is set to false by default and Hoa\Console\Parser has adopted the position of the majority. To modify this parameter, we have to use the setLongOnly method, let's see though:

// long only is set to false.
$parser->parse('-abc');
print_r($parser->getSwitches());

$parser->setLongOnly(true);

// long only is set to true.
$parser->parse('-abc');
print_r($parser->getSwitches());

/**
 * Will output:
 *     Array
 *     (
 *         [a] => 1
 *         [b] => 1
 *         [c] => 1
 *     )
 *     Array
 *     (
 *         [abc] => 1
 *     )
 */

An option can be of two kinds: boolean or valued. If no value is associated, it is considered as a boolean. Thus, -s represents true, but -s -s represents false, -s -s -s represents true and so on. A boolean option behaves like a switch. A valued option has an associated value by using either a space or an equal sign (symbol =). Here is a non-exhaustive list of potential syntaxes (we use a short option but it could be a long one):

The simple (symbol ') and double (symbol ") quotes are supported. But be aware that there is particular syntaxes which are still not standards:

A l'instar of boolean options that behave like switches, the valued options rewrite their values if they are declared more than once. Thus, -a=b -a=c represents c.

Finally, there is values that are considered as special. We distinguish two of them:

Without any specific manipulation, those values will not be considered as special. We will have to use the Hoa\Console\Parser::parseSpecialValue method as we will see bellow.

Read options and inputs

We know how to analyze options but this is not enough to read them correctly. We have to define a little semantics: what do their expect, what is their nature etc. We will use the Hoa\Console\GetOption class. An option is defined by:

These three informations must be specified. They must be given to the constructor of Hoa\Console\GetOption as first argument. The second one is the option analyzer (the analysis must be already done). Thus, we describe two options: extract that is a boolean option and directory that is a valued option:

$parser = new Hoa\Console\Parser();
$parser->parse('-x --directory=value inputA inputB inputC');

$options = new Hoa\Console\GetOption(
    [
        // long name                  type                  short name
        //   ↓                         ↓                         ↓
        ['extract',   Hoa\Console\GetOption::NO_ARGUMENT,       'x'],
        ['directory', Hoa\Console\GetOption::REQUIRED_ARGUMENT, 'd']
    ],
    $parser
);

We are now ready to read our options! The options reader behaves like an iterator, or like a pipette though, thanks to the getOption method. This method returns the short name of the option currently read and will assign the value of the option (a boolean or a string) to its first argument passed by reference. When the pipette is empty, the getOption method will return false. This structure might seem original but it is widely spread, you will not be lost when seeing it in other progams (examples in Linux, in FreeBSD or in Mac OS X —same code base—). The simplest way to read options are to define them default values and then to use getOption, thus:

$extract   = false;
$directory = '.';

//          short name                  value
//               ↓                        ↓
while (false !== $c = $options->getOption($v)) {
    switch($c) {
        case 'x':
            $extract = $v;

            break;

        case 'd':
            $directory = $v;

            break;
    }
}

var_dump($extract, $directory);

/**
 * Will output:
 *     bool(true)
 *     string(5) "value"
 */

We read it like this: “while we have an option to read, we get the short name in $c and its value in $v, then we see what to do”.

To read inputs, we will use the Hoa\Console\Parser::listInputs whose all arguments (a total of 26) are passed by reference. Thus:

$parser->listInputs($inputA, $inputB, $inputC);

var_dump($inputA, $inputB, $inputC);

/**
 * Will output:
 *     string(6) "inputA"
 *     string(6) "inputB"
 *     string(6) "inputC"
 */

Attention, this approach implies that the inputs are ordered (as usualy most of the time). But also, reading the inputs without having formely given the analyzer to Hoa\Console\GetOption can lead to unexpected results (because by default, all options are considered as booleans). If we would like all the inputs and analyze them manually if they are not ordered, we can use the Hoa\Console\Parser::getInputs method which will return all the inputs.

Special or ambiguous options

Back to our Hoa\Console\Parser::parseSpecialValue method. It takes two arguments: a value and an array of keywords. We re-use our example and modify the case of the d option:

while (false !== $c = $options->getOption($v)) {
    switch($c) {
        case 'x':
            $extract = $v;

            break;

        case 'd':
            $directory = $parser->parseSpecialValue($v, ['HOME' => '/tmp']);

            break;
    }
}

print_r($directory);

If we try with -d=a,b,HOME,c,d then -d will have the following value:

/**
 * Array
 * (
 *     [0] => a
 *     [1] => b
 *     [2] => /tmp
 *     [3] => c
 *     [4] => d
 * )
 */

Finally, when a read option does not exist but it is close to an existing option modulo some typos (for example --dirzctory instead of --directory), we can use the __ambiguous case to capture and compute it:

while (false !== $c = $options->getOption($v)) {
    switch($c) {
        case 'x':
            $extract = $v;

            break;

        case 'd':
            $directory = $parser->parseSpecialValue($v, ['HOME' => '/tmp']);

            break;

        case '__ambiguous':
            print_r($v);

            break;
    }
}

The value (in $v) is an array with three entries. For example with --dirzctory, we have:

/**
 * Array
 * (
 *     [solutions] => Array
 *         (
 *             [0] => directory
 *         )
 *
 *     [value] => y
 *     [option] => dirzctory
 * )
 */

The solutions key provides all the similar options, the value key gives the value of the option and option is the original read name. It is part to the user to decide what to do based on these informations. We can use the Hoa\Console\GetOption::resolveOptionAmbiguity method by giving it the array, and it will choose the best option if it exists:

    case '__ambiguous':
        $options->resolveOptionAmbiguity($v);

        break;

It is preferable to advise the user that an ambiguity happened and ask her a decision. Sometimes, it can be dangerous to make decision in her place.

Integrate a router and a dispatcher

So far, we coerced the options and the inputs to the analyzer. Hoa\Router\Cli allows to extract data from a command line program. One method interests us: Hoa\Router\Cli::getURI will give us all the options and inputs of the running program, that we will provide to our analyzer. Thus:

$parser = new Hoa\Console\Parser();
$parser->parse(Hoa\Router\Cli::getURI());

// …

Now, it is possible to interprete the options that we will give to our program. If you have written the tests in a file named Test.php, then you will be able to write:

$ php Test.php -x -d=a,b,HOME,c,d inputA inputB
bool(true)
Array
(
    [0] => a
    [1] => b
    [2] => /tmp
    [3] => c
    [4] => d
)
string(6) "inputA"
string(6) "inputB"
NULL

The -x option is set to true, the -d option is an array (because we have analyzed it with the Hoa\Console\Parser::parseSpecialValue method), and we have inputA, inputB and null as inputs.

This is a good start and we can stop here most of the time. But it is possible to go further by using a dispatcher: writing commands in several functions or classes and call them regarding the given options and inputs to our program. We recommend to read the source code of hoa://Library/Cli/Bin/Hoa.php to help yourself, along with the Hoa\Router and Hoa\Dispatcher chapters. We propose a quick example without giving too much details about those libraries.

The idea is as follows. Thanks to Hoa\Router\Cli we are going to extract data of the following form: $ php script.php controller tail, where controller will be the name of the controller (of a class) on which we will call the main action (it means the main method with its default parameters) and where tail represents the options and the inputs. The name of the controller is identified by the special _call variable (from Hoa\Router\Cli) and the options along with the inputs are identified by _tail (from Hoa\Dispatcher\Kit). The options and the inputs are not mandatory. Then, we will use Hoa\Dispatcher\Basic with the dedicated kit of terminals, namely Hoa\Console\Dispatcher\Kit. The dispatcher will try to load the Application\Controller\controller class by default, and the autoloader will load them from the hoa://Application/Controller/controller directory. We will then set quickly where the application is located. Finally, the exit code of our program will be given by the value returned by our controller and action. If an error occurred, we will print it and force an exit code greater than zero. Thus:

try {
    // Prepare the router.
    $router = new Hoa\Router\Cli();
    $router->get(
        'g',
        '(?<_call>\w+)(?:\s+(?<_tail>.+))?'
    );

    // Prepare the dispatcher.
    $dispatcher = new Hoa\Dispatcher\ClassMethod([
        'synchronous.call' => 'Application\Controller\(:call:U:)',
        'synchronous.able' => 'main'
    ]);
    $dispatcher->setKitName('Hoa\Console\Dispatcher\Kit');

    // Dispatch!
    exit($dispatcher->dispatch($router));
} catch (Hoa\Exception $e) {
    echo $e->raise(true);
    exit($e->getCode() + 1);
}

At the same level of our program, let's create the Application/Controller/ directory with the Foo.php file inside, which will contain the following code:

<?php

namespace Application\Controller;

class Foo extends \Hoa\Console\Dispatcher\Kit
{
    protected $options = [
        ['extract',   \Hoa\Console\GetOption::NO_ARGUMENT,       'x'],
        ['directory', \Hoa\Console\GetOption::REQUIRED_ARGUMENT, 'd'],
        ['help',      \Hoa\Console\GetOption::NO_ARGUMENT,       'h']
    ];

    public function MainAction()
    {
        $extract   = false;
        $directory = '.';

        while (false !== $c = $this->getOption($v)) {
            switch($c) {
              case 'x':
                  $extract = $v;

                  break;

              case 'd':
                  $directory = $this->parser->parseSpecialValue($v, ['HOME' => '/tmp']);

                  break;

              case 'h':
                  return $this->usage();
            }
        }

        echo 'extract:   ';
        var_dump($extract);
        echo 'directory: ';
        print_r($directory);

        return;
    }

    public function usage()
    {
        echo
            'Usage   : foo <options>', "\n",
            'Options :', "\n",
            $this->makeUsageOptionsList([
                'x' => 'Whether we need to extract.',
                'd' => 'Directory to extract.',
                'h' => 'This help.'
            ]);
    }
}

Our class extends our kit to benefit from the provided methods. Among other things, its own getOption method that will exploit the $options attribute where the options are declared, makeUsageOptionsList to print a usage, its own resolveOptionAmbiguity method that asks a confirmation from the user, the router access through the $router attribute etc. Kits offer services to the application, they aggregate services offered by the libraries. Now, let's test:

$ php Test.php foo -x -d=1:3
extract:   bool(true)
directory: Array
(
    [0] => 1
    [1] => 2
    [2] => 3
)

Awesome!

Let's note that the hoa script is exactly build like this. Do not hesitate to find inspiration from it.

Processus

In our context, a processus is a classical program that is executed in a terminal. The interesting part is that such a program communicates with the rest of its environment thanks to pipes, numbered from zero. Some of them have even names:

When a processus is executed in a terminal, STDIN will use the keyboard as the source of data, and STDOUT with STDERR are linked to the window of the terminal. But when a processus is executed in a sub-terminal, it means executed from another processus, STDIN is no longer linked to the keyboard, like STDOUT and STDERR are not linked to the screen. This is the parent processus that will read and write on these streams to interact with the “sub”-processus. This mechanism is called a redirection of stream, we use it very often when we write a command line (please, see the section Redirections from the Bash Reference Manual). What we are going to do use another syntax but the mechanism is exactly the same.

It is very important to know that these streams are all asynchronous from each other. Not one of these streams will have an impact on another one, there is no link between them and that is very important for the rest of this section.

From the PHP level, it is possible to access to these streams by using respectively the following URI: php://stdin, php://stdout and php://stderr. However, we also have the eponymous STDIN, STDOUT and STDERR constants. They are defined as follows (example with STDIN):

define('STDIN', fopen('php://stdin', 'r'));

These streams are available if and only if the program is executed from a command line. Remind that pipes are identified by numbers. We are then able to use php://fd/0 to represent STDIN, php://fd/1 to represent STDOUT etc. The php://fd/i URI allows to access the file with the i descriptor.

Very basic execution

The Hoa\Console\Processus class provides a very quick way to execute a processus and get the result of STDOUT. This is the most common case. Thus, we will use the execute static method:

var_dump(Hoa\Console\Processus::execute('id -u -n'));

/**
 * Could output:
 *     string(3) "hoa"
 */

By default, the command will be escaped for security reasons. If you are confident in the command, you can desactivate the escaping by setting the second argument to false.

We have no control on the pipes and even if it is suitable for the majority of cases, this is not enough when we would like a minimum of interaction with the processus.

Reading and writing

Let's see how to interact with a processus. We will consider the following LittleProcessus.php program:

<?php

$range = range('a', 'z');

while (false !== $line = fgets(STDIN)) {
    echo '> ', $range[intval($line)], "\n";
}

To test and understand its behavior, let's write the following command line and hit 3 and 4 on the keyboard:

$ php LittleProcessus.php
3
> d
4
> e

We can also write:

$ seq 0 4 | php LittleProcessus.php
> a
> b
> c
> d
> e

Our program will read each line on the standard input, considering it is a number and transforming it into a character that will be printed on the standard output. We would like to execute this program by giving it our own custom list of numbers (like the seq program does) and observe the produced result.

An instance of the Hoa\Console\Processus class represents a processus. During the instanciation, we can set:

There is other arguments but we will see them further.

The description of pipes has the form of an array where each key represents the number of the pipe (more generally, this is the i from php://fd/i) and its value is also an array describing the nature of the pipe: either a “real” pipe, or a file with their reading or writing mode (among r, w or a). Let's illustrate with an example:

$processus = new Hoa\Console\Processus(
    'php',
    ['LittleProcessus.php'],
    [
        // STDIN.
        0 => ['pipe', 'r'],
        // STDOUT.
        1 => ['file', '/tmp/output', 'a']
    ]
);

In this case, STDIN is a pipe and STDOUT is the /tmp/output file. If we do not set a descriptor, it will be equivalent to write:

$processus = new Hoa\Console\Processus(
    'php',
    ['LittleProcessus.php'],
    [
        // STDIN.
        0 => ['pipe', 'r'],
        // STDOUT.
        1 => ['pipe', 'w'],
        // STDERR.
        2 => ['pipe', 'w']
    ]
);

Each pipe is identified as a stream and can be manipulated as such. When a pipe is in reading (with the r mode), it means that the processus will read from it. So we, the parent processus, will write on this pipe. Let's take the example of STDIN: the processus reads from STDIN what the keyboard has written on it. And conversely, when a pipe is in writing (with the w mode), it means that we will read from it. Let's take the example of STDOUT: the screen will read what the processus has written to it.

The Hoa\Console\Processus class extends the Hoa\Stream class, and consequently, we have all the necessary tools to read and write on the pipe of our choice. This class also provides several listeners:

Let's take directly an example. We will execute the php LittleProcessus.php processus and attach functions to the following listeners: input to write a sequence of numbers and output to read the result.

$processus = new Hoa\Console\Processus('php LittleProcessus.php');
$processus->on('input', function ($bucket) {
    $source = $bucket->getSource();
    $data   = $bucket->getData();

    echo 'INPUT (', $data['pipe'], ')', "\n";

    $source->writeAll(
        implode("\n", range($i = mt_rand(0, 21), $i + 4)) . "\n"
    );

    return false;
});
$processus->on('output', function ($bucket) {
    $data = $bucket->getData();

    echo 'OUTPUT (', $data['pipe'], ') ', $data['line'], "\n";

    return;
});
$processus->run();

/**
 * Could output:
 *     INPUT (0)
 *     OUTPUT (1) > s
 *     OUTPUT (1) > t
 *     OUTPUT (1) > u
 *     OUTPUT (1) > v
 *     OUTPUT (1) > w
 */

Now, let's see the details to understand well.

When a reading stream is ready, the input listener will be fired. Only one data is sent: pipe, that contains the number of the pipe (the i of php://fd/i). When a writing stream is ready, the output listener is fired. Two data are sent: pipe (like input) and line that contains the received line.

We see in the function attached to the input listener that we write a sequence of numbers concatenated by \n (one number per line). In order to achieve this, we use the writeAll method. By default, the writing methods write on the 0 pipe. To change this behavior, we will have to set the number of the pipe as the second argument of the writing methods. Same for the reading methods but the default pipe is 1.

When the callable attached to a listener returns false, the pipe that has fired this call will be closed just after. In our case, the function attached to input returns false just after having written the data, we no longer need this pipe. It is important for performance reasons to close pipes as soon as possible.

Finally, to execute the processus, we use the Hoa\Console\Processus::run method with a null arity.

In our example, we write all data at once but we can send them as soon as they are ready, which is more efficient because the processus does no wait a big bucket of data and can compute them gradually. Let's modify our example to write a data each time STDIN is ready:

$processus->on('input', function ($bucket) {
    static $i = null;
    static $j = 5;

    if (null === $i) {
        $i = mt_rand(0, 20);
    }

    $data = $bucket->getData();

    echo 'INPUT (', $data['pipe'],')', "\n";

    $source = $bucket->getSource();
    $source->writeLine($i++);
    usleep(50000);

    if (0 >= $j--) {
        return false;
    }

    return;
});

We initialize two variables: $i and $j, that hold the number to send and the maximum number of data to send. We introduce a voluntary latency with usleep(50000) in order that STDOUT to be ready, only to illustrate our example. In this case, the output would be:

/** Could output:
 *     INPUT (0)
 *     OUTPUT (1) > h
 *     INPUT (0)
 *     OUTPUT (1) > i
 *     INPUT (0)
 *     OUTPUT (1) > j
 *     INPUT (0)
 *     OUTPUT (1) > k
 *     INPUT (0)
 *     OUTPUT (1) > l
 *     INPUT (0)
 *     OUTPUT (1) > m
 */

The processus is waiting an input and read data when they are received. Once we have sent all the data, we close the pipe.

The processus will close itself. We have the Hoa\Console\Processus::getExitCode method to know the exit code of the processus. Attention, a 0 code represents a success. Because it is a common error, there is the Hoa\Console\Processus::isSuccessful method to know if the processus has been executed with success or not.

Detect the type of pipes

Sometimes, it is useful to know the type of pipes, it means if it is a direct (regular) use, a pipe or a redirection. We will make use of the Hoa\Console\Console class and its isDirect, isPipe and isRedirection static methods to get these informations.

Let's take an example to understand. Let's write the Type.php file that will study the type of STDOUT:

echo 'is direct:      ';
var_dump(Hoa\Console\Console::isDirect(STDOUT));

echo 'is pipe:        ';
var_dump(Hoa\Console\Console::isPipe(STDOUT));

echo 'is redirection: ';
var_dump(Hoa\Console\Console::isRedirection(STDOUT));

And now, let's execute this file to see the result:

$ php Type.php
is direct:      bool(true)
is pipe:        bool(false)
is redirection: bool(false)

$ php Type.php | xargs -I@ echo @
is direct:      bool(false)
is pipe:        bool(true)
is redirection: bool(false)

$ php Type.php > /tmp/foo; cat /tmp/foo
is direct:      bool(false)
is pipe:        bool(false)
is redirection: bool(true)

In the first case, STDOUT is direct (for STDOUT, it means that it is linked to the screen, for STDIN, it will be linked to the keyboard etc.). In the second case, STDOUT is a pipe, it means that it is attached to the STDIN of the command written after the | symbol. In the last case, STDOUT is a redirection, it means that it is redirected in the /tmp/foo file (that we print just after). The operation can be done on STDIN, STDERR or any other resource.

Knowing the type of pipes can allow different behaviors according to the context. For example, Hoa\Console\Readline\Readline reads on STDIN. If its type is a pipe or a redirection, the advanced edition mode of the line will be disabled and it will return false when there will be no more things to read. Another example, the commands verbosity of the hoa script uses the type of STDOUT as the default value: direct to be verbose, else not verbose. Try the following examples to see the difference:

$ hoa --no-verbose
$ hoa | xargs -I@ echo @

Examples are numerous but be careful to use this feature with precautions. We have to adapt the behaviors but keep them consistent.

Execution conditions

The processus is executed in a particular directory and a particular environment. This directory is called the current working directory, often abbreviated cwd. It defines the directory where the processus will be executed. We can find it in PHP with the getcwd function. The environment is defined by an array, which can be retrieved for example by executing /usr/bin/env. For example, the PATH is defined in this environment. These data are given as fourth and fifth arguments of the Hoa\Console\Processus constructor. Thus:

$processus = new Hoa\Console\Processus(
    'php',
    null, /* no option         */
    null, /* use default pipes */
    '/tmp',
    [
        'FOO'  => 'bar',
        'BAZ'  => 'qux',
        'PATH' => '/usr/bin:/bin'
    ]
);
$processus->on('input', function (Hoa\Event\Bucket $bucket) {
    $bucket->getSource()->writeAll(
        '<?php' . "\n" .
        'var_dump(getcwd());' . "\n" .
        'print_r($_ENV);'
    );

    return false;
});
$processus->on('output', function (Hoa\Event\Bucket $bucket) {
    $data = $bucket->getData();
    echo '> ', $data['line'], "\n";

    return;
});
$processus->run();

/**
 * Will output:
 *     > string(12) "/tmp"
 *     > Array
 *     > (
 *     >     [FOO] => bar
 *     >     [PATH] => /usr/bin:/bin
 *     >     [PWD] => /tmp
 *     >     [BAZ] => qux
 *     >     [_] => /usr/bin/php
 *     >
 *     > )
 */

If the current working directory is not defined, we will use the one of the program. If the environment is not defined, the processus will use the one of its parent.

We are also able to set a maximum time in seconds in order to get a response from the processus (defined to 30 seconds by default). This is the last argument of the constructor. We can use the Hoa\Console\Processus::setTimeout method. To know when this time is reached, we must use the timeout listener. No action will be done automatically. We can for example terminate the processus thanks to the Hoa\Console\Processus::terminate method. Thus:

$processus = new Hoa\Console\Processus('php');

// 3 seconds is enough…
$processus->setTimeout(3);

// Sleep 10 seconds.
$processus->on('input', function (Hoa\Event\Bucket $bucket) {
    $bucket->getSource()->writeAll('<?php sleep(10);');

    return false;
});

// Terminate the processus on timeout.
$processus->on('timeout', function (Hoa\Event\Bucket $bucket) {
    echo 'TIMEOUT, terminate', "\n";
    $bucket->getSource()->terminate();

    return;
});

$processus->run();

/**
 * Will output (after 3 secondes):
 *     TIMEOUT, terminate
 */

No action is done automatically because they can be numerous. Maybe we can unblock the processus, or close it to open other ones, emit some logs etc.

About the terminate method, it can take several different values, defined by the constants of Hoa\Console\Processus: SIGHUP, SIGINT, SIGQUIT, SIGABRT, SIGKILL, SIGALRM and SIGTERM (by default). Several signals can be send to the processus in order to stop them. To get the detail, please, see the signal page.

Miscellaneous

The getTitle and setTitle static methods on the Hoa\Console\Processus class respectively allow to get and set the title of the processus. Thus:

Hoa\Console\Processus::setTitle('hoa #1');

And in another terminal:

$ ps | grep hoa
69578 ttys006    0:00.01 hoa #1
70874 ttys008    0:00.00 grep hoa

These methods are really useful when we manipulate a lot of processus and we would like to identify them efficiently (for example with tools like top or ps). Note that they are working only if you have at least PHP5.5.

Another interesting static method is Hoa\Console\Processus::locate that allows to determine the path to a given program. For example:

var_dump(Hoa\Console\Processus::locate('php'));

/**
 * Could output:
 *     string(12) "/usr/bin/php"
 */

In the case where the program is not found, null will be returned. This method is based on the PATH of your system.

Interactive processus and pseudo-terminals

This section is a little bit technical but it explains a problem that can be met with some processus called interactive.

The Hoa\Console\Processus class allows to automate the interaction with processus very easily. However, it is not always possible to create such an automation because of the behavior of the processus. We will illustrate the problem by writing the Interactive.php file:

<?php

echo 'Login: ';

if (false === $login = fgets(STDIN)) {
    fwrite(STDERR, 'Hmm, no login.' . "\n");
    exit(1);
}

echo 'Password: ';

if (false === $password = fgets(STDIN)) {
    fwrite(STDERR, 'Hmm, no password.' . "\n");
    exit(2);
}

echo 'Result:', "\n\t", $login, "\t", $password;

Let's execute this processus to see what it does:

$ php Interactive.php
Login: myLogin
Password: myPassword
Result:
    myLogin
    myPassword

And now, let's automate the execution of this processus:

$ echo 'myLogin\nmyPassword' > data
$ php Interactive.php < data
Login: Password: Result:
    myLogin
    myPassword

Excellent. We could easily get the same result with Hoa\Console\Processus. Now, if our processus want to ensure that STDIN is empty between two inputs, it can add:

}

fseek(STDIN, 0, SEEK_END);

echo 'Password: ';

And then in this case, if we try to automate the execution:

$ php Interactive.php < data
Login: Password: Hmm, no password.

This is absolutely a normal behavior, but Hoa\Console\Processus can do nothing to solve this issue.

The solution would be to use a pseudo-terminal by using the PTY functions (please, see in Linux or in FreeBSD). Unfortunately these functions are not available in PHP for technical reasons. There is no possible solutions in pure PHP, but it is still conceivable to use an external program, written in C for example.

Conclusion

The Hoa\Console library provides comprehensive tools to write programs tailored to a textual interface, whether it be for interaction with the window or the cursor, the interaction with the user thanks to a very customizable readline (with autocompletion and shortcuts), the options reading for programs themselves, the construction of elaborated programs, or even the execution, interaction and communication with processus.

An error or a suggestion about the documentation? Contributions are welcome!

Comments

menu