12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784 |
- <?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>
|