Accueil

Avant-propos

Ce texte est le document qui m'a servi pour mon exposé aux Journées Perl Francophones 2014 sur la façon de rédiger de la documentation utilisateur pour les scripts shell simples. Le texte est plus fourni que mon exposé oral pour deux raisons. D'une part, quand je me suis lancé dans l'exposé, j'ai oublié de me référer à mon papier et du coup j'ai oublié de nombreux arguments auxquels j'avais pensé lors de la préparation. D'autre part, j'ai réintégré quelques réflexions faites par l'auditoire.

En outre, j'avais préparé mon exposé sur mon eeePC, notamment les démos. Le problème est que mon eeePC a refusé de collaborer avec le vidéo-projecteur et j'ai dû utiliser un autre PC. Et cet autre PC n'avait pas tout-à-fait la même configuration que mon eeePC donc les démos fonctionnaient différemment. Lorsque je voulais faire planter un programme, celui-ci fonctionnait sans problème, lorsque je voulais exécuter un programme pour qu'il affiche quelque chose de bien, ce programme n'était pas installé.

Préalable : documentation utilisateur != commentaires de programme

Une remarque préliminaire : dans un programme, il y a la documentation utilisateur et il y a la documentation pour le programmeur et le mainteneur. Ce sont deux documentations différentes, au contenu différent, pour des publics différents.

La documentation pour le programmeur et le mainteneur prend la forme, en général, de commentaires. « # » en shell et en Perl, « /* ... */ » en C traditionnel, « // » en C++ et en C moderne, « -- » en Lua et en Ada, etc. Et elle colle à l'ordre dans lequel les instructions sont codées.

La documentation utilisateur est plus globale. Elle n'entre pas dans les détails d'implémentation, sauf si c'est absolument indispensable.

Scripts shells : première catégorie, les scripts provenant de grands projets

La plupart des grands projets (GNU/Linux, Apache, Perl, Mozilla, etc) fournissent un ou plusieurs exécutables écrits avec un langage compilé (C, C++, autre) ou un langage de script évolué (Perl, Ruby, Python, etc). Mais ces projets incluent aussi des scripts shell pour des tâches annexes. Comment la documentation utilisateur est-elle implémentée ?

Documentation : première solution, manpage pour Unix

Lorsqu'un script shell fait partie d'un projet de taille respectable et utilisé par des dizaines de milliers d'utilisateurs, voire des millions, il est livré avec sa documentation utilisateur. Celle-ci est livrée sous la forme d'une « manpage ».

Documentation : deuxième solution, texinfo pour GNU/Linux

Lorsque le projet fait partie de GNU/Linux, la documentation utilisateur est sous la forme d'un document texinfo, avec des renvois d'une page à l'autre sous forme de liens hypertextes. Il existe aussi une manpage, mais en général cette manpage est assez réduite et son but essentiel est de renvoyer à la documentation sous texinfo.

Examen (très subjectif) de ces deux variantes

Avantages :

Inconvénients, dans l'hypothèse où c'est moi qui devrais écrire cette documentation :

Mais heureusement ce n'est pas moi qui m'occupe de ces scripts et de leur documentation utilisateur, je m'accommode de ce type de documentation.

Scripts shell : deuxième catégorie, les wrappers

J'ai mentionné la surcharge cognitive que risque de subir un utilisateur qui prend connaissance de toutes les possibilités offertes par un script. En général, pour s'éviter cette surcharge, l'utilisateur se dépêche d'écrire un « emballage », ou un « wrapper », qui reprend les options nécessaires, puis il oublie la quasi-totalité de ce qu'il a lu dans la manpage.

En plus, cela permet à l'utilisateur de coder certains paramètres en dur, comme les noms de répertoire en vigueur sur son projet. Ou bien d'utiliser quelques variables d'environnement communes à plusieurs wrappers de ce type.

En revanche, l'utilisateur fait en général l'impasse sur la documentation : créer une manpage ou un document texinfo pour un simple wrapper, c'est écraser une mouche avec un marteau-pilon.

