nodebook
Version:
Node.js • Apprendre par la pratique. Familiarisez-vous avec JavaScript, Node.js et l'écosystème de modules npm. Apprenez à concevoir et à déployer des *applications web* et des *outils en ligne de commande*.
1,421 lines (1,337 loc) • 284 kB
HTML
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="generator" content="Asciidoctor 2.0.9">
<title>Créer un outil en ligne de commande</title>
<style>
#header,
#content,
#footer {
margin: 2rem auto;
max-width: 46rem;
}
#header {
margin-top: 0;
}
.title,
#toctitle {
color: var(--dark-accent);
}
a {
/* white-space: nowrap; */
}
img, iframe, video, audio {
max-width: 100%;
}
p {
font-weight: normal;
}
/* Taken out from book.css */
dl dt {
margin-bottom: 0.3125em;
font-weight: bold;
}
dl dd {
margin-bottom: 1.25em;
}
dt,
th.tableblock,
td.content,
div.footnote {
text-rendering: optimizeLegibility;
}
.literalblock pre,
.literalblock pre[class],
.listingblock pre,
.listingblock pre[class] {
overflow: auto;
word-wrap: break-word;
}
.literalblock pre.nowrap,
.literalblock pre[class].nowrap,
.listingblock pre.nowrap,
.listingblock pre[class].nowrap {
overflow-x: auto;
white-space: pre;
word-wrap: normal;
}
.listingblock {
margin: 0 0 2em;
}
.listingblock > .content {
position: relative;
}
.listingblock > .title {
font-weight: bold;
}
.listingblock code[data-lang]::before {
display: none;
content: attr(data-lang);
position: absolute;
font-size: 0.75em;
top: 1em;
right: 1em;
line-height: 1;
text-transform: uppercase;
color: #999;
}
.listingblock:hover code[data-lang]::before {
display: block;
}
.listingblock.terminal pre .command::before {
content: attr(data-prompt);
padding-right: 0.5em;
color: #999;
}
td.hdlist1,
td.hdlist2 {
vertical-align: top;
padding-right: 0.625em;
}
td.hdlist1 {
font-weight: bold;
}
.literalblock + .colist,
.listingblock + .colist {
margin-top: -1.5em;
}
.colist td:not([class]):first-child {
padding: 0.4em 0.75em 0 0.75em;
line-height: 1;
vertical-align: top;
}
.colist td:not([class]):first-child img {
max-width: none;
}
.colist td:not([class]):last-child {
padding: 0.25em 0;
}
/* Custom classes */
.line-through {
text-decoration: line-through;
}
.RemarquePreTitre,
#toctitle {
font-style: normal;
font-weight: bold;
}
.RemarquePreTitre::after {
content: "•";
padding-left: 5px;
}
.admonitionblock {
}
.admonitionblock > table,
.exampleblock {
--commented: rgba(17, 17, 68, .65);
--border-radius-base: 8px;
background-color: #fafafa;
border: 1px solid var(--dark-shade);
border-left: none;
border-right: none;
margin: 1.5em 0;
padding: 1em;
}
.exampleblock .title {
font-weight: bold;
}
.icon .title {
font-size: 2em;
}
.admonitionblock > table td.icon {
display: none;
vertical-align: middle;
}
@media screen and (min-width: 769px) {
.admonitionblock > table td.icon {
display: table-cell;
}
}
.admonitionblock > table td.icon {
padding-right: 1em;
}
.admonitionblock > table td.icon img {
max-width: none;
}
.colist ol {
margin-left: 1.5em; /* aligns with the listing edge */
padding-left: 0;
font-weight: bold; /* makes it stand out more */
}
.colist ol p {
margin: 0 0 .5em;
}
.listingblock:not(.prismjs) pre,
.language-bash.hljs {
background: #323232;
color: wheat;
margin: 0;
padding: 1rem;
}
.language-bash.hljs .hljs-built_in,
.language-bash.hljs .hljs-builtin-name {
color: white;
}
.language-bash.hljs .hljs-string {
color: lightgreen;
}
.language-bash.hljs .hljs-variable {
color: lightskyblue;
}
.keyseq {
font-weight: normal;
white-space: nowrap;
}
.language-bash.hljs .keyseq {
color: white;
}
.language-bash.hljs kbd {
background: transparent;
box-shadow: none;
color: white;
font-size: 0.8em;
font-weight: bold;
padding: 0.1em 0.4em;
}
.listingblock pre.highlightjs,
.listingblock pre.prismjs {
background-color: transparent;
margin: 0;
padding: 0;
}
.listingblock pre.highlightjs > code,
.listingblock pre.prismjs {
border-left: 4px solid var(--dark-accent);
padding-left: 1em;
font-size: .8em;
}
.listingblock pre.highlightjs > code.language-bash {
border-left-color: limegreen;
}
.token.comment .conum {
font-weight: normal;
}
.hdlist .hdlist1 {
text-align: right;
white-space: nowrap;
}
td > p:first-child {
margin-top: 0;
}
.hljs-comment {
font-style: normal !important;
}
#toc.toc2 a {
text-decoration: none;
white-space: normal;
}
#toc.toc2 a:hover,
#toc.toc2 a:focus {
text-decoration: underline;
}
#toc.toc2 ul {
list-style: none;
}
#toc.toc2 > ul {
padding-left: 0;
}
#toc.toc2 ul ul {
padding-left: 1em;
}
@media screen and (min-width: 769px) {
body {
padding-left: 25vw !important;
}
#header,
#content,
#footer {
margin-left: 0;
}
#toc.toc2 {
height: 100%;
left: 0;
max-width: 20vw;
overflow: auto;
padding: 1rem;
position: fixed;
top: 0;
z-index: 1000;
}
#toc.toc2 > ul {
font-size: 0.85em;
}
#toc li.active > a[href^="#"],
[id]:target {
background: #ffc;
}
}
.admonitionblock.context-callout > table {
border-width: 5px;
border-color: var(--brand-color);
}
</style>
<style type="text/css" class="prism-theme">/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*="language-"],
pre[class*="language-"] {
color: black;
background: none;
text-shadow: 0 1px white;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
font-size: 1em;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
code[class*="language-"]::selection, code[class*="language-"] ::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #f5f2f0;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #690;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #9a6e3a;
background: hsla(0, 0%, 100%, .5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function,
.token.class-name {
color: #DD4A68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
</style>
<link rel="stylesheet" href="https://oncletom.io/styles/blog.css?v=v3.3.2">
<!-- WebMentions -->
<link rel="pingback" href="https://webmention.io/oncletom.io/xmlrpc">
<link rel="webmention" href="https://webmention.io/oncletom.io/webmention">
<!-- Open Graph -->
<meta property="og:type" content="book">
<meta property="og:image" content="https://oncletom.io/images/publications/nodejs-cover.png">
<meta property="og:book:author" content="Thomas Parisot">
<meta property="og:book:isbn" content="978-2212139938">
<meta property="og:book:release_date" content="2018-12-06">
<meta property="og:book:tag" content="Node.js">
<meta property="og:book:tag" content="JavaScript">
<meta property="og:book:tag" content="npm">
<meta property="og:book:tag" content="Développement front-end">
<meta property="og:book:tag" content="Développement back-end">
<meta property="og:locale" content="fr_FR">
<meta property="og:site_name" content="Node.js • Apprendre par la pratique">
<!-- Twitter OpenGraph -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@oncletom">
<meta name="twitter:creator" content="@oncletom">
<style type="text/css" class="extension-interactive-runner">[data-interactive-runtime="loaded"] .interactive--javascript code[data-lang]:before {
background-color: #fcfcfc;
border: 1px solid #0c2;
border-radius: 1px;
color: #0c2;
cursor: pointer;
display: inline-block;
font-weight: bold;
padding: .5em 1em;
top: -2em;
}
[lang="en"] [data-interactive-runtime="loaded"] .interactive--javascript code[data-lang]:before {
content: "▶ run code";
}
[lang="fr"] [data-interactive-runtime="loaded"] .interactive--javascript code[data-lang]:before {
content: "▶ voir le résultat";
}
.interactive--javascript code[data-label]:before {
content: attr(data-label);
}
.interactive iframe {
position: absolute;
visibility: hidden;
}
.interactive.status--loaded iframe {
position: inherit;
visibility: visible;
}
.interactive.status--loading {
opacity: .5;
}
</style>
<script class="extension-interactive-runner">
(function(d){
document.addEventListener('DOMContentLoaded', function(){
const script = d.createElement('script');
script.src = 'https://embed.runkit.com/';
script.async = true;
script.onload = function(){
function makeListingInteractive (element){
if (element.classList.contains('interactive--installed') || element.classList.contains('status--loading')) {
return;
}
const code = element.querySelector('code');
const nodeVersion = /interactive--runtime--node-([^\s]+)/.exec(element.className)[1];
const isEndpoint = element.classList.contains('interactive--endpoint');
const mode = isEndpoint ? 'endpoint' : null;
let preamble = '';
const source = code
.textContent
.replace(/\/\/\s*\(\d+\)$/gm, '')
.replace(/^["']?use strict["'][; ]*\n/, '');
if (isEndpoint) {
preamble = `process.nextTick(() => {
if (typeof module.exports === 'function') {
exports.endpoint = module.exports;
}
else if (typeof server !== 'undefined') {
exports.endpoint = server.listeners("request").pop();
}
});`
}
element.classList.add('status--loading');
// eslint-disable-next-line no-undef
RunKit.createNotebook({
nodeVersion,
element,
source,
mode,
preamble,
onLoad: function(ntbk) {
element.classList.add('interactive--installed');
element.classList.remove('status--loading');
element.classList.add('status--loaded');
code.parentNode.setAttribute('hidden', true);
ntbk.evaluate();
}
});
}
function installEvents () {
function getParent(el, condition) {
let parent = el;
while(parent = parent.parentNode) {
if (condition(parent)) {
return parent;
}
}
}
function hasClass(className) {
return function check(el) {
return el.classList.contains(className);
}
}
document.querySelector('body').addEventListener('click', function(el) {
if (el.target.classList.contains('language-javascript') || el.target.classList.contains('language-js')) {
const parentNode = getParent(
el.target,
hasClass('interactive--javascript')
);
makeListingInteractive(parentNode);
}
});
}
installEvents();
document.body.dataset.interactiveRuntime = 'loaded';
};
document.body.appendChild(script);
});
})(document);</script>
<style type="text/css">
.listingblock [data-bash-subs]::before {
content: attr(data-bash-subs) " ";
opacity: .5; }
.listingblock [data-bash-conum]::before {
content: "(" attr(data-bash-conum) ")";
font-weight: bold;
opacity: .7;
}</style>
<script>
(function(d){
d.addEventListener('DOMContentLoaded', function(){
const {origin} = window.location;
Array.from(document.querySelectorAll('a[href]'))
.filter(link => link.href.indexOf(origin) !== 0)
.forEach(link => {
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener');
});
});
})(document);</script>
<script>
(function(d){
d.addEventListener('DOMContentLoaded', function(){
const script = d.createElement('script');
script.src = 'https://unpkg.com/menuspy@1.3.0/dist/menuspy.js';
script.async = true;
script.onload = () => new MenuSpy(document.querySelector('#toc'), {enableLocationHash: false});
d.body.appendChild(script);
});
})(document);</script>
<style type="text/css">
#toc li.active > a[href^="#"] {
font-weight: bold;
}
#toc li.active > a[href^="#"]::before {
content: "▶ ";
display: inline-block;
position: absolute;
margin-left: -1.2em;
font-size: .8em;
margin-top: 3px;
}
</style>
</head>
<body class="book toc2 toc-left">
<div id="header">
<h1>Créer un outil en ligne de commande</h1>
<div id="toc" class="toc2">
<div id="toctitle">Table des matières</div>
<ul class="sectlevel1">
<li><a href="#start">1. Créer un script exécutable</a>
<ul class="sectlevel2">
<li><a href="#script">1.1. Au départ, un simple script Node</a></li>
<li><a href="#permissions">1.2. Modifier les permissions du script</a></li>
<li><a href="#shebang">1.3. Préciser le contexte d’exécution (shebang)</a></li>
<li><a href="#package.json">1.4. Faire le lien avec un module npm</a></li>
</ul>
</li>
<li><a href="#interactive">2. Du script au programme interactif</a>
<ul class="sectlevel2">
<li><a href="#args">2.1. Utiliser des arguments et des options</a></li>
<li><a href="#colours">2.2. Améliorer la lisibilité grâce aux couleurs</a></li>
<li><a href="#prompt">2.3. Demander une série d’informations</a></li>
<li><a href="#progress">2.4. Informer de la progression</a></li>
<li><a href="#tables">2.5. Afficher des informations sous forme de tableau</a></li>
<li><a href="#update">2.6. Inviter à mettre à jour le module</a></li>
</ul>
</li>
<li><a href="#tests">3. Vers un code réutilisable et testable</a>
<ul class="sectlevel2">
<li><a href="#interface">3.1. Modulariser le code du fichier exécutable</a></li>
<li><a href="#tests.interface">3.2. Tester le code partagé</a></li>
<li><a href="#display">3.3. Présenter les messages en contexte</a></li>
<li><a href="#tests.cli">3.4. Tester l’exécutable</a></li>
<li><a href="#doc">3.5. Documenter notre programme</a></li>
</ul>
</li>
<li><a href="#advanced">4. Pour aller plus loin</a>
<ul class="sectlevel2">
<li><a href="#application">4.1. Utilisation d’un framework d’application en ligne de commande</a></li>
<li><a href="#files">4.2. Stratégies pour gérer les chemins d’accès</a></li>
<li><a href="#streaming">4.3. Utiliser les flux de données (stdin, stdout et stderr)</a></li>
<li><a href="#autocomplete">4.4. Activer l’autocomplétion des commandes</a></li>
<li><a href="#packaging">4.5. Rendre le programme indépendant de Node</a></li>
</ul>
</li>
<li><a href="#conclusion">5. Conclusion</a></li>
</ul>
</div>
</div>
<div id="content">
<div id="preamble">
<div class="sectionbody">
<div class="admonitionblock tip context-callout">
<table>
<tr>
<td class="icon">
<div class="title">💡</div>
</td>
<td class="content">
<div class="paragraph">
<p>Vous êtes en train de lire
le Chapitre 8
du <a href="https://oncletom.io/node.js/">livre “Node.js”</a>, écrit par
<a href="https://oncletom.io">Thomas Parisot</a> et publié aux
<a href="https://editions-eyrolles.com/Livre/9782212139938">Éditions Eyrolles</a>.</p>
</div>
<div class="paragraph">
<p>L’ouvrage vous plaît ? Achetez-le sur <a href="https://amzn.to/2E58PEw">Amazon.fr</a> ou en
<a href="https://www.placedeslibraires.fr/livre/9782212139938">librairie</a>.
<a href="https://opencollective.com/nodebook">Donnez quelques euros</a> pour contribuer
à sa gratuité en ligne.</p>
</div>
</td>
</tr>
</table>
</div>
<div class="paragraph">
<p>Créer un outil en ligne de commande est un savoir utile pour forger ses
propres outils, automatiser des actions et mieux s’intégrer au
système d’exploitation.</p>
</div>
<div class="exampleblock">
<div class="content">
<div class="ulist">
<div class="title">Sommaire</div>
<ul>
<li>
<p>Créer un script exécutable.</p>
</li>
<li>
<p>Du script au programme interactif.</p>
</li>
<li>
<p>Vers un code réutilisable et testable.</p>
</li>
<li>
<p>Utilisation d’un framework d’application en ligne de commandes.</p>
</li>
<li>
<p>Gérer les chemins d’accès et les flux de données.</p>
</li>
<li>
<p>Rendre le programme indépendant de Node.</p>
</li>
</ul>
</div>
</div>
</div>
<div class="quoteblock abstract">
<blockquote>
<div class="paragraph">
<p>Ce chapitre est une invitation à créer des applications au plus proche des
systèmes d’exploitation, là où Node excelle.</p>
</div>
<div class="paragraph">
<p>Nous apprendrons à passer d’un script Node ordinaire à un script qui
s’exécute comme un programme de notre système d’exploitation.</p>
</div>
<div class="paragraph">
<p>Nous compléterons ce programme en améliorant son expérience utilisateur
mais aussi en le rendant robuste grâce aux tests et à l’écriture d’une
documentation minimaliste, générée automatiquement.</p>
</div>
<div class="paragraph">
<p>Enfin, nous verrons aussi comment aller plus loin en organisation son code
comme dans une véritable application, avec une compréhension plus poussée
des chemins d’accès et des traitements en continu sur des flux de données.</p>
</div>
</blockquote>
</div>
<div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<div class="title">💬</div>
</td>
<td class="content">
<div class="title"><span class="RemarquePreTitre">Remarque</span> Versions de Node et npm</div>
<div class="paragraph">
<p>Le contenu de ce chapitre utilise les versions <strong>Node v10</strong>
et <strong>npm v6</strong>.
Ce sont les versions stables recommandées en 2019.</p>
</div>
</td>
</tr>
</table>
</div>
<div class="paragraph">
<p>Un script en ligne de commande revient souvent à une installation globale
d’un module <code>npm</code> (<code>npm install --global <module></code>).
Il prend aussi bien la forme d’un petit outil que d’une application complète.
Dans tous les cas, le terminal est l’interface d’affichage.
</p>
</div>
<div class="paragraph">
<p>Node est particulièrement adapté à la création d’outils en ligne de commande
grâce à son modèle de gestion mémoire et son processus unique.
Il doit toutefois partager la mémoire et les ressources de la machine avec
les autres programmes – à nous de faire le choix de la frugalité.</p>
</div>
<div class="paragraph">
<p>Ces codes nous servent à outiller nos projets, à créer des programmes autonomes,
des interfaces visuelles dans un terminal et à automatiser ce qui doit l’être.
Leur distribution sur le registre <code>npm</code>
(<a href="../chapter-05/index.html">chapitre 5</a>) en facilite l’accès et le partage,
surtout si vous les avez bien <a href="#tests">testés et documentés</a>.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="start">1. Créer un script exécutable</h2>
<div class="sectionbody">
<div class="paragraph">
<p></p>
</div>
<div class="paragraph">
<p>La première étape est de rendre exécutable votre script Node.
Le système d’exploitation ne le percevra plus comme un simple fichier texte,
mais bel et bien comme un programme, au même titre que l’exécutable <code>npm</code>.</p>
</div>
<div class="paragraph">
<p>Nous allons apprendre ce cheminement ensemble, jusqu’à rendre notre
code distribuable sous forme d’un <a href="../chapter-05/index.html#modules">module <code>npm</code></a>
(<a href="../chapter-05/index.html">chapitre 5</a>).</p>
</div>
<div class="sect2">
<h3 id="script">1.1. Au départ, un simple script Node</h3>
<div class="paragraph">
<p></p>
</div>
<div class="paragraph">
<p>Ce dont nous avons besoin pour démarrer, c’est d’un script Node que nous
pouvons appeler depuis notre terminal.
Nous allons placer l’exemple suivant dans le répertoire <code>bin</code>
(pour <em>binary</em> en anglais, c’est-à-dire <em>exécutable</em>).
Cela n’a pas d’incidence technique, mais c’est une pratique courante au sein
de la communauté Node pour repérer plus facilement les exécutables
sans ambiguïté.</p>
</div>
<div class="listingblock interactive interactive--javascript interactive--runtime--node-v10">
<div class="title">bin/time.js</div>
<div class="content">
<pre class="highlight highlight-prismjs prismjs language-javascript"><code class="language-javascript" data-lang="javascript"><span class="token string">'use strict'</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> date <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> // <b class="conum">(1)</b>
<span class="token keyword">const</span> hour <span class="token operator">=</span> date<span class="token punctuation">.</span><span class="token function">getHours</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> minutes <span class="token operator">=</span> date<span class="token punctuation">.</span><span class="token function">getMinutes</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token string">`Il est </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>hour<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">h</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>minutes<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">.`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
</div>
</div>
<div class="colist arabic">
<ol>
<li>
<p>Crée un objet qui représente la <a href="../chapter-03/index.html#date">date</a> courante (<a href="../chapter-03/index.html">chapitre 3</a>).</p>
</li>
</ol>
</div>
<div class="admonitionblock tip">
<table>
<tr>
<td class="icon">
<div class="title">💡</div>
</td>
<td class="content">
<div class="title"><span class="RemarquePreTitre">Pratique</span> Jouer avec les exemples dans un terminal</div>
<div class="paragraph">
<p>Les exemples titrés d’un nom de fichier peuvent être installés sur votre ordinateur.
Exécutez-les dans un terminal et amusez-vous à les modifier en parallèle de
votre lecture pour voir ce qui change.</p>
</div>
<div class="listingblock">
<div class="title">Installation des exemples via le module npm <code>nodebook</code></div>
<div class="content">
<pre><span data-bash-subs="$"></span>npm install --global nodebook
<span data-bash-subs="$"></span>nodebook install chapter-08
<span data-bash-subs="$"></span>cd $(nodebook dir chapter-08)</pre>
</div>
</div>
<div class="paragraph">
<p>La commande suivante devrait afficher un résultat qui confirme que vous êtes
au bon endroit :</p>
</div>
<div class="listingblock">
<div class="content">
<pre><span data-bash-subs="$"></span>node hello.js</pre>
</div>
</div>
<div class="paragraph">
<p>Suivez à nouveau les instructions d’installation pour rétablir les exemples
dans leur état initial.</p>
</div>
</td>
</tr>
</table>
</div>
<div class="paragraph">
<p>L’exécution du script avec Node retourne la date et l’heure courante – selon
l’horloge de l’ordinateur qui exécute le code.</p>
</div>
<div class="listingblock">
<div class="content">
<pre><span data-bash-subs="$"></span>node bin/time.js
Il est 13h42.</pre>
</div>
</div>
</div>
<div class="sect2">
<h3 id="permissions">1.2. Modifier les permissions du script</h3>
<div class="paragraph">
<p>
</p>
</div>
<div class="paragraph">
<p>Les systèmes d’exploitation modernes distinguent les fichiers ordinaires
des fichiers exécutables.
L’appel à un fichier exécutable se fait sans avoir à connaître quoi que ce soit
d’autre que son emplacement.</p>
</div>
<div class="paragraph">
<p>Essayons d’exécuter le script précédent pour nous en rendre compte.
Pour ce faire, nous allons l’invoquer seulement avec son chemin
– ici, son chemin relatif :
</p>
</div>
<div class="listingblock">
<div class="content">
<pre><span data-bash-subs="$"></span>./bin/time.js
sh: permission denied: ./bin/time.js</pre>
</div>
</div>
<div class="paragraph">
<p>Le système refuse de l’exécuter car les permissions du fichier ne sont pas
adéquates.
Comme nous ne les connaissons pas, utilisons la commande <code>ls</code> ainsi que
l’option <code>-l</code> pour afficher ses informations détaillées :</p>
</div>
<div class="listingblock">
<div class="content">
<pre><span data-bash-subs="$"></span>ls -l bin/time.js
<mark>-rw-r--r--</mark> oncletom staff 175 Jun 14 13:47 bin/time.js</pre>
</div>
</div>
<div class="paragraph">
<p>Cet affichage détaille les permissions du fichier, l’utilisateur et le groupe
propriétaire, son poids et enfin la date de dernière modification.</p>
</div>
<div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<div class="title">💬</div>
</td>
<td class="content">
<div class="title"><span class="RemarquePreTitre">Déchiffrer</span> Lire les permissions Unix</div>
<div class="paragraph">
<p>
Le premier caractère spécifie le type (fichier, répertoire, lien symbolique) et
ensuite, ce sont des blocs de trois caractères qui décrivent les permissions de
l’utilisateur propriétaire, du groupe propriétaire et du reste des utilisateurs
du système d’exploitation.</p>
</div>
<div class="paragraph">
<p>Chaque bloc affiche <code>r</code> s’il est lisible, <code>w</code> s’il est modifiable et
<code>x</code> s’il est exécutable – c’est ce dernier qui nous intéresse.</p>
</div>
</td>
</tr>
</table>
</div>
<div class="paragraph">
<p>Nous allons rendre le fichier exécutable (<code>+x</code>) pour notre utilisateur (<code>u</code>)
grâce à la commande <code>chmod</code>.
Je préfère utiliser cette notation car elle évite des effets de bord :</p>
</div>
<div class="listingblock">
<div class="content">
<pre><span data-bash-subs="$"></span>chmod u+x bin/time.js</pre>
</div>
</div>
<div class="paragraph">
<p>L’utilisation renouvelée de la commande <code>ls</code> confirme que la
permission <em>exécutable</em> du fichier a été attribuée à l’utilisateur propriétaire
du fichier :
</p>
</div>
<div class="listingblock">
<div class="content">
<pre><span data-bash-subs="$"></span>ls -l bin/time.js
-rwxr--r-- oncletom staff 175 Jun 14 13:47 bin/time.js</pre>
</div>
</div>
<div class="paragraph">
<p>Nous sommes accueillis avec un nouveau message d’erreur lorsque nous
tentons d’exécuter le fichier <code>bin/time.js</code> :</p>
</div>
<div class="listingblock">
<div class="content">
<pre><span data-bash-subs="$"></span>./bin/time.js
./bin/time.js: line 1: use strict: command not found
./bin/time.js: line 3: syntax error near unexpected token `('</pre>
</div>
</div>
<div class="paragraph">
<p>La bonne nouvelle, c’est que le fichier est exécutable.
Néanmoins, il semblerait que le système d’exploitation ait du mal
à l’interpréter.</p>
</div>
</div>
<div class="sect2">
<h3 id="shebang">1.3. Préciser le contexte d’exécution (shebang)</h3>
<div class="paragraph">
<p>
</p>
</div>
<div class="paragraph">
<p>Donner les permissions d’exécution à un fichier ne suffit donc pas.
Nous avons perdu un élément contextuel en supprimant l’appel à <code>node</code>
dans l’exécution du script.</p>
</div>
<div class="admonitionblock caution">
<table>
<tr>
<td class="icon">
<div class="title">⚠️</div>
</td>
<td class="content">
<div class="title"><span class="RemarquePreTitre">Interopérabilité</span> Un fonctionnement différent sous Windows</div>
<div class="paragraph">
<p>Ce mécanisme n’est pas compris par le système d’exploitation Windows.
Ce dernier utilise une surcouche qui serait trop longue à expliquer dans
cet ouvrage.</p>
</div>
<div class="paragraph">
<p>Je recommande cependant de conserver le contexte d’exécution sous Windows
car l’exécutable <code>npm</code> gère l’interopérabilité pour nous.
Nous verrons comment dans la <a href="#package.json">section suivante</a>.</p>
</div>
</td>
</tr>
</table>
</div>
<div class="paragraph">
<p>Le caractère <code>+<mark>+</code> placé en début de ligne d’un script système signale une ligne
placée en commentaire.
C’est l’équivalent de <code>+//+</code> en ECMAScript.
Il existe un cas spécial : lorsque le caractère <code>+</mark>+</code> est suivi d’un <code>!</code>
<em>et</em> lorsqu’il s’agit de la première ligne d’un fichier.
Le contenu du commentaire est alors utilisé par le système d’exploitation
pour déterminer quel programme utiliser pour interpréter le script.
C’est ce qu’on appelle <em>shebang</em>.</p>
</div>
<div class="paragraph">
<p>Modifions le script de la section précédente pour ajouter le <em>shebang</em> :</p>
</div>
<div class="listingblock">
<div class="title">bin/time-sh.js</div>
<div class="content">
<pre class="highlight highlight-prismjs prismjs language-javascript"><code class="language-javascript" data-lang="javascript">#<span class="token operator">!</span><span class="token operator">/</span>usr<span class="token operator">/</span>bin<span class="token operator">/</span>env node <span class="token operator"><</span>span data<span class="token operator">-</span>bash<span class="token operator">-</span>conum<span class="token operator">=</span><span class="token string">"1"</span><span class="token operator">></span><span class="token operator"><</span><span class="token operator">/</span>span<span class="token operator">></span>
<span class="token string">'use strict'</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> date <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token operator">...</span></code></pre>
</div>
</div>
<div class="colist arabic">
<ol>
<li>
<p>Le programme <code>/usr/bin/env</code> est exécuté avec un argument, <code>node</code>.</p>
</li>
</ol>
</div>
<div class="paragraph">
<p>Le programme <code>/usr/bin/env</code> crée un nouvel environnement d’exécution
et le reste du script est passé au programme référencé en argument – ici, <code>node</code>.
Ce nouvel environnement dure le temps de l’exécution du script.</p>
</div>
<div class="listingblock">
<div class="content">
<pre><span data-bash-subs="$"></span>./bin/time-sh.js
Il est 13h42.</pre>
</div>
</div>
<div class="paragraph">
<p>Le dernier effort à faire pour distribuer ce script exécutable de manière
interopérable est de le lier à un
<a href="../chapter-05/index.html#package.json">module <code>npm</code></a>.</p>
</div>
</div>
<div class="sect2">
<h3 id="package.json">1.4. Faire le lien avec un module npm</h3>
<div class="paragraph">
<p>
</p>
</div>
<div class="paragraph">
<p>Nous avons vu dans le <a href="../chapter-05/index.html">chapitre 5</a>
que Node utilisait la valeur <code>main</code> du fichier <code>package.json</code> pour déterminer
quel script inclure en faisant <code>require('<module>')</code> ou <code>import <module> from '<module>'</code>.</p>
</div>
<div class="paragraph">
<p>Le champ <code>bin</code> est une transposition de <code>main</code> pour associer un script
exécutable à notre module <code>npm</code> :</p>
</div>
<div class="listingblock">
<div class="title">package.json</div>
<div class="content">
<pre class="highlight highlight-prismjs prismjs language-json"><code class="language-json" data-lang="json"><span class="token punctuation">{</span>
<span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"nodebook.chapter-08"</span><span class="token punctuation">,</span>
<span class="token property">"bin"</span><span class="token operator">:</span> <span class="token string">"examples/bin/time-sh.js"</span><span class="token punctuation">,</span>
<span class="token property">"..."</span><span class="token operator">:</span> <span class="token string">"..."</span>
<span class="token punctuation">}</span></code></pre>
</div>
</div>
<div class="paragraph">
<p>Le moyen le plus simple pour tester l’intégration de l’exécutable
avec notre système d’exploitation est de
l'<a href="../chapter-05/index.html#install.global">installer globalement</a>.
L’exécutable <code>npm</code> sait aussi installer un module à partir
d’un chemin vers un répertoire contenant un fichier <code>package.json</code> :
</p>
</div>
<div class="listingblock">
<div class="content">
<pre><span data-bash-subs="$"></span>npm install --global .</pre>
</div>
</div>
<div class="paragraph">
<p>Par défaut, l’exécutable est disponible sous le nom du module en question,
déclaré dans le champ <code>name</code> du fichier <code>package.json</code> :</p>
</div>
<div class="listingblock">
<div class="content">
<pre><span data-bash-subs="$"></span>nodebook.chapter-08
Il est 13h42.</pre>
</div>
</div>
<div class="admonitionblock tip">
<table>
<tr>
<td class="icon">
<div class="title">💡</div>
</td>
<td class="content">
<div class="title"><span class="RemarquePreTitre">Pratique</span> Un autre nom ou plusieurs exécutables</div>
<div class="paragraph">
<p>
Le champ <code>bin</code> s’écrit sous forme d’un objet si vous souhaitez utiliser
un autre nom que celui du module <code>npm</code>.
La clé correspond au nom de l’exécutable tel qu’il sera utilisable sur le
système, tandis que la valeur contient le chemin d’accès au script exécutable.
Plusieurs exécutables sont alors installés si nous renseignons plusieurs
clés et valeurs.</p>
</div>
<div class="listingblock">
<div class="title">package.json</div>
<div class="content">
<pre class="highlight highlight-prismjs prismjs language-json"><code class="language-json" data-lang="json">{
"name": "nodebook.chapter-08",
"bin": {
"<mark>quelle-heure-est-il</mark>": "examples/bin/time-sh.js"
}
}</code></pre>
</div>
</div>
<div class="paragraph">
<p>L’installation de l’exécutable <code>examples/bin/time-sh.js</code> se fera sous le nom <code>quelle-heure-est-il</code>.</p>
</div>
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="interactive">2. Du script au programme interactif</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Nous avons appris à transformer un script Node ordinaire en un script
exécutable et prêt à <a href="../chapter-05/index.html#publish">publier sur un registre <code>npm</code></a>
(<a href="../chapter-05/index.html">chapitre 5</a>).</p>
</div>
<div class="paragraph">
<p>Cette section se focalise sur l’enrichissement d’un tel script pour en faire
une application plus complète, interactive et robuste, de quoi se constituer
un outillage sur mesure, partageable avec le reste de notre équipe et de
l’écosystème de modules <code>npm</code>.</p>
</div>
<div class="sect2">
<h3 id="args">2.1. Utiliser des arguments et des options</h3>
<div class="paragraph">
<p>
</p>
</div>
<div class="paragraph">
<p>Nous avons vu comment récupérer les arguments d’un script Node
en découvrant le <a href="../chapter-04/index.html#process">module <code>process</code></a>
au <a href="../chapter-04/index.html">chapitre 4</a>.
Pour rappel, la variable <code>process.argv</code> est un tableau qui contient tous
les arguments passés au script principal :</p>
</div>
<div class="listingblock">
<div class="title">options/intro.js</div>
<div class="content">
<pre class="highlight highlight-prismjs prismjs language-javascript"><code class="language-javascript" data-lang="javascript"><span class="token string">'use strict'</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> args <span class="token operator">=</span> process<span class="token punctuation">.</span>argv<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>args<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
</div>
</div>
<div class="paragraph">
<p>Cela donne le résultat suivant quand nous lançons ce script dans un terminal :</p>
</div>
<div class="listingblock">
<div class="content">
<pre><span data-bash-subs="$"></span>node options/intro.js --country FR --fast
[ '--country', 'FR', '--fast' ]</pre>
</div>
</div>
<div class="paragraph">
<p>Ce tableau est un peu “léger” car il se contente de retourner les arguments,
sans compréhension de la logique recherchée.
C’est à nous de dire que <code>FR</code> est une valeur associée à <code>--country</code>.</p>
</div>
<div class="paragraph">
<p>Dans le contexte des outils en ligne de commandes,
les arguments et les options sont des paramètres qui sont interprétés
par le programme pour contextualiser son action.
Ils fonctionnent un peu comme des arguments de
<a href="../chapter-03/index.html#function">fonction</a> et des paramètres d’URL.</p>
</div>
<div class="paragraph">
<p>L’enjeu des arguments et des options est de les
<strong>transformer en une structure de données</strong> afin de les passer en tant
que paramètres d’une fonction.</p>
</div>
<table class="tableblock frame-all grid-all stretch">
<caption class="title">Tableau 1. Représentation des arguments et des options dans un outil en ligne de commandes, une fonction et une URL</caption>
<colgroup>
<col style="width: 20%;">
<col style="width: 40%;">
<col style="width: 40%;">
</colgroup>
<thead>
<tr>
<th class="tableblock halign-left valign-top"></th>
<th class="tableblock halign-left valign-top">Options</th>
<th class="tableblock halign-left valign-top">Arguments</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">Fonction</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>prog({country: 'FR', 'fast': true})</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>prog('FR', 'fast')</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">Ligne de commande</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>prog --country FR --fast</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>prog FR fast</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">URL</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>prog/?country=FR&fast=true</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>prog/FR/fast</code></p></td>
</tr>
</tbody>
</table>
<div class="paragraph">
<p>
</p>
</div>
<div class="paragraph">
<p>Nous allons utiliser le module <code>npm</code> <em>minimist</em> (<span class="URL"><a href="https://npmjs.com/minimist" class="bare">npmjs.com/minimist</a></span>)
dans les exemples suivants.
Il prend en charge la complexité de l’interprétation de <code>process.argv</code> et je
le trouve robuste, bien testé et minimaliste.</p>
</div>
<div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<div class="title">💬</div>
</td>
<td class="content">
<div class="title"><span class="RemarquePreTitre">Histoire</span> Au commencement était getopt</div>
<div class="paragraph">
<p>
<code>getopt</code> (<span class="URL"><a href="https://linux.die.net/man/3/getopt" class="bare">linux.die.net/man/3/getopt</a></span>) est le programme Linux
qui sert à l’analyse des arguments et des options.
Les modules <code>npm</code> se calquent sur son modèle de fonctionnement.</p>
</div>
</td>
</tr>
</table>
</div>
<div id="args.options" class="paragraph">
<p>Commençons par les <strong>options</strong> et voyons ce que <em>minimist</em> affiche :
</p>
</div>
<div class="listingblock">
<div class="content">
<pre><span data-bash-subs="$"></span>node options/parse.js --country FR --fast
{ _: [], country: 'FR', fast: true } <span data-bash-conum="1"></span></pre>
</div>
</div>
<div class="colist arabic">
<ol>
<li>
<p>La valeur <code>_</code> contient les <a href="#args.arguments">arguments</a> d’exécution – nous y reviendrons.</p>
</li>
</ol>
</div>
<div class="paragraph">
<p>Les options sont adaptées pour <strong>nommer des paramètres facultatifs</strong> dont
l’ordre n’a pas d’importance, sous forme d’un “interrupteur” dans le cas
d’un booléen ou d’une valeur – nombre ou chaîne de caractères, peu importe.</p>
</div>
<div class="listingblock">
<div class="title">options/parse.js</div>
<div class="content">
<pre class="highlight highlight-prismjs prismjs language-javascript"><code class="language-javascript" data-lang="javascript"><span class="token string">'use strict'</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> parse <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'minimist'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> args <span class="token operator">=</span> <span class="token function">parse</span><span class="token punctuation">(</span>process<span class="token punctuation">.</span>argv<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>args<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
</div>
</div>
<div class="paragraph">
<p>Les options s’écrivent sous une <strong>forme raccourcie</strong> (<em>alias</em>).
Un alias réduit l’encombrement visuel et est signalé avec un seul tiret
(au lieu de deux pour leur forme complète) :
</p>
</div>
<div class="listingblock">
<div class="content">
<pre><span data-bash-subs="$"></span>node options/alias.js <mark>-c</mark> FR
{ _: [], c: 'FR', country: 'FR' }</pre>
</div>
</div>
<div class="listingblock">
<div class="title">options/alias.js</div>
<div class="content">
<pre class="highlight highlight-prismjs prismjs language-javascript"><code class="language-javascript" data-lang="javascript"><span class="token string">'use strict'</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> parse <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'minimist'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> alias <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token string">'c'</span><span class="token punctuation">:</span> <span class="token string">'country'</span> <span class="token punctuation">}</span><span class="token punctuation">;</span> // <b class="conum">(1)</b>
<span class="token keyword">const</span> args <span class="token operator">=</span> <span class="token function">parse</span><span class="token punctuation">(</span>process<span class="token punctuation">.</span>argv<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> alias <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>args<span class="token punctuation">)</span><span class="token punctuation">;</span> // <b class="conum">(2)</b></code></pre>
</div>
</div>
<div class="colist arabic">
<ol>
<li>
<p>Définition de l’option <code>-c</code> en tant qu’alias de <code>--country</code>.</p>
</li>
<li>
<p>L’affichage représente à la fois la valeur de l’option et celle de l’alias.</p>
</li>
</ol>
</div>
<div class="paragraph">
<p>La lecture est rendue plus difficile pour celui ou celle qui n’a pas consulté
le manuel d’utilisation en détail – ce qui est souvent le cas, surtout
pour des personnes qui découvrent un nouveau logiciel.</p>
</div>
<div class="paragraph">
<p>J’ai tendance à utiliser les alias pour les options principales ou importantes.
Je privilégie la forme longue dans les exemples et dans la documentation,
afin d’augmenter les chances de compréhension.</p>
</div>
<div class="paragraph">
<p>Les <strong>valeurs par défaut</strong> simplifient le paramétrage en rendant certaines
valeurs implicites :
</p>
</div>
<div class="listingblock">
<div class="content">
<pre><span data-bash-subs="$"></span>node options/defaults.js --fast
{ _: [], country: 'FR', fast: true } <span data-bash-conum="1"></span></pre>
</div>
</div>
<div class="colist arabic">
<ol>
<li>
<p>La clé <code>country</code> affiche une valeur alors que nous ne l’avons pas spécifiée dans la commande.</p>
</li>
</ol>
</div>
<div class="paragraph">
<p>L’enjeu réside donc dans l’utilisation des valeurs à bon escient, pour
que le programme fasse ce qui est attendu d’un point de vue utilisateur.
Le paramétrage est similaire à celui des <em>alias</em>.</p>
</div>
<div class="listingblock">
<div class="title">options/defaults.js</div>
<div class="content">
<pre class="highlight highlight-prismjs prismjs language-javascript"><code class="language-javascript" data-lang="javascript"><span class="token string">'use strict'</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> parse <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'minimist'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> options <span class="token operator">=</span> <span class="token punctuation">{</span>defaults<span class="token punctuation">:</span> <span class="token punctuation">{</span>country<span class="token punctuation">:</span> <span class="token string">'FR'</span><span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">;</span> // <b class="conum">(1)</b>
<span class="token keyword">const</span> args <span class="token operator">=</span> <span class="token function">parse</span><span class="token punctuation">(</span>process<span class="token punctuation">.</span>argv<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">,</span> options<span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>args<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
</div>
</div>
<div class="colist arabic">
<ol>
<li>
<p>L’option <code>--country</code> aura <code>FR</code> comme valeur par défaut.</p>
</li>
</ol>
</div>
<div class="paragraph">
<p>Un autre concept utile est celui des <strong>types</strong>.
Nous définissons explicitement nos attentes sur ce que telle ou telle option
est censée recevoir