File: /var/www/vhost/disk-apps/magento.bikenow.co/vendor/hoa/console/Documentation/Fr/Index.xyl
<?xml version="1.0" encoding="utf-8"?>
<overlay xmlns="http://hoa-project.net/xyl/xylophone">
<yield id="chapter">
<p>Le terminal est une <strong>interface</strong> très
<strong>puissante</strong> qui repose sur de multiples concepts.
<code>Hoa\Console</code> permet d'écrire des <strong>outils</strong> adaptés à
ce type d'environnement.</p>
<h2 id="Table_of_contents">Table des matières</h2>
<tableofcontents id="main-toc" />
<h2 id="Introduction" for="main-toc">Introduction</h2>
<p>De nos jours, nous comptons deux types d'interfaces :
<strong>textuelle</strong> et <strong>graphique</strong>. L'interface
textuelle existe depuis l'origine des ordinateurs, alors appelés
<strong>terminaux</strong>. Cette interface, malgré son aspect « brut », est
fonctionnellement très <strong>puissante</strong> grâce à plusieurs concepts
comme par exemple la ligne de commande ou les <em lang="en">pipes</em>.
Aujourd'hui, elle est encore très utilisée car elle est souvent plus rapide
pour exécuter des tâches <strong>complexes</strong> qu'une interface
graphique. Elle peut être aussi très facilement utilisée à travers des réseaux
ou sur des machines à faibles ressources. Bref, cette interface est toujours
<strong>incontournable</strong>.</p>
<p>Du point de vue de l'utilisateur, il y a trois niveaux à considérer :</p>
<ul>
<li>l'<strong>interface</strong> : afficher et éditer du texte, manipuler la
fenêtre, le curseur etc. ;</li>
<li>le <strong>programme</strong> : interagir avec l'utilisateur avec un
maximum de confort, utiliser la ligne de commande à son plein potentiel,
construire des programmes adaptés à ce type d'interface ;</li>
<li>l'<strong>interaction</strong> avec d'autres programmes : interagir
automatiquement et communiquer avec d'autres programmes.</li>
</ul>
<p>La bibliothèque <code>Hoa\Console</code> propose des outils pour répondre à
ces trois niveaux de problématique. Pour cela, elle se base sur des
<strong>standards</strong>, comme
l'<a href="http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-048.pdf">ECMA-48</a>
qui spécifie la communication avec le système à travers des suites de
caractères ASCII et des codes de contrôle (aussi appelés séquences
d'échappement), ce afin de manipuler la fenêtre, le curseur ou des
périphériques de la machine. D'autres fonctionnalités sont aussi standards
comme la manière de lire des options depuis un programme, très
<strong>inspirée</strong> de systèmes comme
<a href="http://linux.org/">Linux</a>, <a href="http://freebsd.org/">FreeBSD</a> ou
encore <a href="https://en.wikipedia.org/wiki/UNIX_System_V">System V</a>.
D'ailleurs, si vous êtes familier avec plusieurs bibliothèques C, vous ne
serez pas déroutés. Et <em>a contrario</em>, si vous apprenez à utiliser
<code>Hoa\Console</code>, vous ne serez pas perdus en retournant sur des
langages de plus bas niveaux comme le C.</p>
<p>Avant de commmencer, nous aimerions ajouter une petite note
<strong>uniquement</strong> à propos de la gestion de la fenêtre et du
curseur. Aujourd'hui, nous avons le choix entre <strong>plusieurs</strong>
terminaux par système et certains sont plus complets que d'autres. Par
exemple, <a href="https://windows.microsoft.com/">Windows</a> et son terminal
par défaut, le <a href="http://en.wikipedia.org/wiki/MS-DOS">MS-DOS</a>, ne
respecte aucun standard. Dans ce cas, oubliez le standard ECMA-48 et
tournez-vous vers
<a href="http://msdn.microsoft.com/library/ms682087.aspx"
title="Console Reference">la bibliothèque <code>Wincon</code></a>. Il est
souvent recommandé d'utiliser une machine Unix <strong>virtuelle</strong> ou
un <strong>émulateur</strong> de terminal, comme
<a href="http://ttssh2.sourceforge.jp/">TeraTerm</a>, très complet. Même sur
des systèmes proches de la famille BSD, les terminaux distribués par défaut ne
supportent pas tous les standards. C'est le cas de Mac OS X, où nous vous
conseillons d'utiliser <a href="http://iterm2.com">iTerm2</a> au lieu de
Terminal. Enfin, sur d'autres systèmes de la famille Linux ou BSD, nous
conseillons
<a href="http://software.schmorp.de/pkg/rxvt-unicode.html">urxvt</a>. Pour
les autres fonctionnalités, comme la lecture en ligne, la lecture d'options,
les processus etc., <code>Hoa\Console</code> est parfaitement
<strong>compatible</strong> et fonctionnel.</p>
<h2 id="Window" for="main-toc">Fenêtre</h2>
<p>La fenêtre d'un terminal doit être vue comme un <strong>canevas</strong> de
<strong>colonnes</strong> et de <strong>lignes</strong>. La classe
<code>Hoa\Console\Window</code> permet de manipuler la
<strong>fenêtre</strong> du terminal et son <strong>contenu</strong> à travers
des méthodes statiques.</p>
<h3 id="Size_and_position" for="main-toc">Taille et position</h3>
<p>Les premières opérations élémentaires concernent la <strong>taille</strong>
et la <strong>position</strong> de la fenêtre, grâce aux méthode
<code>setSize</code>, <code>getSize</code>, <code>moveTo</code> et
<code>getPosition</code>. La taille se définie avec les unités
<em>colonne</em> × <em>ligne</em> et la position se définie en pixels.
Ainsi :</p>
<pre><code class="language-php">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
* )
*/</code></pre>
<p>Nous remarquerons que la fenêtre se redimensionne <strong>toute
seule</strong>. Ni la taille ni la position de la fenêtre ne sont stockées en
mémoire, elles sont calculées à chaque appel de la méthode
<code>getSize</code> et <code>getPosition</code>. Attention, l'axe <em>y</em>
de la position de la fenêtre se calcule depuis <strong>le bas</strong> de
l'écran et non pas depuis le haut de l'écran comme nous pourrions nous y
attendre !</p>
<p>Il est aussi possible d'écouter l'<strong>événement</strong>
<code>hoa://Event/Console/Window:resize</code> qui est lancé à chaque fois que
la fenêtre est redimensionnée : soit manuellement, soit avec la méthode
<code>setSize</code>. Nous avons besoin de deux choses pour que cet événement
fonctionne :</p>
<ol>
<li><a href="http://php.net/pcntl">l'extension <code>pcntl</code></a> doit
être activée ;</li>
<li>nous devons utiliser
<a href="http://php.net/declare">la structure <code>declare</code></a> pour
que <a href="http://php.net/pcntl_signal">la fonction
<code>pcntl_signal</code></a> fonctionne correctement.</li>
</ol>
<p>Pour mettre le programme en attente passive, nous allons utiliser
<a href="http://php.net/stream_select">la fonction
<code>stream_select</code></a>, c'est un <strong>détail</strong> présent
uniquement pour tester notre code, sinon le programme se terminerait tout de
suite. Ainsi :</p>
<pre><code class="language-php">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);
}</code></pre>
<p>Lorsque nous modifions la taille de la fenêtre, nous verrons s'afficher par
exemple : <samp>New size (45, 67)</samp>, et ce pour chaque redimensionnement.
Cet événement est intéressant si nous voulons <strong>ré-adapter</strong>
notre présentation.</p>
<p>Enfin, nous pouvons minimiser ou restaurer la fenêtre grâce aux méthodes
statiques <code>Hoa\Console\Window::minimize</code> et
<code>Hoa\Console\Window::restore</code>. Par ailleurs, nous pouvons placer la
fenêtre en arrière-plan (derrière toutes les autres fenêtres) grâce à la
méthode statique <code>Hoa\Console\Window::lower</code>, tout comme nous
pouvons la placer en avant-plan avec <code>Hoa\Console\Window::raise</code>.
Par exemple :</p>
<pre><code class="language-php">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";</code></pre>
<h3 id="Title_and_label" for="main-toc">Titre et label</h3>
<p>Le <strong>titre</strong> d'une fenêtre correspond au texte affiché dans sa
<strong>barre</strong> supérieure, dans laquelle sont souvent placés les
contrôles de la fenêtre comme la maximisation, la minimisation etc. Le
<strong>label</strong> correspond au nom associé au <strong>processus</strong>
actuel. Nous trouvons les méthodes <code>setTitle</code>,
<code>getTitle</code> et <code>getLabel</code>, il n'est pas prévu de modifier
le label. Pour définir le titre du processus (ce que nous voyons avec la
commande <code>top</code> ou <code>ps</code> par exemple), il faudra se
référer à <code>Hoa\Console\Processus::setTitle</code> et à
<code>Hoa\Console\Processus::getTitle</code> pour l'obtenir. Ainsi :</p>
<pre><code class="language-php">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"
*/</code></pre>
<p>Encore une fois, le titre et le label ne sont pas stockés en mémoire, ils
sont calculés à chaque appel de méthode.</p>
<h3 id="Interact_with_the_content" for="main-toc">Interagir avec le
contenu</h3>
<p><code>Hoa\Console\Window</code> permet aussi de contrôler le
<strong>contenu</strong> de la fenêtre, ou du moins le
<em lang="en">viewport</em>, c'est à dire le contenu <strong>visible</strong>
de la fenêtre. Une seule méthode est actuellement disponible :
<code>scroll</code>, qui permet de <strong>déplacer</strong> le contenu vers
le haut ou vers le bas. Les arguments de cette méthode sont très simples :
<code>up</code> ou <code>↑</code> pour monter d'une ligne, et
<code>down</code> ou <code>↓</code> pour descendre d'une ligne. Nous pouvons
concaténer ces directions par un espace ou alors préciser le nombre de fois où
une direction sera répétée :</p>
<pre><code class="language-php">Hoa\Console\Window::scroll('↑', 10);</code></pre>
<p>En réalité, cette méthode va déplacer le contenu pour qu'il y ait
<em>x</em> lignes respectivement en-dessous ou au-dessus du curseur.
Attention, le curseur <strong>ne change pas</strong> de position !</p>
<p>Même si c'est très souvent inutile, il est possible de
<strong>rafraîchir</strong> la fenêtre, c'est à dire de refaire un rendu
complet. Nous pouvons nous aider de la méthode <code>refresh</code> toujour
sur <code>Hoa\Console\Window</code>.</p>
<p>Enfin, il est possible de placer un texte dans le
<strong>presse-papier</strong> de l'utilisateur à l'aide de la méthode
<code>copy</code> :</p>
<pre><code class="language-php">Hoa\Console\Window::copy('Foobar');</code></pre>
<p>Puis si l'utilisateur colle ce qui est dans son presse-papier, il verra
<samp>Foobar</samp> s'afficher.</p>
<h2 id="Cursor" for="main-toc">Curseur</h2>
<p>À l'intérieur d'une fenêtre, nous avons un curseur qui peut être vu comme
la <strong>pointe</strong> d'un stylo. La classe
<code>Hoa\Console\Cursor</code> permet de manipuler le
<strong>curseur</strong> du terminal à travers des méthodes statiques.</p>
<h3 id="Moving" for="main-toc">Déplacement</h3>
<p>Nous allons commencer par <strong>déplacer</strong> le curseur. Il se
déplace partout dans le <em lang="en">viewport</em>, c'est à dire le contenu
<strong>visible</strong> de la fenêtre du terminal, mais nous allons écrire un
peu de texte et nous déplacer dedans dans un premier temps. La méthode
<code>move</code> sur <code>Hoa\Console\Cursor</code> permet de déplacer le
curseur dans plusieurs <strong>directions</strong>. Tout d'abord de manière
<strong>relative</strong> :</p>
<ul>
<li><code>u[p]</code> ou <code>↑</code>, pour le déplacer à la ligne
supérieure ;</li>
<li><code>r[ight]</code> ou <code>→</code>, pour le déplacer à la colonne
suivante ;</li>
<li><code>d[own]</code> ou <code>↓</code>, pour le déplacer à la ligne
inférieure ;</li>
<li><code>l[eft]</code> ou <code>←</code>, pour le déplacer à la colonne
précédente.</li>
</ul>
<p>Nous trouvons aussi des déplacements <strong>semi-absolus</strong> :</p>
<ul>
<li><code>U[P]</code>, pour le déplacer à la première ligne du
<em lang="en">viewport</em> ;</li>
<li><code>R[IGHT]</code>, pour le déplacer à la dernière colonne du
<em lang="en">viewport</em> ;</li>
<li><code>D[OWN]</code>, pour le déplacer à la dernière ligne du
<em lang="en">viewport</em> ;</li>
<li><code>L[EFT]</code>, pour le déplacer à la première colonne du
<em lang="en">viewport</em>.</li>
</ul>
<p>Ces directions peuvent être concaténées par des espaces, ou alors nous
pouvons préciser le nombre de fois où une direction sera répétée.</p>
<pre><code class="language-php">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);</code></pre>
<p>Lors de l'exécution, nous verrons le curseur se déplacer <strong>tout
seul</strong> de « lettre en lettre » toutes les secondes.</p>
<p>Pour réellement déplacer le curseur de manière <strong>absolue</strong>,
nous utiliserons la méthode <code>moveTo</code> qui prend en argument des
coordonnées en <em>colonne</em> × <em>ligne</em> (la numérotation commence à 1
et non pas à 0). Nous en profitons pour parler de la méthode
<code>getPosition</code> qui permet de connaître la <strong>position</strong>
du curseur. Ainsi, si nous voulons déplacer le curseur à la colonne 12 et à la
ligne 7, puis afficher ces coordonnées, nous écrirons :</p>
<pre><code class="language-php">Hoa\Console\Cursor::moveTo(12, 7);
print_r(Hoa\Console\Cursor::getPosition());
/**
* Will output:
* Array(
* [x] => 12
* [y] => 7
* )
*/</code></pre>
<p>Enfin, il arrive très régulièrement que nous voulions déplacer le curseur
<strong>temporairement</strong> pour quelques opérations. Dans ce cas, il est
inutile de récupérer la position actuelle, le déplacer, puis le
repositionner ; nous pouvons profiter des méthodes <code>save</code> et
<code>restore</code>. Comme leur nom l'indique, ces méthodes respectivement
<strong>enregistre</strong> la position du curseur puis
<strong>restaure</strong> le curseur à la position précédemment enregistrée.
Ces fonctions ne manipulent pas de <strong>pile</strong>, il est impossible
d'enregistrer plus d'une seule position à la fois (le nouvel enregistrement
<strong>écrasera</strong> l'ancien). Ainsi, nous allons écrire un texte,
enregistrer la position du curseur, revenir en arrière et réécrire par dessus,
pour enfin revenir à notre position précédente :</p>
<pre><code class="language-php">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 '!';</code></pre>
<p>Le résultat final sera <samp>Hello World!</samp>. Nous remarquons qu'à
chaque fois qu'un caractère est écrit, le curseur se
<strong>déplace</strong>.</p>
<h3 id="Content" for="main-toc">Affichage</h3>
<p>Maintenant que le déplacement est acquis, nous allons voir comment
<strong>nettoyer</strong> des lignes et/ou des colonnes. Pour cela, nous nous
appuyons sur la méthode <code>clear</code> qui prend en argument les symboles
suivants (concaténés par un espace) :</p>
<ul>
<li><code>a[ll]</code> ou <code>↕</code>, pour nettoyer tout l'écran et
déplacer le curseur en haut à gauche du <em lang="en">viewport</em> ;</li>
<li><code>u[p]</code> ou <code>↑</code>, pour nettoyer toutes les lignes
au-dessus du curseur ;</li>
<li><code>r[ight]</code> ou <code>→</code>, pour nettoyer le reste de la
ligne à partir du curseur ;</li>
<li><code>d[own]</code> ou <code>↓</code>, pour nettoyer toutes les lignes
en-dessous du curseur ;</li>
<li><code>l[eft]</code> ou <code>←</code>, pour nettoyer du début de la
ligne jusqu'au curseur ;</li>
<li><code>line</code> ou <code>↔</code>, pour nettoyer toute la ligne et
déplacer le curseur en début de ligne.</li>
</ul>
<p>Ainsi, pour nettoyer <strong>toute une ligne</strong> :</p>
<pre><code class="language-php">Hoa\Console\Cursor::clear('↔');</code></pre>
<p>Le curseur peut aussi agir comme un <strong>pinceau</strong> et ainsi
écrire avec différentes <strong>couleurs</strong> ou différents
<strong>styles</strong> grâce à la méthode <code>colorize</code> (nous pouvons
tout mélanger en séparant chaque « commande » par des espaces). Commençons
par énumérer les styles :</p>
<ul>
<li><code>n[ormal]</code>, pour annuler tous les styles appliqués ;</li>
<li><code>b[old]</code>, pour écrire en gras ;</li>
<li><code>u[nderlined]</code>, pour avoir un texte souligné ;</li>
<li><code>bl[ink]</code>, pour avoir un texte qui clignote ;</li>
<li><code>i[nverse]</code>, pour inverser les couleurs d'avant et
d'arrière-plan ;</li>
<li><code>!b[old]</code>, pour annuler le gras ;</li>
<li><code>!u[nderlined]</code>, pour annuler le soulignement ;</li>
<li><code>!bl[ink]</code>, pour annuler le clignotement ;</li>
<li><code>!i[nverse]</code>, pour ne plus inverser les couleurs d'avant et
d'arrière-plan.</li>
</ul>
<p>Ces styles sont très classiques. Passons maintenant aux couleurs. Tout
d'abord, nous devons préciser si nous appliquons une couleur sur
l'<strong>avant-plan</strong> du texte, soit le texte lui-même, ou alors sur
son <strong>arrière-plan</strong>. Pour cela, nous allons nous aider
respectivement de la syntaxe <code>f[ore]g[round](<em>color</em>)</code> et
<code>b[ack]g[round](<em>color</em>)</code>. La valeur de
<code><em>color</em></code> peut être :</p>
<ul>
<li><code>default</code>, pour reprendre la couleur par défaut du
plan ;</li>
<li><code>black</code>, <code>red</code>, <code>green</code>,
<code>yellow</code>, <code>blue</code>, <code>magenta</code>,
<code>cyan</code> ou <code>white</code>, respectivement pour noir, rouge,
vert, jaune, bleu, magenta, cyan ou blanc ;</li>
<li>un numéro entre <code>0</code> et <code>256</code>, correspondant au
numéro de la couleur dans la palette des 256 couleurs ;</li>
<li><code>#<em>rrggbb</em></code> où <code><em>rrggbb</em></code> est un
nombre en hexadécimal correspondant au numéro de la couleur dans la palette
des 2<sup>64</sup> couleurs.</li>
</ul>
<p>Les terminaux manipulent <strong>une</strong> des deux palettes : 8
couleurs ou 256 couleurs. Chaque couleur est <strong>indexée</strong> à partir
de 0. Les noms des couleurs sont <strong>transformés</strong> vers leur index
respectif. Quand une couleur est précisée en hexadécimal, elle est
<strong>rapportée</strong> à la couleur la plus proche dans la palette
comportant 256 couleurs.</p>
<p>Ainsi, si nous voulons écrire <samp>Hello</samp> en jaune sur fond presque
rouge (<code>#932e2e</code>) et en plus souligné, puis <samp> world</samp>
mais non-souligné :</p>
<pre><code class="language-php">Hoa\Console\Cursor::colorize('fg(yellow) bg(#932e2e) underlined');
echo 'Hello';
Hoa\Console\Cursor::colorize('!underlined');
echo ' world';</code></pre>
<p>Enfin, il est possible de modifier les palettes de couleurs grâce à la
méthode <code>changeColor</code>, mais c'est à utiliser avec
<strong>précaution</strong>, cela peut perturber l'utilisateur. Cette méthode
prend en premier argument l'index de la couleur et en second argument sa
valeur en hexadécimal. Par exemple, <code>fg(yellow)</code> correspond à
l'index <code>33</code>, et nous voulons que ce soit maintenant totalement
bleu :</p>
<pre><code class="language-php">Hoa\Console\Cursor::changeColor(33, 0xf00);</code></pre>
<p>Toutefois, la palette de 256 couleurs est suffisamment
<strong>complète</strong> pour ne pas avoir besoin de modifier les
couleurs.</p>
<h3 id="Style" for="main-toc">Style</h3>
<p>Le curseur n'est pas forcément toujours visible. Lors de certaines
opérations, nous pouvons le <strong>cacher</strong>, effectuer nos
déplacements, puis le rendre à nouveau <strong>visible</strong>. Les méthodes
<code>hide</code> et <code>show</code>, toujours sur
<code>Hoa\Console\Cursor</code>, sont là pour ça :</p>
<pre><code class="language-php">echo 'Visible', "\n";
sleep(5);
echo 'Invisible', "\n";
Hoa\Console\Cursor::hide();
sleep(5);
echo 'Visible', "\n";
Hoa\Console\Cursor::show();
sleep(5);</code></pre>
<p>Il existe aussi trois <strong>types</strong> de curseurs, que nous pouvons
choisir avec la méthode <code>setStyle</code> :</p>
<ul>
<li><code>b[lock]</code> ou <code>▋</code>, pour un curseur en forme de
bloc ;</li>
<li><code>u[nderline]</code> ou <code>_</code>, pour un curseur en forme de
trait de soulignement ;</li>
<li><code>v[ertical]</code> ou <code>|</code>, pour un curseur en forme de
barre vertical.</li>
</ul>
<p>Cette méthode prend en second argument un booléen indiquant si le curseur
doit <strong>clignoter</strong> (valeur par défaut) ou pas. Ainsi, nous allons
faire tous les styles :</p>
<pre><code class="language-php">echo 'Block/steady: ';
Hoa\Console\Cursor::setStyle('▋', false);
sleep(3);
echo "\n", 'Vertical/blink: ';
Hoa\Console\Cursor::setStyle('|', true);
sleep(3);
// etc.</code></pre>
<p>Souvent le curseur indique des <strong>zones</strong> ou éléments
d'<strong>interactions</strong> différents, comme le pointeur de la
souris.</p>
<h3 id="Sound" for="main-toc">Son</h3>
<p>Le curseur est aussi capable d'émettre un petit « bip », souvent pour
<strong>attirer</strong> l'attention de l'utilisateur. Nous allons utiliser la
méthode éponyme <code>bip</code> :</p>
<pre><code class="language-php">Hoa\Console\Cursor::bip();</code></pre>
<p>Il n'y a qu'une seule <strong>tonalité</strong> disponible.</p>
<h2 id="Readline" for="main-toc">Lecture en ligne</h2>
<p>Une manière d'<strong>interagir</strong> avec les utilisateurs est de lire
le flux <code>STDIN</code>, à savoir le flux d'entrée. Cette
<strong>lecture</strong> est par défaut très basique : impossible d'effacer,
impossible d'utiliser les flèches, impossible d'utiliser des raccourcis etc.
C'est pourquoi il existe la « lecture en ligne », ou
<em lang="en">readline</em> en anglais, qui reste une lecture sur le flux
<code>STDIN</code>, mais plus <strong>évoluée</strong>. La bibliothèque
<code>Hoa\Console\Readline\Readline</code> propose plusieurs fonctionnalités
que nous allons décrire.</p>
<h3 id="Basic_usage" for="main-toc">Usage basique</h3>
<p>Pour <strong>lire une ligne</strong> (c'est à dire une entrée de
l'utilisateur), nous allons instancier la classe
<code>Hoa\Console\Readline\Readline</code> et appeler dessus la méthode
<code>readLine</code>. Chaque appel de cette méthode va attendre que
l'utilisateur <strong>saisisse</strong> une donnée puis appuye sur
<kbd title="Enter">↵</kbd>. À ce moment là, la méthode retournera la saisie de
l'utilisateur (ou <code>false</code> s'il n'y a plus rien à lire). Cette
méthode prend aussi en argument un <strong>préfixe</strong>, c'est à dire une
donnée à afficher avant la saisie de la ligne. Il arrive que le terme
<em>prompt</em> soit aussi utilisé dans la littérature, les deux notions sont
identiques.</p>
<p>Ainsi, nous allons écrire un programme qui va lire les entrées de
l'utilisateur et faire un écho. Le programme terminera si l'utilisateur saisit
<samp>quit</samp> :</p>
<pre><code class="language-php">$rl = new Hoa\Console\Readline\Readline();
do {
$line = $rl->readLine('> ');
echo '&lt; ', $line, "\n\n";
} while (false !== $line &amp;&amp; 'quit' !== $line);</code></pre>
<p>Maintenant, détaillons les services que nous offre
<code>Hoa\Console\Readline\Readline</code>.</p>
<p>Nous sommes capables de nous <strong>déplacer</strong> (comprendre,
déplacer le curseur) dans la ligne à l'aide des touches <kbd>←</kbd> et
<kbd>→</kbd>. Nous pouvons à tout moment <strong>effacer</strong> un caractère
en arrière avec la touche <kbd title="Backspace">⌫</kbd> ou tous les
caractères jusqu'au début du mot avec <kbd>Ctrl</kbd> + <kbd>W</kbd> (où
<kbd>W</kbd> signifie <em lang="en">word</em>). Nous pouvons également nous
déplacer avec des <strong>raccourcis</strong> claviers communs à beaucoup de
logiciels :</p>
<ul>
<li><kbd>Ctrl</kbd> + <kbd>A</kbd>, pour se déplacer en début de
ligne ;</li>
<li><kbd>Ctrl</kbd> + <kbd>E</kbd>, pour se déplacer en fin de ligne ;</li>
<li><kbd>Ctrl</kbd> + <kbd>B</kbd>, pour se déplacer au début du mot courant
(<kbd>B</kbd> pour <em lang="en">backward</em>) ;</li>
<li><kbd>Ctrl</kbd> + <kbd>F</kbd>, pour se déplacer en fin du mot courant
(<kbd>F</kbd> pour <em lang="en">forward</em>).</li>
</ul>
<p>Nous avons aussi accès à l'<strong>historique</strong> lorsque nous
appuyons sur les touches <kbd>↑</kbd> et <kbd>↓</kbd>, respectivement pour
chercher en arrière et avant dans l'historique. La touche
<kbd title="Tabulation">⇥</kbd> déclenche l'<strong>auto-complétion</strong>
si elle est définie. Et enfin, la touche <kbd title="Enter">↵</kbd> retourne
la saisie.</p>
<p>Il existe aussi la classe <code>Hoa\Console\Readline\Password</code> qui
permet d'avoir un lecteur de lignes avec exactement les mêmes services mais
les caractères <strong>ne s'impriment pas</strong> à l'écran, très utile pour
lire un <strong>mot de passe</strong> :</p>
<pre><code class="language-php">$rl = new Hoa\Console\Readline\Password();
$pwd = $rl->readLine('Password: ');
echo 'Your password is: ', $pwd, "\n";</code></pre>
<h3 id="Shortcuts" for="main-toc">Raccourcis</h3>
<p>Pour comprendre comment créer des raccourcis, il faut un tout petit peu
comprendre le fonctionnement <strong>interne</strong> de
<code>Hoa\Console\Readline\Readline</code>, et il est très simple. À chaque
fois que nous appuyons sur une ou plusieurs touches, une
<strong>chaîne</strong> de caractères représentant cette
<strong>combinaison</strong> est reçue par notre lecteur. Il regarde si une
action est associée à cette chaîne : si oui, il l'exécute, si non, il en
utilise une par défaut qui consiste à afficher la chaîne telle quelle. Chaque
action retourne un <strong>état</strong> pour le lecteur (qui sont des
constantes sur <code>Hoa\Console\Readline\Readline</code>) :</p>
<ul>
<li><code>STATE_CONTINUE</code>, pour continuer la lecture ;</li>
<li><code>STATE_BREAK</code>, pour arrêter la lecture ;</li>
<li><code>STATE_NO_ECHO</code>, pour ne pas afficher la lecture.</li>
</ul>
<p>Ainsi, si une action retourne <code class="language-php">STATE_CONTINUE |
STATE_NO_ECHO</code>, la lecture continuera mais la chaîne qui vient d'être
reçue ne sera pas affichée. Autre exemple, l'action associée à la touche
<kbd title="Enter">↵</kbd> retourne l'état <code>STATE_BREAK</code>.</p>
<p>Pour <strong>ajouter</strong> des actions, nous utilisons la méthode
<code>addMapping</code>. Elle facilite l'ajout grâce à une syntaxe
dédiée :</p>
<ul>
<li><code>\e[<em>…</em></code>, pour les séquences commençant par le
caractère <kbd>Esc</kbd> ;</li>
<li><code>\C-<em>…</em></code>, pour les séquences commençant par le
caractère <kbd>Ctrl</kbd> ;</li>
<li><code><em>x</em></code>, n'importe quel caractère.</li>
</ul>
<p>Par exemple, si nous voulons afficher <code>z</code> à la place de
<code>a</code>, nous écrirons :</p>
<pre><code class="language-php">$rl->addMapping('a', 'z');</code></pre>
<p>Plus compliqué maintenant, nous pouvons utiliser un
<em lang="en">callable</em> en second paramètre de
<code>addMapping</code>. Ce <em lang="en">callable</em> va recevoir l'instance
de <code>Hoa\Console\Readline\Readline</code> en seul argument. Plusieurs
méthodes sont là pour aider à <strong>manipuler</strong> le lecteur (gestion
de l'historique, de la ligne etc.). Par exemple, à chaque fois que nous
appuyerons sur <kbd>Ctrl</kbd> + <kbd>R</kbd>, nous inverserons la casse de la
ligne :</p>
<pre><code class="language-php">$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 &lt; $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('> '));</code></pre>
<p>Il ne faut pas hésiter à regarder comment sont implémentés les raccourcis
précédemment énoncés pour se donner des idées.</p>
<h3 id="Auto-completion" for="main-toc">Auto-complétion</h3>
<p>Un outil également très utile lorsque nous écrivons un lecteur de lignes
est l'<strong>auto-complétion</strong>. Elle se déclenche en appuyant sur la
touche <kbd title="Tabulation">⇥</kbd> si un auto-compléteur a été défini à
l'aide de la méthode <code>setAutocompleter</code>.</p>
<p>Tous les auto-compléteurs doivent implémenter l'interface
<code>Hoa\Console\Readline\Autocompleter\Autocompleter</code>. Quelqu'uns sont
déjà présents pour nous <strong>aider</strong> dans notre développement, comme
<code>Hoa\Console\Readline\Autocompleter\Word</code> qui va auto-compléter la
saisie à partir d'une <strong>liste de mots</strong>. Par exemple :</p>
<pre><code class="language-php">$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('> '));</code></pre>
<p>Essayons d'écrire ce que nous voulons, puis où nous le souhaitons, appuyons
sur <kbd title="Tabulation">⇥</kbd>. Si le texte à gauche du curseur commence
par <code>h</code>, alors nous verrons <samp>hoa</samp> s'écrire <strong>d'un
coup</strong> car l'auto-compléteur n'a pas de choix (il retourne une chaîne).
Si l'auto-compléteur ne trouve aucun mot adapté, il ne se passera
<strong>rien</strong> (il retournera <code>null</code>). Et enfin, s'il
trouve <strong>plusieurs mots</strong> (il retournera un tableau), alors un
<strong>menu</strong> s'affichera. Essayons d'auto-compléter simplement
<code>a</code> : le menu proposera <code>autocompleter</code>,
<samp>autocompletion</samp> et <samp>awesome</samp>. Soit nous continuons à
taper et le menu va <strong>disparaître</strong>, soit nous pouvons nous
<strong>déplacer</strong> dans le menu avec les touches
<kbd title="Tabulation">⇥</kbd>, <kbd>↑</kbd>, <kbd>→</kbd>, <kbd>↓</kbd> et
<kbd>←</kbd>, puis <kbd title="Enter">↵</kbd> pour
<strong>sélectionner</strong> un mot. Le comportement est assez
<strong>naturel</strong>.</p>
<p>En plus de l'auto-compléteur sur les mots, nous trouvons un auto-compléteur
sur les <strong>chemins</strong> avec la classe
<code>Hoa\Console\Readline\Autocompleter\Path</code>. À partir d'une racine et
d'un itérateur de fichiers, il est capable d'auto-compléter des chemins. Si la
racine n'est pas précisée, le dossier courant sera utilisé. À chaque
auto-complétion, une nouvelle instance de l'itérateur de fichiers est créée
par une <em lang="en">factory</em>. Elle reçoit en seul argument le chemin à
itérer. La <em lang="en">factory</em> par défaut est définie par la méthode
statique <code>getDefaultIteratorFactory</code> sur
<code>Hoa\Console\Readline\Autocompleter\Path</code>. Elle construit un
itérateur de fichiers de type
<a href="http://php.net/directoryiterator"><code>DirectoryIterator</code></a>.
Chaque valeur calculée par l'itérateur doit être un objet de type
<a href="http://php.net/splfileinfo"><code>SplFileInfo</code></a>. Ainsi, pour
auto-compléter tous les fichiers et dossiers à partir de la racine
<a href="@central_resource:path=Library/Console"><code>hoa://Library/Console</code></a>,
nous écrirons :</p>
<pre><code class="language-php">$rl->setAutocompleter(
new Hoa\Console\Readline\Autocompleter\Path(
resolve('hoa://Library/Console')
)
);</code></pre>
<p>Utiliser une <em lang="en">factory</em> nous offre beaucoup de
<strong>souplesse</strong> et nous permet d'utiliser n'importe quel itérateur
de fichiers, comme par exemple <code>Hoa\File\Finder</code> (voir
<a href="@hack:chapter=File">la bibliothèque <code>Hoa\File</code></a>).
Ainsi, pour n'auto-compléter que les fichiers et dossiers non cachés qui ont
été modifiés les 6 derniers mois triés par leur taille, nous écrirons :</p>
<pre><code class="language-php">$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;
}
)
);</code></pre>
<p>Nous pouvons remplacer l'itérateur de fichiers locaux par un itérateur
totalement <strong>différent</strong> : sur des fichiers stockés sur une autre
machine, un service tiers ou même des ressources qui ne sont pas des fichiers
mais ont des URI de la forme d'un chemin.</p>
<p>Enfin, nous pouvons assembler plusieurs auto-compléteurs entre eux grâce à
la classe <code>Hoa\Console\Readline\Autocompleter\Aggregate</code>. L'ordre
de déclaration des auto-compléteurs est important : le premier qui reconnaît
un mot à auto-compléter prendra la main. Ainsi, pour auto-compléter des
chemins et des mots, nous écrirons :</p>
<pre><code class="language-php">$rl->setAutocompleter(
new Hoa\Console\Readline\Autocompleter\Aggregate([
new Hoa\Console\Readline\Autocompleter\Path(),
new Hoa\Console\Readline\Autocompleter\Word($words)
])
);
</code></pre>
<p>La méthode <code>getAutocompleters</code> de
<code>Hoa\Console\Readline\Autocompleter\Aggregate</code> retourne un objet
<a href="http://php.net/arrayobject"><code>ArrayObject</code></a> pour plus de
souplesse. Nous pouvons ainsi toujours ajouter ou supprimer des
auto-compléteurs après les avoir déclarés dans le constructeur.</p>
<figure>
<img src="https://central.hoa-project.net/Resource/Library/Console/Documentation/Image/Readline_autocompleters.gif?format=raw" />
<figcaption>Exemple d'une agrégation de l'auto-compléteur
<code>Hoa\Console\Readline\Autocompleter\Path</code> avec
<code>Hoa\Console\Readline\Autocompleter\Word</code>.</figcaption>
</figure>
<h2 id="Reading_options" for="main-toc">Lecture d'options</h2>
<p>Une grande force des programmes en ligne de commande est leur
<strong>flexibilité</strong>. Ils sont <strong>dédiés</strong> à une seule
(petite) <strong>tâche</strong> et nous pouvons les paramétrer grâce aux
<strong>options</strong> qu'ils exposent. La <strong>lecture</strong> de ces
options doit être simple et rapide car c'est une tâche répétitive et délicate.
La classe <code>Hoa\Console\Parser</code> et
<code>Hoa\Console\GetOption</code> fonctionnent en <strong>duo</strong> afin
de répondre à cette problématique.</p>
<h3 id="Analyzing_options" for="main-toc">Analyser les options</h3>
<p>Nous allons commencer par utiliser <code>Hoa\Console\Parser</code> qui
permet d'<strong>analyser</strong> les options données à un programme. Peu
importe les options que nous voulons précisément, nous nous contentons de les
analyser pour l'instant. Commençons par utiliser la méthode
<code>parse</code> :</p>
<pre><code class="language-php">$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
* )
*/</code></pre>
<p>Étudions un peu de quoi est constituée une ligne de commande. Nous avons
deux catégories : les <strong>options</strong> (<em lang="en">switches</em>)
et les <strong>entrées</strong> (<em lang="en">inputs</em>). Les entrées sont
tout ce qui n'est pas une option. Une option peut avoir deux formes :
<strong>courte</strong> si elle n'a qu'un seul caractère ou
<strong>longue</strong> si elle en a plusieurs.</p>
<p>Ainsi, <code>-s</code> est une option courte, et <code>--long</code> est
une option longue. Toutefois, il faut aussi considérer le nombre de tirets
devant l'option : avec deux tirets, ce sera toujours une option longue, avec
un seul tiret, ça dépend. Il y a deux écoles qui se différencient avec un seul
<strong>paramètre</strong> : <em lang="en">long only</em>. Prenons un
exemple : <code>-abc</code> est considéré comme <code>-a -b -c</code> si le
paramètre <em lang="en">long only</em> est définie à <code>false</code>, sinon
ce sera équivalent à une option longue, comme <code>--abc</code>.
Majoritairement, ce paramètre est définie à <code>false</code> par défaut et
<code>Hoa\Console\Parser</code> s'est rangé du côté de la majorité. Pour
modifier ce paramètre, il faut utiliser la méthode <code>setLongOnly</code>,
voyons plutôt :</p>
<pre><code class="language-php">// 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
* )
*/</code></pre>
<p>Une option peut être de deux sortes : <strong>booléenne</strong> ou
<strong>valuée</strong>. Si aucune valeur ne lui est associée, elle est
considérée comme booléenne. Ainsi, <code>-s</code> vaut <code>true</code>,
mais <code>-s -s</code> vaut <code>false</code>, et du coup <code>-s -s
-s</code> vaut <code>true</code> et ainsi de suite. Une option booléenne
fonctionne comme un <strong>interrupteur</strong>. Une option valuée a une
valeur associée, soit par un espace, soit par un signe d'égalité (symbole
<code>=</code>). Voici une liste non-exhaustive des possibilités avec la
valeur associée (nous utilisons une option courte mais ça peut être une option
longue) :</p>
<ul>
<li><code>-x=value</code> : <code>value</code> ;</li>
<li><code>-x=va\ lue</code> : <code>va lue</code> ;</li>
<li><code>-x="va lue"</code> : <code>va lue</code> ;</li>
<li><code>-x="va l\"ue"</code> : <code>va l"ue</code> ;</li>
<li><code>-x value</code> : <code>value</code> ;</li>
<li><code>-x va\ lue</code> : <code>va lue</code> ;</li>
<li><code>-x "value"</code> : <code>value</code> ;</li>
<li><code>-x "va lue"</code> : <code>va lue</code> ;</li>
<li><code>-x va\ l"ue</code> : <code>va l"ue</code> ;</li>
<li><code>-x 'va "l"ue'</code> : <code>va "l"ue</code> ;</li>
<li>etc.</li>
</ul>
<p>Les simples (symbole <code>'</code>) et doubles (symbole <code>"</code>)
guillemets sont supportés. Mais attention, il y a des cas particuliers qui ne
sont pas toujours <strong>standards</strong> :</p>
<ul>
<li><code>-x=-value</code> : <code>-value</code> ;</li>
<li><code>-x "-value"</code> : <code>-value</code> ;</li>
<li><code>-x \-value</code> : <code>-value</code> ;</li>
<li><code>-x -value</code> : équivaut à deux options booléennes
<code>-x</code> et <code>-value</code> ;</li>
<li><code>-x=-7</code> : <code>-7</code> ;</li>
<li>etc.</li>
</ul>
<p><em>À l'instar</em> des options booléennes qui fonctionnent comme des
interrupteurs, les options valuées <strong>réécrivent</strong> leurs valeurs
si elles sont déclarées plusieurs fois. Ainsi avec <code>-a=b -a=c</code>,
<code>-a</code> vaudra <code>c</code>.</p>
<p>Enfin, il y a des valeurs qui sont considérées comme
<strong>spéciales</strong>. Nous en distingons deux :</p>
<ul>
<li>les <strong>listes</strong>, à l'aide de la virgule comme séparateur :
<code>-x=a,b,c</code> ;</li>
<li>les <strong>intervalles</strong>, à l'aide du symbole <code>:</code>
(sans espace autour) : <code>-x=1:7</code>.</li>
</ul>
<p>Sans aucune manipulation, ces valeurs ne seront pas considérées comme
spéciales. Il faudra utiliser la méthode
<code>Hoa\Console\Parser::parseSpecialValue</code> comme nous allons le voir
très prochainement.</p>
<h3 id="Read_options_and_inputs" for="main-toc">Lire les options et les
entrées</h3>
<p>Nous savons analyser les options mais ce n'est pas suffisant pour les lire
correctement. Il faut leur donner une petite <strong>sémantique</strong> :
qu'attendent-elles, quelle est leur nature etc. Pour cela, nous allons nous
aider de la classe <code>Hoa\Console\GetOption</code>. Une option est
caractérisée par :</p>
<ul>
<li>un nom <strong>long</strong> ;</li>
<li>un nom <strong>court</strong> ;</li>
<li>un <strong>type</strong>, donné par une des constantes de
<code>Hoa\Console\GetOption</code>, parmi :
<ul>
<li><code>NO_ARGUMENT</code> si l'option est booléenne ;</li>
<li><code>REQUIRED_ARGUMENT</code> si l'option est valuée ;</li>
<li><code>OPTIONAL_ARGUMENT</code> si l'option peut avoir une
valeur.</li>
</ul>
</li>
</ul>
<p>Ces trois informations sont <strong>obligatoires</strong>. Elles doivent
être données au constructeur de <code>Hoa\Console\GetOption</code> en premier
argument. Le second argument est l'analyseur d'options (l'analyse doit être
<strong>préalablement</strong> effectuée). Ainsi nous décrivons deux options :
<code>extract</code> qui est une option booléenne, et <code>directory</code>
qui est une option valuée :</p>
<pre><code class="language-php">$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
);</code></pre>
<p>Nous pouvons maintenant lire nos options ! Le lecteur d'options fonctionne
comme un itérateur, ou plutôt une <strong>pipette</strong>, à l'aide de la
méthode <code>getOption</code>. Cette méthode retourne le nom court de
l'option lue et assignera la valeur de l'option (un booléen ou une chaîne de
caractères) à son premier argument passé en référence. Quand la pipette est
vide, la méthode <code>getOption</code> retourne <code>false</code>.
Cette structure peut paraître originale mais elle est pourtant très
<strong>répandue</strong>, vous ne serez pas déroutés en la voyant autre part
(exemples
<a href="http://kernel.org/doc/man-pages/online/pages/man3/getopt.3.html#EXAMPLE"
title="getopt(3), Linux Programmer's Manual">dans Linux</a>,
<a href="http://freebsd.org/cgi/man.cgi?query=getopt&sektion=3#EXAMPLES"
title="getopt(3), FreeBSD Library Functions Manual">dans FreeBSD</a> ou
<a href="http://developer.apple.com/library/Mac/#documentation/Darwin/Reference/ManPages/man3/getopt.3.html"
title="getopt(3), BSD Library Functions Manual">dans Mac OS X</a> — même
base de code —). La manière la plus simple pour lire les options est de
définir des valeurs par défaut pour nos options, puis d'utiliser
<code>getOption</code>, ainsi :</p>
<pre><code class="language-php">$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"
*/</code></pre>
<p>Cela se lit : « tant que nous avons une option à lire, nous récupérons
son nom court dans <code>$c</code> et sa valeur dans <code>$v</code>, puis
nous regardons quoi en faire ».</p>
<p>Pour lire les entrées, nous utiliserons la méthode
<code>Hoa\Console\Parser::listInputs</code> dont tous les arguments (au nombre
de 26) sont passés en <strong>référence</strong>. Ainsi :</p>
<pre><code class="language-php">$parser->listInputs($inputA, $inputB, $inputC);
var_dump($inputA, $inputB, $inputC);
/**
* Will output:
* string(6) "inputA"
* string(6) "inputB"
* string(6) "inputC"
*/</code></pre>
<p>Attention, cette façon de procéder implique que les entrées sont
<strong>ordonnées</strong> (comme c'est pratiquement toujours le cas). Mais
aussi, lire les entrées sans avoir préalablement donné l'analyseur à
<code>Hoa\Console\GetOption</code> peut produire des résultats imprévus (car
par défaut, toutes les options sont considérées comme booléennes). Si nous
voulons toutes les entrées et les analyser manuellement si elles ne sont pas
ordonnées, nous pouvons utiliser la méthode
<code>Hoa\Console\Parser::getInputs</code> qui retournera toutes les
entrées.</p>
<h3 id="Special_or_ambiguous_options" for="main-toc">Options spéciales ou
ambiguës</h3>
<p>Revenons sur la méthode <code>Hoa\Console\Parser::parseSpecialValue</code>.
Elle prend deux arguments : une valeur et un tableau de mots-clés. Voyons
plutôt. Nous reprenons notre exemple et modifions le cas pour l'option
<code>d</code> :</p>
<pre data-line="8-11"><code class="language-php">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);</code></pre>
<p>Si nous essayons avec <code>-d=a,b,HOME,c,d</code>, alors <code>-d</code>
aura la valeur suivante :</p>
<pre><code class="language-php">/**
* Array
* (
* [0] => a
* [1] => b
* [2] => /tmp
* [3] => c
* [4] => d
* )
*/</code></pre>
<p>Enfin, quand une option lue n'existe pas mais qu'elle est très
<strong>proche</strong> d'une option existante à quelques
<strong>fautes</strong> près (par exemple <code>--dirzctory</code> au lieu de
<code>--directory</code>), nous pouvons utiliser le cas
<code>__ambiguous</code> pour la capturer et la traiter :</p>
<pre data-line="13-16"><code class="language-php">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;
}
}</code></pre>
<p>La valeur (dans <code>$v</code>) est un tableau avec trois entrées. Par
exemple avec <code>--dirzctory</code>, nous obtenons :</p>
<pre><code class="language-php">/**
* Array
* (
* [solutions] => Array
* (
* [0] => directory
* )
*
* [value] => y
* [option] => dirzctory
* )
*/</code></pre>
<p>La clé <code>solutions</code> propose toutes les options
<strong>similaires</strong>, la clé <code>value</code> donne la valeur de
l'option et <code>option</code> le nom <strong>original</strong> lu. C'est à
l'utilisateur de décider quoi faire à partir de ces informations. Nous pouvons
utiliser la méthode <code>Hoa\Console\GetOption::resolveOptionAmbiguity</code>
en lui donnant ce tableau, et elle choisira la meilleure option si elle existe :</p>
<pre><code class="language-php"> case '__ambiguous':
$options->resolveOptionAmbiguity($v);
break;
</code></pre>
<p>Il est quand même préférable d'<strong>avertir</strong> l'utilisateur qu'il
y a une ambiguïté et de lui demander son avis. Il peut parfois être
<strong>dangereux</strong> de prendre la décision à sa place.</p>
<h3 id="Integrate_a_router_and_a_dispatcher" for="main-toc">Intégrer un
routeur et un dispatcheur</h3>
<p>Jusqu'à maintenant, nous forcions des options et des entrées à l'analyseur.
<code>Hoa\Router\Cli</code> permet d'<strong>extraire</strong> des données
depuis un programme en ligne de commande. Une méthode nous intéresse :
<code>Hoa\Router\Cli::getURI</code>, qui va nous donner toutes les options et
les entrées du programme courant, que nous pourrons alors
<strong>fournir</strong> à notre analyseur. Ainsi :</p>
<pre data-line="2"><code class="language-php">$parser = new Hoa\Console\Parser();
$parser->parse(Hoa\Router\Cli::getURI());
// …</code></pre>
<p>Il est maintenant possible d'interpréter les options que nous donnons à
notre propre programme. Si vous avez écrit les tests dans un fichier nommé
<code>Test.php</code>, alors vous pourrez écrire :</p>
<pre><code class="language-shell">$ 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</code></pre>
<p>L'option <code>-x</code> vaut bien <code>true</code>, l'option
<code>-d</code> vaut un tableau (car nous l'avons analysé avec la méthode
<code>Hoa\Console\Parser::parseSpecialValue</code>), et nous avons
<code>inputA</code>, <code>inputB</code> et <code>null</code> en entrée.</p>
<p>C'est un bon début, et nous pourrions nous arrêter là dans la plupart des
cas. Mais il est possible d'aller plus loin en mettant en place un
<strong>dispatcheur</strong> : écrire des commandes dans plusieurs fonctions
ou classes et les appeler en fonction des options et entrées données à notre
programme. Nous vous conseillons de regarder le code source de
<a href="@central_resource:path=Library/Cli/Bin/Hoa.php"><code>hoa://Library/Cli/Bin/Hoa.php</code></a>
pour vous aider, ainsi que les chapitres de
<a href="@hack:chapter=Router"><code>Hoa\Router</code></a> et
<a href="@hack:chapter=Dispatcher"><code>Hoa\Dispatcher</code></a>. Nous
proposons un exemple rapide sans donner trop de détails sur les bibliothèques
précédement citées.</p>
<p>L'idée est la suivante. Grâce à <code>Hoa\Router\Cli</code>, nous allons
extraire des données de la forme suivante : <code>$ php script.php
<em>controller</em> <em>tail</em></code>, où <code><em>controller</em></code>
sera le nom du contrôleur (d'une classe) sur laquelle nous appellerons
l'action <code>main</code> (soit la méthode <code>main</code> avec les
paramètres par défaut), et où <code><em>tail</em></code> correspond aux
options et aux entrées. Le nom du contrôleur est identifié par la variable
spéciale <code>_call</code> (au niveau de <code>Hoa\Router\Cli</code>) et les
options ainsi que les entrées par <code>_tail</code> (au niveau de
<code>Hoa\Dispatcher\Kit</code>). Les options et entrées ne sont pas
obligatoires. Ensuite, nous allons utiliser <code>Hoa\Dispatcher\Basic</code>
avec le kit dédié aux terminaux, à savoir
<code>Hoa\Console\Dispatcher\Kit</code>. Le dispatcheur va chercher à charger
les classes <code>Application\Controller\<em>controller</em></code> par
défaut, et l'auto-chargeur va les chercher dans le dossier
<code>hoa://Application/Controller/<em>controller</em></code>. Nous allons
donc préciser où se trouve l'application très rapidement. Enfin, le code de
retour de notre programme sera donné par la valeur de retour de notre
contrôleur et de notre action. En cas d'erreur, nous l'afficherons et nous
forcerons un code de retour supérieur à zéro. Ainsi :</p>
<pre><code class="language-php">try {
// Prepare the router.
$router = new Hoa\Router\Cli();
$router->get(
'g',
'(?&lt;_call>\w+)(?:\s+(?&lt;_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);
}</code></pre>
<p>Au même niveau que notre programme, créons le dossier
<code>Application/Controller/</code> avec le fichier <code>Foo.php</code> à
l'intérieur, qui contiendra le code suivant :</p>
<pre><code class="language-php">&lt;?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 &lt;options>', "\n",
'Options :', "\n",
$this->makeUsageOptionsList([
'x' => 'Whether we need to extract.',
'd' => 'Directory to extract.',
'h' => 'This help.'
]);
}
}</code></pre>
<p>Notre classe étend bien notre kit pour bénéficier des méthodes qu'il
propose. Entre autre, sa propre méthode <code>getOption</code>, qui va
exploiter l'attribut <code>$options</code> où sont déclarées les options,
<code>makeUsageOptionsList</code> pour afficher une aide, sa propre méthode
<code>resolveOptionAmbiguity</code> qui demande une confirmation à
l'utilisateur, l'accès au routeur à travers l'attribut <code>$router</code>
etc. Les kits offrent des <strong>services</strong> à l'application, ils
<strong>aggrègent</strong> des services offerts par les bibliothèques.
Maintenant testons :</p>
<pre><code class="language-shell">$ php Test.php foo -x -d=1:3
extract: bool(true)
directory: Array
(
[0] => 1
[1] => 2
[2] => 3
)</code></pre>
<p>Magnifique !</p>
<p>Précisons que le script <code>hoa</code> est exactement construit de cette
manière. N'hésitez pas à vous en inspirer.</p>
<h2 id="Processus" for="main-toc">Processus</h2>
<p>Dans notre contexte, un <strong>processus</strong> est un programme
classique qui s'exécute dans un <strong>terminal</strong>. Ce qui est
intéressant, c'est qu'un tel programme <strong>communique</strong> avec le
reste de son <strong>environnement</strong> grâce à des
<strong>tuyaux</strong>, ou <em lang="en">pipes</em> en anglais, numérotés à
partir de zéro. Certains ont même des noms et sont standards :</p>
<ul>
<li><code>STDIN</code> (<code>0</code>) pour lire des
<strong>entrées</strong> (<em lang="en">standard input</em>) ;</li>
<li><code>STDOUT</code> (<code>1</code>) pour écrire des
<strong>sorties</strong> (<em lang="en">standard output</em>) ;</li>
<li><code>STDERR</code> (<code>2</code>) pour écrire des
<strong>erreurs</strong> (<em lang="en">standard error</em>).</li>
</ul>
<p>Quand un processus s'exécute dans un terminal, <code>STDIN</code> utilise
le <strong>clavier</strong> comme source de données, et <code>STDOUT</code>
comme <code>STDERR</code> sont reliés à la <strong>fenêtre</strong> d'un
terminal. Mais quand un processus est exécuté dans un
<strong>sous-terminal</strong>, c'est à dire exécuté à partir d'un autre
processus, <code>STDIN</code> n'est pas relié au clavier, tout comme
<code>STDOUT</code> et <code>STDERR</code> ne sont pas reliés à l'écran.
C'est le processus parent qui va écrire et lire sur ces flux pour
<strong>interagir</strong> avec le « sous »-processus. Ce mécanisme s'appelle
la <strong>redirection</strong> de flux, nous l'utilisons très souvent quand
nous écrivons une ligne de commande (voir
<a href="http://gnu.org/software/bash/manual/bashref.html#Redirections">section
<em lang="en">Redirections</em> du <em lang="en">Bash Reference
Manual</em></a>). Ce que nous allons faire utilise une autre syntaxe mais le
mécanisme est le même.</p>
<p>Il est très important de savoir que ces flux sont tous
<strong>asynchrones</strong> les uns par rapport aux autres. Aucun flux n'a
un impact sur un autre, il n'y a aucun lien entre eux et c'est important pour
la suite.</p>
<p>Au niveau de PHP, il est possible d'accéder à ces flux en utilisant
respectivement les URI suivants : <code>php://stdin</code>,
<code>php://stdout</code> et <code>php://stderr</code>. Mais nous avons aussi
les constantes éponymes <code>STDIN</code>, <code>STDOUT</code> et
<code>STDERR</code>. Elles sont définies comme suit (exemple avec
<code>STDIN</code>) :</p>
<pre><code class="language-php">define('STDIN', fopen('php://stdin', 'r'));</code></pre>
<p>Ces flux ne sont disponibles que si le programme s'exécute en ligne de
commande. Rappelons-nous également que les <em lang="en">pipes</em> sont
identifiés par des numéros. Nous pouvons alors utiliser
<code>php://fd/0</code> pour se référer à <code>STDIN</code>,
<code>php://fd/1</code> pour <code>STDOUT</code> etc. L'URI
<code>php://fd/<em>i</em></code> permet d'accéder au fichier ayant le
<strong>descripteur</strong> <code><em>i</em></code>.</p>
<h3 id="Very_basic_execution" for="main-toc">Exécution très basique</h3>
<p>La classe <code>Hoa\Console\Processus</code> propose une manière très
<strong>rapide</strong> d'exécuter un processus et d'obtenir le résultat de
<code>STDOUT</code>. C'est le cas le plus commun. Ainsi, nous allons utiliser
la méthode statique <code>execute</code> :</p>
<pre><code class="language-php">var_dump(Hoa\Console\Processus::execute('id -u -n'));
/**
* Could output:
* string(3) "hoa"
*/</code></pre>
<p>Par défaut, la commande sera échappée pour des raisons de sécurité. Si vous
avez confiance dans la commande, vous pouvez désactiver l'échappement en
passant <code>false</code> en second argument.</p>
<p>Nous n'avons aucun contrôle sur les <em lang="en">pipes</em> et même si ça
convient dans la plupart des cas, ce n'est pas suffisant quand nous souhaitons
un minimum d'interaction avec le processus.</p>
<h3 id="Reading_and_writing" for="main-toc">Lecture et écriture</h3>
<p>Voyons comment <strong>interagir</strong> avec un processus. Nous allons
considérer le programme <code>LittleProcessus.php</code> suivant :</p>
<pre><code class="language-php">&lt;?php
$range = range('a', 'z');
while (false !== $line = fgets(STDIN)) {
echo '> ', $range[intval($line)], "\n";
}</code></pre>
<p>Pour tester et comprendre son fonctionnement, écrivons la ligne de commande
suivante et entrons au clavier <code>3</code>, puis <code>4</code> :</p>
<pre><code class="language-shell">$ php LittleProcessus.php
3
> d
4
> e
</code></pre>
<p>Nous pouvons aussi écrire :</p>
<pre><code class="language-shell">$ seq 0 4 | php LittleProcessus.php
> a
> b
> c
> d
> e</code></pre>
<p>Notre programme va lire chaque ligne sur l'entrée standard, considérer que
c'est un nombre, et le transformer en caractère qui sera affiché sur la sortie
standard. Nous aimerions exécuter ce programme en lui donnant nous-même une
liste de nombres (comme le programme <code>seq</code>) et en observant le
résultat qu'il produira.</p>
<p>Une instance de la classe <code>Hoa\Console\Processus</code> représente un
<strong>processus</strong>. Lors de l'instanciation, nous devons
préciser :</p>
<ul>
<li>le <strong>nom</strong> du processus ;</li>
<li>ses <strong>options</strong> ;</li>
<li>la <strong>description</strong> des <em lang="en">pipes</em>.</li>
</ul>
<p>Il y a d'autres arguments mais nous les verrons plus tard.</p>
<p>La description des <em lang="en">pipes</em> a la forme d'un tableau où
chaque clé représente le numéro du <em lang="en">pipe</em> (plus généralement,
c'est le <code><em>i</em></code> de <code>php://fd/<em>i</em></code>) et la
valeur est encore un tableau décrivant la nature du <em lang="en">pipe</em>,
soit un « vrai » <em lang="en">pipe</em>, soit un fichier, avec leur mode de
lecture ou d'écriture (parmi <code>r</code>, <code>w</code> ou
<code>a</code>). Illustrons avec un exemple :</p>
<pre><code class="language-php">$processus = new Hoa\Console\Processus(
'php',
['LittleProcessus.php'],
[
// STDIN.
0 => ['pipe', 'r'],
// STDOUT.
1 => ['file', '/tmp/output', 'a']
]
);</code></pre>
<p>Dans ce cas, <code>STDIN</code> est un <em lang="en">pipe</em> et
<code>STDOUT</code> est le fichier <code>/tmp/output</code>. Si nous ne
précisions pas de descripteur, ce sera équivalent à écrire :</p>
<pre><code class="language-php">$processus = new Hoa\Console\Processus(
'php',
['LittleProcessus.php'],
[
// STDIN.
0 => ['pipe', 'r'],
// STDOUT.
1 => ['pipe', 'w'],
// STDERR.
2 => ['pipe', 'w']
]
);</code></pre>
<p>Chaque <em lang="en">pipe</em> est reconnu comme un <strong>flux</strong>
et peut être manipulé comme tel. Quand un <em lang="en">pipe</em> est en
<strong>lecture</strong> (avec le mode <code>r</code>), cela signifie que le
processus va <strong>lire</strong> dessus. Donc nous, le processus parent,
nous allons <strong>écrire</strong> sur ce <em lang="en">pipe</em>. Prenons
l'exemple de <code>STDIN</code> : le processus lit sur <code>STDIN</code> ce
que le clavier a écrit dessus. Et inversement, quand un
<em lang="en">pipe</em> est en <strong>écriture</strong> (avec le mode
<code>w</code>), cela signifie que nous allons <strong>lire</strong> dessus.
Prenons l'exemple de <code>STDOUT</code> : l'écran va lire ce que le processus
lui a écrit.</p>
<p>La classe <code>Hoa\Console\Processus</code> étend la classe
<a href="@hack:chapter=Stream"><code>Hoa\Stream</code></a>, et de ce fait,
nous avons tous les outils nécessaires pour lire et écrire sur les
<em lang="en">pipes</em> de notre choix. Cette classe propose aussi plusieurs
<strong>écouteurs</strong> :</p>
<ul>
<li><code>start</code>, quand le processus est démarré ;</li>
<li><code>stop</code>, quand le processus est arrêté ;</li>
<li><code>input</code>, quand les flux en lecture sont prêts ;</li>
<li><code>output</code>, quand les flux en écriture sont prêts ;</li>
<li><code>timeout</code>, quand le processus s'exécute depuis trop
longtemps.</li>
</ul>
<p>Prenons directement un exemple. Nous allons exécuter le processus
<code>php LittleProcessus.php</code> et attacher des fonctions aux écouteurs
suivants : <code>input</code> pour écrire une série de chiffres et
<code>output</code> pour lire le résultat.</p>
<pre><code class="language-php">$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
*/</code></pre>
<p>Maintenant, rentrons dans le détail pour bien comprendre les choses.</p>
<p>Quand un flux en <strong>lecture</strong> est <strong>prêt</strong>, alors
l'écouteur <code>input</code> se déclenche. Une seule donnée est envoyée :
<code>pipe</code> qui contient le numéro du <em lang="en">pipe</em> (le
<code><em>i</em></code> de <code>php://fd/<em>i</em></code>). Quand un flux en
<strong>écriture</strong> est prêt, alors l'écouteur <code>output</code> se
déclenche. Deux données sont envoyées : <code>pipe</code> (comme pour
<code>input</code>) et <code>line</code> qui est la <strong>ligne
reçue</strong>.</p>
<p>Nous voyons dans la fonction attachée à l'écouteur <code>input</code> que
nous écrivons une suite de nombres concaténés par <code>\n</code> (un nombre
par ligne). Pour cela, nous utilisons la méthode <code>writeAll</code>. Par
défaut, les méthodes d'écriture écrivent sur le <em lang="en">pipe</em>
<code>0</code>. Pour changer ce comportement, il faudra donner le numéro de
<em lang="en">pipe</em> en second argument des méthodes d'écriture. Pareil
pour les méthodes de lecture mais le <em lang="en">pipe</em> par défaut est
<code>1</code>.</p>
<p>Quand un <em lang="en">callable</em> attaché à un écouteur retourne
<code>false</code>, le <em lang="en">pipe</em> qui a déclenché cet appel sera
<strong>fermé</strong> juste après. Dans notre cas, la fonction attachée à
<code>input</code> retourne <code>false</code> juste après avoir écrit, nous
n'avons plus besoin de ce <em lang="en">pipe</em>. Il est important pour des
raisons de <strong>performances</strong> de fermer les
<em lang="en">pipes</em> dès que possible.</p>
<p>Enfin, pour <strong>exécuter</strong> le processus, nous utilisons la
méthode <code>Hoa\Console\Processus::run</code> d'arité nulle.</p>
<p>Dans notre exemple, nous écrivons toutes les données d'un coup mais nous
pouvons envoyer les données dès qu'elles sont disponibles, ce qui est plus
performant car le processus n'attend pas un gros paquet de données : il peut
les traiter au fur et à mesure. Modifions notre exemple pour écrire une donnée
à chaque fois que <code>STDIN</code> est prêt :</p>
<pre><code class="language-php">$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;
});</code></pre>
<p>Nous initialisons deux variables : <code class="language-php">$i</code> et
<code class="language-php">$j</code>, qui portent le nombre à envoyer et le
nombre maximum de données à envoyer. Nous introduisons une latence volontaire
avec <code class="language-php">usleep(50000)</code> pour laisser le temps à
<code>STDOUT</code> d'être prêt, ceci afin de mieux illustrer notre exemple.
Dans ce cas, la sortie serait :</p>
<pre><code class="language-php">/** 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
*/</code></pre>
<p>Le processus est en attente d'une entrée et lit les données dès qu'elles
arrivent. Une fois que nous avons envoyé toutes les données, nous fermons le
<em lang="en">pipe</em>.</p>
<p>Le processus se <strong>ferme</strong> de lui-même. Nous avons la méthode
<code>Hoa\Console\Processus::getExitCode</code> pour connaître le
<strong>code</strong> de retour du processus. Attention, un code
<code>0</code> représente un <strong>succès</strong>. Comme l'erreur est
répandue, il existe la méthode
<code>Hoa\Console\Processus::isSuccessful</code> pour savoir si le processus
s'est exécuté avec succès ou pas.</p>
<h3 id="Detect_the_type_of_pipes" for="main-toc">Détecter le type des
<em lang="en">pipes</em></h3>
<p>Parfois, il est utile de connaître le <strong>type</strong> des
<em lang="en">pipes</em>, c'est à dire si c'est une utilisation
<strong>directe</strong>, un <strong><em lang="en">pipe</em></strong> ou une
<strong>redirection</strong>. Nous allons nous aider de la classe
<code>Hoa\Console\Console</code> et de ses méthodes statiques
<code>isDirect</code>, <code>isPipe</code> et <code>isRedirection</code> pour
obtenir ces informations.</p>
<p>Prenons un exemple pour comprendre plus rapidement. Écrivons le fichier
<code>Type.php</code> qui va étudier le type de <code>STDOUT</code> :</p>
<pre><code class="language-php">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));</code></pre>
<p>Et maintenant, exécutons ce fichier pour voir le résultat :</p>
<pre><code class="language-shell">$ 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)</code></pre>
<p>Dans le premier cas, <code>STDOUT</code> est bien <strong>direct</strong>
(pour <code>STDOUT</code>, cela signifie qu'il est <strong>relié</strong> à
l'écran, pour <code>STDIN</code>, il serait relié au clavier etc.). Dans le
deuxième cas, <code>STDOUT</code> est un
<strong><em lang="en">pipe</em></strong>, c'est à dire qu'il est
<strong>attaché</strong> au <code>STDIN</code> de la commande située après le
symbole <code>|</code>. Dans le dernier cas, <code>STDOUT</code> est une
<strong>redirection</strong>, c'est à dire qu'il est <strong>redirigé</strong>
dans le fichier <code>/tmp/foo</code> (que nous affichons juste après).
L'opération peut se faire sur <code>STDIN</code>, <code>STDERR</code> ou
n'importe quelle autre ressource.</p>
<p>Connaître le type des <em lang="en">pipes</em> peut permettre des
comportements différents selon le <strong>contexte</strong>. Par exemple,
<code>Hoa\Console\Readline\Readline</code> lit sur <code>STDIN</code>. Si son
type est un <em lang="en">pipe</em> ou une redirection, le mode d'édition de
ligne avancé sera désactivé et il retourne <code>false</code> quand il n'a
plus rien à lire. Autre exemple, la verbosité des commandes du script
<code>hoa</code> utilise le type de <code>STDOUT</code> comme valeur par
défaut : direct pour être verbeux, sinon non-verbeux. Essayez les exemples
suivants pour voir la différence :</p>
<pre><code class="language-shell">$ hoa --no-verbose
$ hoa | xargs -I@ echo @</code></pre>
<p>Les exemples ne manquent pas mais attention à utiliser cette fonctionnalité
avec intelligence. Il faut adapter les comportements mais rester
<strong>cohérent</strong>.</p>
<h3 id="Execution_conditions" for="main-toc">Condition d'exécution</h3>
<p>Le processus s'exécute dans un <strong>dossier</strong> particulier et un
<strong>environnement</strong> particulier. Le dossier est appelé
<em lang="en">current working directory</em>, souvent abrégé
<abbr lang="en">cwd</abbr>. Il définit le dossier où sera exécuté le
processus. Nous pouvons le retrouver en PHP avec
<a href="http://php.net/getcwd">la fonction <code>getcwd</code></a>.
L'environnement se définit par un tableau que nous retrouvons par exemple en
exécutant <code>/usr/bin/env</code>. C'est dans cet environnement qu'est
présent le <code>PATH</code> par exemple. Ces données sont passées en
quatrième et cinquième arguments du constructeur de
<code>Hoa\Console\Processus</code>. Ainsi :</p>
<pre><code class="language-php">$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(
'&lt;?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
* >
* > )
*/</code></pre>
<p>Si le <em lang="en">current working directory</em> n'est pas précisé, nous
utiliserons le même que le programme. Si aucun environnement n'est précisé, le
processus utilisera celui de son parent.</p>
<p>Nous pouvons aussi imposer un <strong>temps maximum</strong> de
<strong>réponse</strong> en seconde au processus (défini à 30 secondes par
défaut). C'est le dernier argument du constructeur. Nous pouvons utiliser la
méthode <code>Hoa\Console\Processus::setTimeout</code>. Pour savoir quand ce
temps est atteint, nous devons utiliser l'écouteur <code>timeout</code>.
Aucune action ne sera faite automatiquement. Nous pouvons par exemple terminer
le processus grâce à la méthode <code>Hoa\Console\Processus::terminate</code>.
Ainsi :</p>
<pre><code class="language-php">$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('&lt;?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
*/</code></pre>
<p>Aucun action n'est réalisée automatiquement car elles peuvent être
nombreuses. Nous pouvons peut-être débloquer le processus, le fermer pour en
ouvrir un autre, émettre des rapports etc.</p>
<p>À propos de la méthode <code>terminate</code>, elle peut prendre plusieurs
valeurs différentes, définies par les constantes de
<code>Hoa\Console\Processus</code> : <code>SIGHUP</code>, <code>SIGINT</code>,
<code>SIGQUIT</code>, <code>SIGABRT</code>, <code>SIGKILL</code>,
<code>SIGALRM</code> et <code>SIGTERM</code> (par défaut). Plusieurs
<strong>signaux</strong> peuvent être envoyés aux processus pour qu'ils
s'arrêtent. Pour avoir le détail, voir
<a href="http://freebsd.org/cgi/man.cgi?query=signal"
title="signal(3), FreeBSD Library Functions Manual">la page
<code>signal</code></a>.</p>
<h3 id="Miscellaneous" for="main-toc">Miscellaneous</h3>
<p>Les méthodes statiques <code>getTitle</code> et <code>setTitle</code> sur
la classe <code>Hoa\Console\Processus</code> permettent respectivement
d'obtenir et de définir le titre du processus. Ainsi :</p>
<pre><code class="language-php">Hoa\Console\Processus::setTitle('hoa #1');</code></pre>
<p>Et dans un autre terminal :</p>
<pre data-line="2"><code class="language-shell">$ ps | grep hoa
69578 ttys006 0:00.01 hoa #1
70874 ttys008 0:00.00 grep hoa</code></pre>
<p>Ces méthodes sont très pratiques lorsque nous manipulons beaucoup de
processus et que nous voulons les identifier efficacement (par exemple avec
des outils comme <code>top</code> ou <code>ps</code>). Notons qu'elles ne sont
fonctionnelles que si vous avez PHP5.5 au minimum.</p>
<p>Une autre méthode statique intéressante est
<code>Hoa\Console\Processus::locate</code> qui permet de déterminer le chemin
vers un programme. Par exemple :</p>
<pre><code class="language-php">var_dump(Hoa\Console\Processus::locate('php'));
/**
* Could output:
* string(12) "/usr/bin/php"
*/</code></pre>
<p>Dans le cas où le programme n'est pas trouvé, <code>null</code> sera
retournée. Cette méthode se base sur le <code>PATH</code> de votre
système.</p>
<h3 id="Interactive_processus_and_pseudo-terminals" for="main-toc">Processus
interactifs et pseudo-terminaux</h3>
<p>Cette section est un peu plus technique mais explique un
<strong>problème</strong> qui peut arriver avec certains processus dits
<strong>interactifs</strong>.</p>
<p>La classe <code>Hoa\Console\Processus</code> permet d'automatiser
l'interaction avec des processus très facilement. Toutefois, ce n'est pas
toujours possible de créer cette automatisation, à cause du comportement du
processus. Nous allons illustrer le problème en écrivant le fichier
<code>Interactive.php</code> :</p>
<pre><code class="language-php">&lt;?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;</code></pre>
<p>Exécutons ce processus pour voir ce qu'il fait :</p>
<pre><code class="language-shell">$ php Interactive.php
Login: myLogin
Password: myPassword
Result:
myLogin
myPassword</code></pre>
<p>Et maintenant, automatisons l'exécution de ce processus :</p>
<pre><code class="language-shell">$ echo 'myLogin\nmyPassword' > data
$ php Interactive.php &lt; data
Login: Password: Result:
myLogin
myPassword</code></pre>
<p>Excellent. Nous pourrions avoir le même résultat avec
<code>Hoa\Console\Processus</code> sans problème. Maintenant, si notre
processus veut s'assurer que <code>STDIN</code> est vide entre deux entrées,
il peut ajouter :</p>
<pre data-line-offset="7" data-line="10"><code class="language-php">}
fseek(STDIN, 0, SEEK_END);
echo 'Password: ';</code></pre>
<p>Et alors dans ce cas, si nous essayons d'automatiser l'exécution :</p>
<pre><code class="language-shell">$ php Interactive.php &lt; data
Login: Password: Hmm, no password.</code></pre>
<p>C'est un comportement tout à fait normal, mais
<code>Hoa\Console\Processus</code> ne peut rien faire pour remédier à ce
problème.</p>
<p>La solution serait d'utiliser un
<a href="https://en.wikipedia.org/wiki/Pseudo_terminal">pseudo-terminal</a> en
utilisant les fonctions PTY (voir
<a href="http://kernel.org/doc/man-pages/online/pages/man7/pty.7.html"
title="pty(7), Linux Programmer's Manual">dans Linux</a> ou
<a href="http://freebsd.org/cgi/man.cgi?query=pty"
title="pty(3), FreeBSD Library Functions Manual" >dans FreeBSD</a>).
Malheureusement ces fonctions ne sont pas disponibles dans PHP pour des
raisons techniques. Il n'y a pas de solution possible en PHP pur, mais il
est toujours envisageable d'utiliser un programme <strong>externe</strong>,
écrit par exemple en C.</p>
<h2 id="Conclusion" for="main-toc">Conclusion</h2>
<p>La bibliothèque <code>Hoa\Console</code> offre des outils
<strong>complets</strong> pour écrire des programmes adaptés à une interface
<strong>textuelle</strong>, que ce soit l'interaction avec la fenêtre ou le
curseur, l'interaction avec l'utilisateur grâce à un lecteur de lignes très
personnalisable (avec de l'auto-complétion ou des raccourcis), la lecture
d'options pour les programmes eux-mêmes, la construction de programmes
élaborés, ou encore l'exécution, l'interaction et la communication avec des
processus.</p>
</yield>
</overlay>