Voici un exemple que j'ai écrit en 1993. Tout d'abord, quelques mots pour resituer l'environnement technique et l'environnement humain de ce script. Il s'agit d'un projet sur lequel j'ai travaillé de décembre 1992 à octobre 1993, puis de nouveau d'août 1994 à décembre 1994. De l'origine à mon départ temporaire en octobre 1993, ce projet était un projet HP-UX + C + Oracle + SQL*Forms. Chaque personne disposait d'un terminal de type VT220, donc en mode caractères, avec 24 lignes de 80 caractères. À mon retour en août 1994, jusqu'à mon départ définitif en décembre 1994, le projet avait migré sur AIX, mais c'était toujours Oracle + C + SQL*Forms. Et chacun avait maintenant un PC sous Windows, avec un émulateur de terminal appelé « Reflection 2 ».

L'environnement humain : lors de la première époque, il y avait une trentaine de personnes, réparties en une bonne vingtaine de pisseurs de lignes (lignes de code ou lignes de spécifications) et une petite dizaine de geeks et de proto-geeks. À mon retour en 1994, l'équipe était plus réduite et la proportion de pisseurs de ligne et de geeks et proto-geeks s'était équilibrée, voire inversée.

La plupart des éditions se faisaient en mode caractères, avec la taille traditionnelle de 132 caractères par ligne. Difficile à maquetter avec un éditeur sur un terminal en mode 24 x 80. Au fil de mes lectures de documentation papier et de manpages, j'ai découvert que certains terminaux de type VT220 pouvaient passer en mode 132 caractères (je ne me souviens plus du nombre de lignes) avec une séquence Escape. Mais pour bien faire, il fallait aviser l'ordinateur central que le terminal avait changé de mode et cela se faisait avec la commande stty et des paramètres compliqués. Donc, pour éviter de me référer systématiquement à la documentation, j'ai réuni cette séquence Escape et cette commande stty dans le script ci-dessous. Remarque : ce n'est pas tout-à-fait la version d'époque, j'ai enlevé les commentaires "corporate". Et aussi... voir point suivant.

Exemple, script permettant de lancer vi en mode 132 colonnes

#!/bin/sh
TERM=vt220-w;export TERM
stty columns 132
echo '\033[?3h'     # Escape sequence for 132-column mode
tabs
vi $*
echo '\033[?3l'     # Escape sequence for 80-column mode
stty columns 80

Avantage

Pas de surcharge cognitive, l'utilisateur peut consacrer la totalité de ses méninges à son problème.

Inconvénient

À son retour de vacances, l'utilisateur a oublié quels sont les prérequis pour le fonctionnement de son script (*). Et les commentaires (documentation programmeur) ne lui sont d'aucune utilité, car leur rôle est d'expliquer le pourquoi de telle ou telle instruction. Peut-être que cela aurait été utile de rédiger une documentation utilisateur ?
(*) Bon, le but de cet exemple est limpide, donc à mon retour après une absence de 10 mois, je n'ai eu aucun mal à comprendre à quoi il servait. J'ai eu plus de mal avec d'autres scripts que j'avais écrits. Mais ces scripts étaient nettement plus longs, donc moins adaptés à une présentation aux Journées Perl.

Documentation : troisième solution, texte inclus dans le source shell

Cette solution est celle qui était en place sur le projet de 1993--1994. Elle consiste à écrire la documentation utilisateur directement à l'intérieur du script. J'ai rétabli cette documentation utilisateur et la documentation programmeur, mais j'ai laissé de côté les commentaires "corporate". Et vous avez droit à la version mise à jour pour AIX + Reflection 2, pas la version initiale HP-UX + VT220 dont je n'ai pas conservé la trace.

#!/bin/sh
export utilname=$0
#
#---  keyword analysis loop
for i
do 
  case $i in
#--- user ask help manual  
    help)
      echo "\f
============================================================================
                            $utilname
                            ===========
purpose : 
        edit one or more file(s) in wide mode (132 chars)
        Originally used for QVT321 and QVT323  devices,  it  has  been
        successfully tested on Reflection 2. So, why not...
keywords :
   ===> help
        Display text concerning usage of this utility
parameters :
   ===> one or more file names
result :
   Change Reflection 2 display terminal settings and enter vi.

============================================================================" |
      more
      exit 0;;
  esac
