Quelques mois plus tôt…
En ce moment je travaille sur debootstick, un outil pour générer des systèmes bootables. Mon but final est de pouvoir enchaîner les commandes suivantes sur un système Debian (ou Ubuntu...) :
$ debootstrap jessie os_tree
$ debootstick os_tree usb.img
L’outil debootstrap (à ne pas confondre avec debootstick donc !) utilisé en première ligne est un grand classique des systèmes Debian. Il permet de générer dans le répertoire donné en paramètre (ici os_tree) une arborescence minimale d’OS Debian (ici en version jessie). Mon outil debootstick se chargera alors, dans un deuxième temps, de convertir cette arborescence en image bootable (usb.img dans cet exemple).
On pourra ensuite écrire l’image bootable obtenue sur une clé USB (ici /dev/sdb) :
$ dd bs=10M if=usb.img of=/dev/sdb
Et l’OS pourra alors être démarré sur n’importe quelle machine.
Vous vous dites peut-être qu’on pourrait simplifier la chose, côté utilisateur, en intégrant dans debootstick l’appel préliminaire à la commande debootstrap. C’est d’ailleurs ce que font, en général, les outils de ce genre. De mon côté, j’ai préféré suivre la philosophie UNIX et restreindre l’outil à une seule fonction « atomique ». Cela rendra l’outil plus souple et, paradoxalement, plus puissant. En effet, on peut imaginer toute une gamme d’autres scénarios pour générer ou customiser notre arborescence os_tree. Par exemple :
$ docker run --name jessie-container -it debian:jessie /bin/sh
[docker-shell]$ [... cutomize ...]
[docker-shell]$ exit
$ mkdiros_tree
$ docker export jessie-container |tar xf - -C os_tree
$ debootstick os_tree usb.img
Et voilà. Après transfert sur une clé USB, on peut faire booter sur une machine physique ce qui n’était jusqu’à présent qu’un conteneur virtuel. Sympa, non ?
Le souci, c’est que pour l’instant cet outil n’existe que dans mon imagination. J’en suis dans les premières phases de développement, et je n’avance pas bien vite. Ce que je vous ai présenté comme une simple « conversion » cache en réalité une certaine complexité : il faut créer un fichier image, le partitionner, demander au noyau de créer des devices virtuels pour manipuler ces partitions, initialiser les systèmes de fichiers, faire des mount et des chroot, copier l’arborescence, installer les éventuels paquets manquants (noyau linux, bootloader, etc.), installer les bootloaders (BIOS et UEFI), puis tout redéfaire. Et je simplifie pas mal de choses (par exemple, pour obtenir une image de taille minimale, il faut ruser). Le plus ennuyeux, c’est quand on rencontre une erreur au beau milieu de l’exécution. Le cas typique, c’est le « no space left on device ». Le script s’arrête, et il laisse votre machine dans un état imprévisible, avec des montages sur un fichier temporaire qui n’existe plus, etc. Si on essaie de faire le ménage à la main, on y passe un bon quart d’heure. Jusque-là, j’optais donc pour la méthode Windows : je rebootais la machine. Mais je perds clairement trop de temps.
Pour résoudre ce souci, il faudrait que debootstick enregistre cet empilement de commandes délicates. En cas d’imprévu, il serait alors capable de tout défaire, en dépilant. S’il était écrit en Python par exemple, on pourrait utiliser des blocs with imbriqués. Mais debootstick est clairement orienté « système », donc le shell est plus approprié. Et en shell, pas de bloc with !
Pardon ? On me dit dans l’oreillette que vous ne seriez pas contre un petit rappel sur ce fameux bloc with. Faisons donc le point sur cette construction, telle qu’elle apparaît en Python.
1. Bloc « with » en Python
Prenons pour exemple la maxime : « Avec ce marteau, tous les problèmes ressemblent à des clous » :
problemes =['clou1','clou2']
with prendre_marteau()as marteau:
for probleme in problemes:
marteau.clouer(probleme)
On voit déjà que le bloc with met en évidence l’outil (ici le marteau), ainsi que le périmètre de son utilisation, le tout de manière assez élégante. Mais allons un peu plus loin. En fait, il y a du code caché qui s’exécute à l’entrée et à la sortie du bloc. Complétons notre exemple :
#!/usr/bin/env python
# -*- coding: utf8 -*-
importsys
class Marteau(object):
def __enter__(self):
print('marteau en main')
returnself
def __exit__(self,type, value, tb):
print('marteau rangé')
def clouer(self, objet):
assert(objet.startswith('clou')), \
"Je ne peux pas clouer '%s'!"%objet
print(objet +' cloué.')
def prendre_marteau():
return Marteau()
problemes =sys.argv[1:]
with prendre_marteau() as marteau:
for probleme in problemes:
marteau.clouer(probleme)
print('Au revoir!')
Voilà ce que ça donne à l’exécution :
$ ./test.py clou1 clou2
marteau en main
clou1 cloué.
clou2 cloué.
marteau rangé
Au revoir!
$
Et voici quelques explications.
1) Quand l’interpréteur Python a rencontré notre bloc with, il exécute prendre_marteau().
2) S’agissant d’un bloc with, le résultat de prendre_marteau() doit être un objet qui implémente les méthodes spéciales __enter__() et __exit__(). On appelle cet objet un context manager. L’interpréteur appelle alors la méthode __enter__() du context manager.
3) La méthode __enter__() renvoie un résultat, ici self (c’est souvent le cas), et ce résultat est assigné à la variable spécifiée après l’instruction as (ici marteau).
4) L’interpréteur exécute le contenu du bloc with.
5) En sortie du bloc, l’interpréteur appelle la méthode __exit__() du context manager.
6) L’exécution reprend après le bloc with.
Et que se passe-t-il si on essaie de clouer autre chose que des clous ? Essayons :
$ ./test.py clou1 vis clou2
marteau en main
clou1 cloué.
marteau rangé
Traceback (most recent call last):
File [...]
AssertionError: Je ne peux pas clouer 'vis'!
$
Pas de problème pour clouer clou1, en revanche la même tentative sur vis a levé une exception. Cette exception a visiblement interrompu la suite du bloc (puisque clou2 n’a pas non plus été cloué), et a provoqué une sortie prématurée du programme (puisque le message « Au revoir! » n’apparaît pas).
En revanche, on voit bien le message « marteau rangé », ce qui prouve que la fonction __exit__() a bien été appelée, pour faire le ménage en sortie du bloc with. Et ceci malgré l’exception !!
Les connaisseurs me diront qu’on pourrait obtenir le même genre de comportement en utilisant la clause finally d’un bloc try. En fait, la sémantique est quand même un peu différente, car avec un bloc with, l’exception n’est pas capturée, elle continue donc son chemin en dehors du bloc. Souvent, c’est ce qu’on attend du programme : en cas d’imprévu, on préfère sortir au plus tôt, pour limiter les dégâts. D’autre part, dans un programme où on doit « faire » puis « défaire » quelque chose, les méthodes __enter__() et __exit__() proposent une sémantique parfaite. Meilleure à mon avis qu’un bloc try...finally.
Le langage python fournit en standard des objets faisant office de context manager. C’est le cas par exemple des « objets fichiers ». Ainsi, sans connaître le détail des méthodes __enter__() et __exit__(), on peut écrire :
with open('mon_fichier.txt')as f:
print(f.read()) # ou tout autre traitement sur f
Ici, l’avantage, c’est qu’on s’assure que le fichier sera fermé en sortie de bloc, quoi qu’il arrive.
Je ne vais pas m’étaler beaucoup plus, mais sachez qu’on peut très bien imbriquer plusieurs blocs with. L’idée, c’est qu’on va empiler des traitements via les fonctions __enter__() et les dépiler via les fonctions __exit__(). Et le dépilage interviendra donc dans tous les cas (exécution normale ou exception).
Vous comprenez donc pourquoi j’aimerais pouvoir appliquer le même principe à mon script shell debootstick. Donc essayons !
2. Bloc « with » en bash (??)
2.1 Implémentation naïve
On va commencer simplement et faire notre test à partir du code bash suivant, proche de la version Python :
#!/usr/bin/env bash
source with_bloc.sh
clouer() {
objet="$1"
if [ "${objet:0:4}" != "clou" ]
then
echo"Je ne peux pas clouer '$objet'!"
return 1 # non-nul = echec
else
echo"$objet cloué."
fi
}
prendre_marteau() {
echo'marteau en main'
}
defaire_prendre_marteau() {
echo'marteau rangé'
}
problemes="$@"
with prendre_marteau
for probleme in$problemes
do
clouer"$probleme"
done
end_with
echo'Au revoir!'
À la fin du code, on voit ce qui ressemble à un bloc with, entre deux balises with et end_with. Afin qu’elles soit réutilisables, je les ai définies dans un fichier with_bloc.sh séparé, importé en deuxième ligne.
La deuxième chose à remarquer dans ce code, c’est la convention que j’ai adoptée : pour pouvoir utiliser une fonction <f>() dans un bloc with, il faudra au préalable définir la fonction defaire_<f>(). Comme vous vous en doutez, cette fonction sera appelée automatiquement au moment du end_with (avec les mêmes arguments).
Avant de vous dévoiler ce que j’ai mis dans with_bloc.sh, voyons déjà si ça marche :
$ ./test.sh clou1 clou2
marteau en main
clou1 cloué.
clou2 cloué.
marteau rangé
Au revoir!
$
Apparemment, oui, en tout cas dans ce cas simple.
Voici donc le contenu de with_bloc.sh :
1 EOL="
2 "
3 a_depiler=""
4
5 with() {
6 cmd="$*"
7 $cmd
8 a_depiler+="${EOL}defaire_$cmd"
9 }
10
11 end_with() {
12 cmd_defaire="$(echo "$a_depiler" | tail -n 1)"
13 $cmd_defaire
14 a_depiler="$(echo "$a_depiler" | head -n -1)"
15 }
Comme vous voyez, il n’y a pas des masses de code ; with et end_with sont en fait de simples fonctions. L’idée générale, c’est qu’à chaque appel with(), on exécute bien sûr la commande donnée en paramètre, mais on doit aussi stocker quelque part la commande defaire_<qqchose> <args> qui sera appelée au moment du end_with. Et pour que ça fonctionne quand on a plusieurs with imbriqués, on doit stocker ces commandes sur une pile : on empile à chaque appel with(), on dépile à chaque end_with().
Pour implémenter la pile [2], j’ai choisi de simplement chaîner des éléments dans une chaîne de caractères (variable a_depiler). Chaque élément est de la forme \n<cmd>. J’ai défini le séparateur \n via la variable EOL (lignes 1 et 2), parce qu’en bash si on écrit \n on écrit en réalité les 2 caractères backslash et n. En revanche, et c’est la raison pour laquelle j’ai choisi ce caractère, en matière de traitement ligne par ligne on est plutôt bien outillé, en shell. Ce codage permet ainsi, de manière assez évidente :
- d’obtenir le dernier élément (= le haut de la pile) avec un simple tail -n 1 (ligne 12) ;
- de dépiler ce dernier élément via head -n -1 (ligne 14).
Pour ce qui est d’empiler, dans la fonction with() donc, on a juste à utiliser la concaténation de chaîne (via l’opérateur +=, ligne 8). Vous noterez que sur cette ligne on ajoute le préfixe 'defaire_' à la commande originale, dans l’esprit de la convention décrite plus haut.
En dehors de cette gestion de pile, la ligne 7 permet d’exécuter la commande donnée en paramètre du with, et la ligne 13 permet d’exécuter la commande préfixée par 'defaire_'.
2.2 Gestion des exceptions
Bon. Tout ça c’est bien beau, mais qu’est-ce qui se passe si on tente de clouer des vis ?
$ ./test.sh clou1 vis clou2
marteau en main
clou1 cloué.
Je ne peux pas clouer 'vis'!
clou2 cloué.
marteau rangé
Au revoir!
$
Mouais, c’est pas idéal... On est censé sortir du bloc with dès qu’une erreur survient, et là au contraire le programme a continué comme si de rien n’était. Il faudrait générer une exception quand on arrive sur la vis. Le problème c’est que… euh... en fait il n’y a pas de notion d’exception, en bash ! [3]
Bon. Mais à bien y réfléchir, on a quelque chose qui s’en rapproche. Un truc qu’on trouve dans tous les articles sur les « bonnes pratiques » en bash. C’est l’instruction set -e. Rajoutons-la en haut du script :
#!/usr/bin/env bash
set -e
source with_bloc.sh
[...]
Relançons le même test :
$ ./test.sh clou1 vis clou2
marteau en main
clou1 cloué.
Je ne peux pas clouer 'vis'!
$
Avouez qu’on a l’impression de sortir sur une exception ! En fait, quand on lance la commande set -e, on indique à l’interpréteur du script qu’il doit vérifier le code de retour de chaque commande. Et si ce code de retour est non nul (ce qui est un signe d’erreur), l’exécution est interrompue. Dans notre exemple, la commande return 1 de la fonction clouer() a donc suffi pour interrompre l’exécution.
Le souci dans ce cas, c’est que le marteau n’a pas été rangé. Cela implique que le code du end_with n’a pas été exécuté, contrairement à ce qui se passe avec un vrai bloc with, comme en Python.
Pour forcer l’appel du end_with en cas de sortie prématurée, il faudrait déclencher un traitement juste avant de sortir du script. Pour ce genre de choses, on utilise une fonctionnalité annexe de la commande trap. L’utilité principale de cette commande est d’associer un traitement à la réception d’un signal. Par exemple :
...
on_sigint() {
# ... traitement SIGINT
}
trap on_sigint SIGINT
...
Mais en réalité, en deuxième paramètre, on n’est pas restreint aux seuls identifiants de signaux : la commande help trap (voir encadré) nous indique quelques possibilités supplémentaires. En particulier, on peut spécifier EXIT pour détecter la sortie du script, et donc exécuter un code adéquat juste avant de sortir.
Pourquoi « help trap » et pas « man trap » ?
Certaines commandes, comme trap, sont internes au shell. La preuve :
$ which trap
$
Il n’y a pas d’exécutable nommé trap sur le système. C’est juste bash qui l’interprète. Par conséquent, la documentation de la commande trap s’obtient par... man bash ! Le souci, c’est que la page de manuel de bash est un peu longuette. C’est pour ça que je vous proposais d’utiliser l’aide en ligne de bash, en tapant help <cmd>.
Pour corser un peu le tout, sachez qu’il y a aussi des commandes qui sont à la fois disponibles sur le système ET implémentées en interne par bash. C’est le cas de la commande echo par exemple :
$ which echo
/bin/echo
$ help echo
echo: echo [-neE] [arg ...]
Write arguments to the standard output.
[...]
Quel intérêt me direz-vous ? La plupart du temps, c’est une question de performance. Si le shell utilise la commande système, alors il lui faut créer un processus fils à chaque fois pour la lancer, ce qui est quand même très coûteux pour une commande aussi basique. Le problème, c’est qu’il peut y avoir quelques différences subtiles entre les deux implémentations. Et au final, si vous tapez man echo, vous ne regardez sûrement pas la bonne documentation ! Il y a d’ailleurs une indication dans cette page de manuel qui devrait vous alerter à ce sujet.
Rajoutons donc le code suivant à la fin de with_bloc.sh :
on_exit() {
while [ "$a_depiler" != "" ]
do
end_with
done
}
trap on_exit EXIT
Voilà, je crois que ce code est assez évident : en sortie du script, on referme les éventuels blocs with restés ouverts. Testons :
$ ./test.sh clou1 vis clou2
marteau en main
clou1 cloué.
Je ne peux pas clouer 'vis'!
marteau rangé
$
Cette fois-ci, ça fonctionne !
2.3 Le test de trop
Il ne reste plus qu’une chose à tester : les bloc with imbriqués. Mais normalement, avec notre gestion basée sur une pile, ça devrait rouler.
Bon voilà, j’ai réécrit la fin de test.sh. Voilà les dernières lignes :
[...]
with ouvrir_tiroir
with prendre_marteau
for probleme in$problemes
do
clouer"$probleme"
done
with prendre_telephone
echo"allo, chef, c'est fait!"
echo'** fin du script **'
Je vous laisse imaginer le code de ouvrir_tiroir(), defaire_ouvrir_tiroir(), prendre_telephone() et defaire_prendre_telephone(). Voyons ce que ça donne à l’exécution, juste avec 2 clous pour commencer :
$ ./test.sh clou1 clou2
tiroir ouvert
marteau en main
clou1 cloué.
clou2 cloué.
telephone en main
allo, chef, c'est fait!
** fin du script **
telephone sur son socle
marteau rangé
tiroir fermé
$
?? Qu’est-ce que c’est que ce bazar ? Y aurait-il un souci dans ma gestion de pile ? (L’avis de mon relecteur est plutôt que le script a développé une intelligence propre et déduit qu’avec deux mains un humain peut très bien prendre le téléphone sans lâcher le marteau...)
Un bon quart d’heure plus tard…
Suis-je idiot ! J’ai juste oublié d’écrire les end_with !! Je pouvais toujours chercher un bug dans ma gestion de pile !
Trois balises end_with rajoutées plus tard…
C’est reparti pour le test :
$ ./test.sh clou1 clou2
tiroir ouvert
marteau en main
clou1 cloué.
clou2 cloué.
marteau rangé
tiroir fermé
telephone en main
allo, chef, c'est fait!
telephone sur son socle
** fin du script **
$
C’est mieux, beaucoup mieux ! Et avec la vis :
$ ./test.sh clou1 vis clou2
tiroir ouvert
marteau en main
clou1 cloué.
Je ne peux pas clouer 'vis'!
marteau rangé
tiroir fermé
$
Impeccable, on a juste refermé les with ouverts, avant de quitter à cause de l’exception.
3. Une intégration plus poussée dans le langage
Cette petite mésaventure d’oubli des end_with m’a fait prendre conscience d’une limite importante de mon implémentation. En Python, la fermeture du bloc with est implicite, elle s’effectue automatiquement quand on quitte le bloc indenté. De ce fait, il n’y a pas de mot-clé comme end_with à indiquer (ou à oublier, en l’occurrence). En bash, l’indentation n’influe pas sur la syntaxe, mais on a quand même des vérifications de syntaxe bien utiles sur certaines constructions. Par exemple, dans une boucle while, si jamais on oublie le done final, on aura une erreur de syntaxe, ce qui a l’avantage d’aiguiller directement le programmeur sur le bon diagnostic.
Avec mon implémentation actuelle, il paraît compliqué de déclencher une telle erreur de syntaxe. Mais si on la fait évoluer un peu, c’est peut-être possible. Je crois d’ailleurs que j’ai une petite idée qui se dessine, un genre de détournement de la boucle while...
3.1 Une boucle while contrôlée
Commençons par un petit test avec le script suivant, boucle_controlee.sh :
#!/bin/bash
boucle_controlee() {
if [ $entree_boucle = 1 ]
then
echo entree du bloc $*
entree_boucle=0
return 0 # condition ok, re-boucler
else
echo sortie du bloc $*
return 1 # condition pas ok, sortir
fi
}
entree_boucle=1
while boucle_controlee
do
echo dans le bloc
done
Si on identifie les itérations du while (variable entree_boucle), on peut la « contrôler » : on décide de passer le premier test, donc d’exécuter le contenu du bloc, mais d’arrêter au deuxième test. Voilà ce que ça donne à l’exécution :
$ ./boucle_controlee.sh
entree du bloc
dans le bloc
sortie du bloc
$
3.2 Un alias pour cacher ce que vous n’êtes pas censés voir
Si vous ne voyez pas où je veux en venir, modifions légèrement la chose en ajoutant la définition d’un alias :
#!/bin/bash
boucle_controlee() {
[... voir plus haut ...]
}
shopt -s expand_aliases
alias with='entree_boucle=1; while boucle_controlee'
with ici_une_commande
do
echo dans le bloc
done
Là c’est clair, non ? Il est pas beau ce bloc with...do...done ? Et là, si on oublie de fermer le bloc avec le done, on aura effectivement une erreur de syntaxe !
On peut vérifier, à l’exécution ça marche toujours :
$ ./boucle_controlee.sh
entree du bloc ici_une_commande
dans le bloc
sortie du bloc ici_une_commande
$
Il reste deux points à éclaircir. Le premier, c’est l’emploi bizarre de la variable entree_boucle, initialisée avant la boucle while (dans l’alias) et ensuite modifiée dans la fonction boucle_controlee(). En fait, on est tenté d’implémenter ce contrôle de la boucle dans la fonction uniquement, en alternant entre deux états, pour gérer respectivement l’entrée et la sortie du bloc. En réalité, ce serait une erreur, car l’entrée et la sortie correspondent à deux appels distincts de cette fonction. Or, en cas d’imbrication de blocs with, on casse cette alternance entre entrée et sortie de bloc. Au final, je me suis donc contenté de simplement repérer l’entrée dans la boucle.
Le deuxième point concerne l’activation de l’option expand_aliases. Il faut savoir que, par défaut, bash n’interprète les alias que lors d’une session interactive. Ici, s’agissant de l’exécution d’un script, ce n’est pas le cas. Il faut donc activer cette option pour que l’alias soit pris en compte.
3.3 Implémentation finale
Notre implémentation finale de with_bloc.sh va reprendre les éléments de notre première implémentation, ainsi que l’idée développée ci-dessus.
EOL="
"
a_depiler=""
start_with() {
cmd="$*"
$cmd
a_depiler+="${EOL}defaire_$cmd"
}
end_with() {
cmd_defaire="$(echo "$a_depiler" | tail -n 1)"
$cmd_defaire
a_depiler="$(echo "$a_depiler" | head -n -1)"
}
on_exit() {
while [ "$a_depiler" != "" ]
do
end_with
done
}
trap on_exit EXIT
boucle_controlee() {
if [ $entree_boucle = 1 ]
then
start_with $*
entree_boucle=0
return 0 # condition ok, re-boucler
else
end_with
return 1 # condition pas ok, sortir
fi
}
shopt-s expand_aliases
aliaswith='entree_boucle=1; while boucle_controlee'
La fonction with() de notre première implémentation a été renommée en start_with(). Excepté ce renommage, elle est inchangée. La fonction end_with() est également inchangée, tout comme le code qui ferme les blocs restés ouverts en cas d’exception (on_exit() et commande trap). La fonction boucle_controlee() a été très légèrement modifiée pour appeler les fonctions start_with() et end_with(). Enfin, la définition de l’alias reprend ce qu’on a écrit ci-dessus.
Pour tester cette version, on pourra adapter notre script de test :
#!/usr/bin/env bash
set-e
source with_bloc.sh
[... mêmes fonctions que précédemment ...]
problemes="$@"
with ouvrir_tiroir; do
with prendre_marteau; do
for probleme in $problemes; do
clouer"$probleme"
done
done
done
with prendre_telephone; do
echo"allo, chef, c'est fait!"
done
echo'** fin du script **'
En testant, avec ou sans la vis, on retrouve des résultats identiques à notre première implémentation. Par contre, si on oublie de fermer un des blocs avec done, l’interpréteur détecte le souci de syntaxe :
$ ./test.sh clou1 vis clou2
./test.sh: ligne 56: erreur de syntaxe : fin de fichier prématurée
$
Pour tout vous dire, c’est bien la première fois que je suis content de voir s’afficher une erreur de syntaxe. :)
Épilogue
Deux ans plus tard…
Après avoir implémenté cette gestion dans debootstick[6], j’ai pu débuguer plus rapidement. Parce qu’un outil qui met le bazar sur votre machine à chaque plantage, c’est plutôt pénible à débuguer ! D’ailleurs, phase de débugage ou pas, cela reste un outil qui enchaîne les opérations bas niveau sur votre système. Ce « filet de sécurité » est donc plutôt bienvenu.
Peu de temps après, j’ai donc pu fournir une première version, capable de prendre en charge les scénarios décrits au début de l’article. Et quelques autres joyeusetés. L’outil est maintenant disponible dans l’OS Debian (testing), depuis l’été 2015. Vous pourrez donc le trouver dans la prochaine version stable de Debian [7], « stretch », qui sera peut-être sortie quand vous lirez ces lignes.
Pour la petite histoire, quand on construit un paquet pour Debian on doit faire tourner lintian, un outil qui vérifie les sources. Comme je l’ai laissé entendre au tout début de l’article, cette construction bizarre n’est alors pas passée inaperçue ! J’ai dû ajouter ce qu’on appelle un « lintian override », une annotation indiquant à l’outil que ce qu’il a détecté n’est pas un souci.
Merci à Henry-Joseph pour la relecture !
Notes et références
[1] Vous avez pu entrevoir dans un article récent [5] cette suite d’opérations à effectuer. Notons toutefois que debootstick vise à créer des systèmes live pérennes sur le long terme, la structure de l’OS est donc plus simple (pas de squashfs), ce qui permet des mises à jour complètes (bootloader & noyau compris). D’autre part, debootstick ne travaille pas directement sur votre clé USB, il construit une image ; vous pourrez donc la tester au préalable avec kvm. En effet, flasher un OS sur une clé USB n’est pas un acte anodin vis-à-vis de la durée de vie de celle-ci.
[2] Dans la plupart des langages, pour implémenter une pile, on se base sur un tableau. Mais à mon avis, en bash, l’usage des tableaux est plutôt cryptique, surtout pour des opérations comme « supprimer le dernier élément du tableau ». Alors je ne vais pas souiller votre magazine préféré avec ce genre d’incantations.
[3] Si c’était un « article dont vous êtes le héros », ici il y aurait une première fin possible ;-)
[4] L’avis de mon relecteur est plutôt que le script a développé une intelligence propre et déduit qu’avec deux mains un humain peut très bien prendre le téléphone sans lâcher le marteau...
[5] ENDRES F., « Live-System from Scratch », GNU/Linux Magazine n°202, mars 2017, p. 54 à 61.
[6] Page GitHub de debootstick : https://github.com/drakkar-lig/debootstick
[7] Package debootstick dans Debian Stretch : https://packages.debian.org/stretch/debootstick