done
#
# Attention : viw n'est pas compatible avec tsm !
#                           Mais comme on n'a plus tsm...
# les séquences escape sont extraites du "QVT321 setup guide" et ont été
# testées sur un QVT323.
# De temps en temps, la séquence escape pour 80 colonnes n'est pas interprétée.
# Dans ce cas, la renvoyer en utilisant le script shell c80.
#
# TERM=vt100w;export TERM
TERM=vt220-w;export TERM
stty columns 132
echo '\033[?3h'     # Escape sequence for 132-column mode
tabs
vi $*
echo '\033[?3l'     # Escape sequence for 80-column mode
stty columns 80

Avantages

Inconvénients

Première expérience : faire planter un script shell correct

#!/bin/sh
# -*- encoding: utf-8; indent-tabs-mode: nil -*-
#
# Script shell qui plante

echo 1
sleep 6
echo 2
sleep 2

Mode d'emploi :

  1. éditer le script dans un xterm
  2. insérer 4 caractères dans le commentaire de la ligne 2 mais ne pas sauvegarder tout de suite
  3. lancer le script dans une autre fenêtre
  4. sauvegarder le script dans la première fenêtre.

Alors le script plante en tentant d'exécuter une commande p 2.

On en déduit que l'interpréteur shell ne lit pas le script en totalité mais qu'il en lit un peu, l'exécute, en lit un peu plus, exécute la suite, en lit encore un peu plus, et ainsi de suite. Si la lecture se fait petit à petit, c'est que l'analyse syntactique se fait aussi petit à petit.

La conclusion est que si un shell se termine en plein milieu, à cause d'un exit, par exemple, alors le reste du fichier n'est pas analysé syntactiquement. Il pourrait donc contenir quelque chose qui n'a rien à voir avec le langage shell.

Deuxième expérience : exécuter un script shell avec des erreurs de syntaxe

#!/bin/sh
# -*- encoding: utf-8; indent-tabs-mode: nil -*-
#
# Script shell avec erreurs de syntaxe mais qui ne plante pas

echo 1
sleep 2
echo 2
sleep 2
exit 0
ga bu zo meu
!v_'(=é'ç&;!
àfdoodsofpatiatà'z'(àz('adakfsdai'zifaa'(atfraere'""é-'&-'e-"fgaef"'-

Et vous pouvez essayer, ce script fonctionne parfaitement, sans aucun message d'erreur. Je viens ainsi de retrouver le fonctionnement des vrais interpréteurs, comportement que j'avais oublié à force d'utiliser Perl qui comporte une phase de compilation juste avant l'exécution.

Troisième expérience :

Acme::MetaSyntactic::{olympics,tour_de_france,good_omens,soviet}

Avez-vous consulté les sources de : Acme::MetaSyntactic::tour_de_france, Acme::MetaSyntactic::olympics, Acme::MetaSyntactic::good_omens ou Acme::MetaSyntactic::soviet ?

Les deux premiers sont d'Abigail, les deux suivants sont de moi. Le fonctionnement est basé sur le squelette suivant :

my @noms = extraire(<<'=cut');
=over 4

=item Foxbat

MiG-25

=item Fishbed

MiG-21

=back

=cut

L'idée d'extraire les noms d'un here-document n'est une surprise pour aucune personne même connaissant moyennement Perl. L'idée d'extraire les noms en analysant un here-document qui respecte la syntaxe POD peut sembler légèrement excentrique, mais cela n'intrigue personne. En revanche, la trouvaille d'Abigail est que le here-document sera traité par perldoc, au même titre que les pavés POD insérés entre deux instructions élémentaires du programme Perl.

La conclusion que j'en ai tirée, c'est que l'analyseur syntactique de POD n'effectue aucune analyse du Perl englobant. Dans le cas présent, il ignore que le source POD se trouve dans un here-document. Peut-être même qu'on pourrait coller la documentation POD dans un fichier qui ne contient aucune ligne de Perl...

Documentation : première étape vers la solution

#!/bin/sh
# -*- encoding: utf-8; indent-tabs-mode: nil -*-
#
# Script shell avec mode d'emploi

echo 1
sleep 2
echo 2
sleep 2
exit 0

=head1 Mode d'emploi

Ce script permet de compter de 1 jusqu'à 2.

=head1 Paramètres et options

Il n'y a aucun paramètre ni aucune option.

Voici la première tentative (en fait sur un autre programme shell) que j'ai essayée. Cela va fonctionner ? Éh bien, lorsque j'ai lancé perldoc ex4, j'ai eu un message d'erreur, me signalant que perldoc a cru détecter des caractères UTF-8 et que cela le dérange un peu.

Documentation : la solution

#!/bin/sh
# -*- encoding: utf-8; indent-tabs-mode: nil -*-
#
# Script shell avec mode d'emploi

echo 1
sleep 2
echo 2
sleep 2
exit 0

=encoding utf8

=head1 Mode d'emploi

Ce script permet de compter de 1 jusqu'à 2.

=head1 Paramètres et options

Il n'y a aucun paramètre ni aucune option.

Comme précédemment, avec une directive =encoding.

Avec cela, vous n'avez aucun problème pour afficher la documentation en 5.10 ou plus. Peu importe où se trouve votre script dans le $PATH, perldoc le trouvera. Et vous pouvez même convertir en HTML, en LATEX, avec les utilitaires POD habituels.

Et pour le déploiement, rien de changé, un seul fichier à transmettre pour partager à la fois l'exécutable et son mode d'emploi avec vos collègues.

Seul inconvénient : perldoc n'est pas installé partout. Notamment, il n'était pas installé sur le PC que j'ai utilisé lors de mon exposé aux Journées Perl. Même si perl se trouve sur quasiment tous les systèmes Unix, ce n'est pas le cas de perldoc.

Évidemment, un script pour compter de 1 jusqu'à 2, ce n'est pas follichon. Et un script pour éditer des fichiers en 132 caractères, cela fait dépassé à l'époque des éditeurs compatibles avec X11R6. Voici un script réel et actuel, que j'utilise pour sauvegarder mon répertoire /home/jf, à l'exception de quelques gros répertoires variant peu. Et comme cela prend du temps, j'ai mis dans la documentation de quoi m'aider à prendre mon mal en patience.

#!/bin/sh
# -*- encoding: utf-8; indent-tabs-mode: nil -*-
#
# Sauvegarde sur disque externe Toshiba
#

export sv='/media/TOSHIBA EXT'
export d=$(date +'%Y-%m-%d')

if [ ! -d "$sv" ]
then
  echo "Le disque n'est pas branché"
  exit 1
fi

# Pour éviter de la copie inutile
rm tmp/sv*iso

if [ ! -d "$sv/sv-$d" ]
then
  mkdir "$sv/sv-$d"
fi

for i in * .*
do
  case $i in
    .|..|Vidéos|tex-live|dwhelper)
      echo pas de sauvegarde de $i
      ;;
    .goutputstream*)
      ;;
    *)
      date +"%X $i"
      cp -a -r $HOME/$i "$sv/sv-$d"
      ;;
  esac
done

exit 0;

=encoding utf-8

=head1 Remarques

Pas de sauvegarde de F<dwhelper>, de F<tex-live>, ni de F<Vidéos>,.

Prévoir 1 h 12 mn et 27 Go (mesuré en avril 2014). En juin 2014,
c'était 1 h 21 mn et 28 Go.

Répertoires longs à sauvegarder (mesure de juin 2014) :

  Documents        6 mn   4,3 Go
  Téléchargements 16 mn   7,6 Go
  msg             16 mn   1,7 Go
  perl5            5 mn     740 Mo
  .local          18 mn   4,0 Go

Variantes

La première variante figure dans l'article de Wikipedia sur POD. Elle repose sur l'utilisation du "noop" shell, ":".

#!/bin/sh
# -*- encoding: utf-8; indent-tabs-mode: nil -*-
#
# Script shell avec mode d'emploi

echo 1
sleep 2
echo 2
sleep 2

:<<=cut

=encoding utf8

=head1 Mode d'emploi

Ce script permet de compter de 1 jusqu'à 2.

=head1 Paramètres et options

Il n'y a aucun paramètre ni aucune option.

=cut

La seconde variante est plus élémentaire. Elle repose sur les propriétés du fichier /dev/null.

#!/bin/sh
# -*- encoding: utf-8; indent-tabs-mode: nil -*-
#
# Script shell avec mode d'emploi

echo 1
sleep 2
echo 2
sleep 2

echo <<=cut > /dev/null

=encoding utf8

=head1 Mode d'emploi

Ce script permet de compter de 1 jusqu'à 2.

=head1 Paramètres et options

Il n'y a aucun paramètre ni aucune option.

=cut

On peut envisager également de créer une variable bidon qui contiendra la documentation utilisateur mais qui ne sera pas utilisée dans le reste du script shell. Question style de programmation, je préfère la version de base avec exit.

Extension du domaine de la lutte : Lua

Avec les langages qui admettent des commentaires multi-lignes, il est facile d'insérer du POD dans le source. Exemple en Lua :

#!/usr/bin/lua
-- -*- encoding: utf-8; indent-tabs-mode: nil -*-
--
-- Script lua avec mode d'emploi

print(1);
print(2);
--[[

=encoding utf8

=head1 Mode d'emploi

Ce script permet de compter de 1 jusqu'à 2.

=head1 Paramètres et options

Il n'y a aucun paramètre ni aucune option.

=cut

]]

Et perldoc ex7.lua vous donnera le mode d'emploi de ce script Lua.

Extension du domaine de la lutte : LATEX

\documentclass{article}
\begin{document}
1

2
\end{document}

=encoding utf8

=head1 Utilisation

Ce document énumère les nombres entiers de 1 jusqu'à 2.

C'est tout.

On peut même documenter un source LATEX. À vrai dire, je ne voyais pas d'utilité pour ce faire, mais des personnes assistant à mon exposé m'ont dit que cette documentation pouvait par exemple rappeler la suite de commandes permettant de compiler ce document.

Extension du domaine de la lutte : Les langages compilés

Lors de mon exposé, on m'a demandé ce qui se passerait avec les langages compilés. J'avoue ne pas y avoir réfléchi ni, a fortiori, avoir expérimenté cet aspect des choses. Certes, avec un commentaire multi-ligne, on peut insérer du source POD dans le source du programme. Mais les commentaires ne sont pas repris dans les exécutables. Et perldoc n'ira pas chercher dans les répertoires contenant les sources, puisque ces répertoires ne figurent pas dans le $PATH. Ou alors, si le langages admet des chaînes littérales multilignes, alimenter une variable bidon avec un littéral contenant le source POD ? Mais les compilateurs d'aujourd'hui comportent un optimiseur capable de reconnaître les variables bidons, qui sont alimentées mais jamais lues et ils les font disparaître de l'exécutable.

Problème ?

Ne risque-t-il pas d'y avoir des problèmes si un langage utilise un « = » en début de ligne ? Je ne connais aucun langage qui impose d'écrire un « = » en début de ligne (je ne prend pas en considération les langages gags tels que INTERCAL, Befunge ou Brainfuck). En revanche, tous les langages admettent un = en début de ligne, sauf quelques-uns qui s'appuient sur des marges (COBOL, FORTRAN, RPG) ou sur l'indentation du code (Python). Par exemple, il est licite d'écrire en Perl :

sub head1 { return "<h1>$_[0]</h1>"; }
sub head2 { return "<h2>$_[0]</h2>"; }

my $variable_1

=head1 "toto";

no strict 'subs';
my $variable_2

=head2 foobar;

Mais qui peut avoir envie de faire cela, à part pour des questions d'obfuscation ? Même les golfeurs ne le feraient pas. Ou plutôt, surtout les golfeurs, pour qui les commentaires et la documentation utilisateur sont très pénalisants.

Conclusion

En fait, j'hésite à qualifier mon astuce. Au début, je la trouvais absolument géniale, puis au fur et à mesure que j'y réfléchis, je la trouve évidente. Et j'ai eu les deux types de réaction de la part de mon auditoire : un participant aux Journées Perl m'a déclaré que cela l'avait impressionné, tandis qu'un autre m'a appris que d'autres personnes (et pas seulement Abigail) y avaient pensé avant moi, puisque le code source de Parrot est commenté avec des commentaires multi-lignes respectant la syntaxe POD. Néanmoins, si mon exposé a pu servir de rappel à ceux qui avaient oublié ou négligé les possibilités de POD et de perldoc, ou s'il a pu ouvrir de nouveaux horizons à ceux qui ne savaient pas...

License

Texte diffusé sous la license CC-BY-NC-ND : Creative Commons avec clause de paternité, excluant l'utilisation commerciale et excluant la modification.

Accueil