Un commentaire à propos de “Avoid Software”


La plupart des informations intéressantes de cet article sont directement tirées de cet article vraiment détaillé. Merci à David A. Wheller qui se trouve avoir le même nom que le premier détenteur d’un doctorat en informatique. Merci aussi au goat Stéphane Chazelas et toutes ses réponses sur stackexchange.

Introduction

Timothée a récemment fait un cours avec des airs de missing semester pour parler de JS, de CLI et de vie professionnelle en tant que graphiste1. A cette occasion là il a trouvé une ressource parlant de CLI, faite par et pour des graphistes, nommée Avoid Software.

Cet article a pour but d’être une contribution à ce fanzine :

Les limites fréquemment rencontrées

Les scripts de ce zine contiennent des limites récurrentes, à savoir :

Avant de rentrer dans les détails de ces limites il faut être clair. Tous les logiciels, et en particulier les scripts de ce zine, n’ont pas vocation à être parfaitement corrects. Leur usage est généralement situé, sur un OS particulier pour une personne particulière dans un dossier particulier. Dans la plupart des contextes les limites de ces scripts ne sont pas gênants. Il n’importe pas qu’un script ne gère pas les fichiers dont les noms comportent des espaces si l’on sait qu’on l’utilise sur des fichiers dont les noms ne comportent pas d’espace. Cela dit puisque ce zine est public et puisque j’aime apprendre des choses sur le shell je pense qu’il est opportun de partager des alternatives plus robustes.

La mauvaise gestion des fichiers commençant par un tiret -

Imaginons que nous voulions lancer une commande sur plusieurs fichiers d’un dossier à la fois. Par exemple, lister tous les jpeg. Pour cela on peut utiliser les “globs” (ou “Pattern Shells”) :

$ ls *.jpeg
machin.jpeg
truc.jpeg

Dans les globs le caractère * veut dire “n’importe quel caractère autant de fois que nécessaire”. Donc *.jpeg veut dire “tous les fichiers se terminant par .jpeg. On peut penser les globs comme des expressions régulières beaucoup moins puissantes.

D’autres caractères significatifs des globs :

On peut donc étendre la commande précédente pour lui faire des choses plus précises et alambiquées comme :

$ ls *-[!1][0-9][0-9]-??.jpeg

lister tous les fichiers commençant n’importe comment, suivi d’un tiret, suivi de n’importe quel caractère n’étant pas un 1 suivi de deux entiers suivi d’un tiret suivi d’exactement deux caractères suivi de .jpeg.

En arrière plan le shell “développe” le glob en le remplaçant par tous les chemins auxquels il correspond. Si l’on a trois fichiers dans notre dossier a.jpg, b.jpg et machin.jpg la commande

ls ?.jpg

va s’étendre en

ls "a.jpg" "b.jpg"

avant de s’éxecuter. Il est possible dans certains shells, à l’écriture de la commande, d’obtenir un retour des fichiers qui correspondent en appuyant sur la touche tabulation comme si l’on voulait compléter le glob. Avec ma configuration de zsh le développement se fait carrément en direct dans le prompt.

Si vous êtes particulièrement alertes ce fonctionnement par réécriture de commande devrait vous donner envie de tester un truc. Que se passe-t-il si un le nom d’un fichier commence par - ? Admettons qu’il existe un fichier nommé -azerty.jpg et que l’on utilise le glob *.jpg en argument de la fonction ls :

$ ls *.jpg
# devient
$ ls "-azerty.jpg"
ls : option invalide -- 'z'

On se retrouve avec une erreur. Et pour cause, la commande a cru que -azerty.jpg était la déclaration d’options de la commande ls.

Il y a deux manières de se prémunir de cette erreur. La manière la plus universelle est d’ajouter ./ devant le glob. ./ voulant dire “le dossier courant” le glob se développera sur exactement les mêmes fichier qu’auparavant mais les chemins démarreront tous ./ :

$ ls ./-azerty.jpg
-azerty.jpg

Il n’y a donc plus d’ambiguité entre les options et les noms des fichiers. Une deuxième solution est d’ajouter -- entre les options et les arguments de la commande :

$ ls -larth -- *.jpg
# devient
$ ls -larth -- -azerty.jpg
-rw-r--r-- 1 arthur arthur 0 10 mai   11:27  -azerty.jpg

Ici -- permet à la commande de savoir que tout ce qui suit doit être interprété comme des arguments et non pas comme de potentielles options. Bien que cette syntaxe soit POSIX toutes les commandes ne respectent pas ce principe, en particulier les commandes qui ne sont pas “de base” sur les systèmes Unix. La première solution est donc préférable.

La mauvaise gestion des fichiers dont le nom contient des espaces

Dans les systèmes UNIX supportant UTF-8 les noms des fichiers peuvent contenir tous les caractères sauf le caractère nul (\0). Écrire des scripts qui gèrent correctement les fichiers en toutes circonstances nécessite de faire attention à la manière dont le shell interprète les blancs (espace, tabulation, retour à la ligne etc). Les blancs, dans la pratique presque toujours des espaces, servent au shell à distinguer les éléments les un des autres. C’est la raison pour laquelle il est important de toujours mettre un ou plusieurs espaces entre les noms des commandes et leurs options et arguments :

$ ls -la fichier.pdf

Comme on le voit dans la commande ci-dessus il peut y avoir des exceptions. Les options peuvent être combinées (-la). Lorsque les options peuvent prendre des arguments on peut généralement coller les deux puisqu’il n’y a pas d’ambiguité sur la découpe. Par exemple l’option -i de sed qui est nécessairement suivi d’un suffixe, espace ou pas :

$ sed -i.bak 's/machin/truc/' *.txt

Imaginons maitenant que l’on a deux pdf fichier.pdf et fichier 2.pdf. Si l’on veut afficher des informations à leurs propos on risque de tomber sur une erreur :

$ ls -la fichier.pdf fichier 2.pdf
ls: impossible d'accéder à 'fichier': Aucun fichier ou dossier de ce type
ls: impossible d'accéder à '2.pdf': Aucun fichier ou dossier de ce type
-rw-r--r-- 1 arthur arthur 365368  9 avril 15:00 fichier.pdf

On voit que le shell a découpé les arguments en trois fichier au lieu de deux. Il n’est pas assez sophistiqué pour tester si fichier et (1).pdf sont en réalité deux parties du nom d’un même fichier. Pour faire comprendre au shell que cet espace ne démarque pas la frontière entre deux éléments différents il faut ajouter un autre marqueur. L’objectif est de faire en sorte que l’espace soit compris littéralement. Il y a deux manière de le faire :

Employer l’une ou l’autre permet de découper les arguments correctement :

$ ls -la fichier.pdf "fichier 2.pdf"
-rw-r--r-- 1 arthur arthur 365368  9 avril 15:00 fichier.pdf
-rw-r--r-- 1 arthur arthur 365368  9 avril 15:00 fichier 2.pdf

Lors d’un usage interactif du shell, en écrivant directement après le prompt, ce problème est rarement rencontré puisque l’opérateurice un peu expérimentée utilisera l’auto-complétion et/ou les globs. Par exemple l’auto-complétion de zsh pré-echappe les espaces et affichera les résultats :

$ ls fich<TAB>
fichier\ 1.pdf  fichier.pdf
$ ls fichier*
'fichier 1.pdf'   fichier.pdf

On remarque ici que le glob est immunisé à la malédiction des espaces dans les noms de fichier. Ce n’est pas par hasard que dans le titre précédent j’ai remplacé ls ?.jpg par ls "a.jpg" "b.jpg". La raison pour laquelle les globs ne rencontrent pas ce problème est parce qu’ils sont interprétés après le découpage des éléments par le shell. Le shell découpe ls fichier* en deux éléments. Il détecte que le second est un glob qu’il développe en fichier.pdf et fichier 1.pdf. A ce stade le shell a déjà découpé les éléments de la commande, il n’y a donc plus de risque de croire que l’espace dans le deuxième fichier sépare deux fichiers différents. Finalement il exécute ls avec les deux bons arguments.

Alors si les globs sont immunisé et si l’on fait rarement l’erreur d’écrire à la main des chemins contenant des espaces, quand est-ce que cela pose problèmes ? Lorsque l’on dépasse la “simple” commande et que l’on se met à utiliser des variables ou à combiner des sorties de commandes avec d’autres éléments de syntaxe. Dès que l’on script un peu quoi.

Imaginons vouloir mettre la valeur fichier 1.pdf dans une variable :

$ file=fichier 1.pdf
sh: 1: 1.pdf: not found
$ echo $file
 

Le shell a découpé cette commande en deux parties : la déclaration de la variable file et la tentative d’exécution de la commande inexistante 1.pdf. Plutôt cohérent avec ce que l’on a appris jusque là. Ce qui est peut-être plus surprenant c’est que la variable “$file” est vide. Et pour cause la syntaxe var=valeur cmd est particulière. Elle ne déclare la variable var que pour l’exécution de la commande cmd. Elle n’existera plus directement après. Cette syntaxe est utilisée pour configurer des variables d’environnement pour des commandes qui en ont besoin. Elle n’est donc pas à confondre avec var=valeur;cmd qui fera les deux opérations successivement. Bref c’était pas le propos.

On peut corriger notre affaire très simplement en faisant file="fichier 1.pdf". Maintenant on veut utiliser la variable plus tard dans le script :

$ file="fichier 1.pdf"
$ ls $file
ls: impossible d'accéder à 'fichier': Aucun fichier ou dossier de ce type
ls: impossible d'accéder à '1.pdf': Aucun fichier ou dossier de ce type

On retombe sur notre souci de découpage ! Contrairement au développement des globs le développement des variables se fait avant le découpage des commandes. ls $file devient alors ls fichier 1.pdf et la commande ls sera donc exécutée avec deux arguments séparés. Pour y remédier il faut préemptivement protéger la variable avec des guillemets - et non pas des apostrophes qui ont pour comportement de ne pas étendre les variables à l’intérieur mais de considérer tout littéralement :

$ ls "$file"
# deviendra
$ ls "fichier 1.pdf"
'fichier 1.pdf'

C’est le premier grand enseignement de toute cette affaire. Il faut toujours englober ses variables avec des " par défaut2 et, dans de rares cas, ne pas le faire lorsque l’on veut que leurs contenus soient découpés en plusieurs éléments par le shell. Il y a potentiellement une infinité de situations distinctes les une des autres dans lesquelles ce genre de situations surviennent. Je ne vais en évoquer une seule autre, celle de l’utilisation de find.

Les globs que l’on a vu précédemment ne permettent pas, dans POSIX du moins, de faire des recherches récursives. ./*.pdf ne correspondra qu’aux fichiers du dossier courant terminant par .pdf. Si l’on veut descendre dans les sous-répertoires et avoir tout un tas d’autres filtres à notre disposition - la taille du fichier, sa date de dernière modification etc - il faut avoir recours à find. find renvoie une liste des fichiers correspondant aux filtres qu’on lui a indiqué. En admettant que l’on a un dossier dans lequel un fichier fichier 2.pdf se trouve :

$ find -name '*.pdf'
./fichier.pdf
./dossier/fichier 2.pdf
./fichier 1.pdf

On peut supposer qu’il sera possible d’itérer sur cette liste à l’aide d’une boucle for pour effectuer une opération sur chacun de ces fichiers :

$ for file in $(find -name '*.pdf');do
     printf "on traite le fichier %s\n" "$file"
  done
on traite le fichier ./fichier.pdf
on traite le fichier ./dossier/fichier
on traite le fichier 2.pdf
on traite le fichier ./fichier
on traite le fichier 1.pdf

Mais patatra, encore notre souci de découpage. Le développement de la capture de commande $(find -name '*.pdf') se fait avant le découpage. On a donc en réalité exécuté :

$ for file in ./fichier.pdf ./dossier/fichier 2.pdf ./fichier 1.pdf;do
...

Il y a tout un tas de manière de contourner ce problème mais je vais en proposer une seule ici, celle qui me semble la plus portable et flexible.

Cette solution consiste à utiliser l’option -print0 de find conjointement avec l’option -0 d’xargs. Cela permettra à find de délimiter les différents fichiers trouvés par le caractère nul et à xargs de délimiter les arguments sur ce même caractère nul. Cela résout notre problème puisque le seul caractère qui ne peut pas être dans le nom d’un fichier est justement le caractère nul3. Depuis 2023 POSIX inclus les options nécessaires dans find et xargs permettant de le faire. Si l’on ne peut pas garantir que tous les find et xargs du monde l’implémentent du fait que la spécification est récente c’est tout de même la solution que je privilégie.

L’idée générale est de trouver les fichiers que l’on veut, par exemple tous les fichiers terminant par .pdf (find -type f -name '*.pdf') et de les passer à xargs pour lancer une commande avec ces fichiers en arguments :

$ find -type f -name '*.pdf' -print0 | xargs -0 ls -la
-rw-r--r-- 1 arthur arthur 0 15 mai   10:27 './dossier/fichier 2.pdf'
-rw-r--r-- 1 arthur arthur 0 15 mai   10:27 './fichier 1.pdf'
-rw-r--r-- 1 arthur arthur 0 15 mai   10:27  ./fichier.pdf

On constate qu’aucun ls n’a été tenté sur un fichier n’existant pas. Le découpage a été fait correctement. La commande xargs est assez complexe et je ne vais pas plus en parler ici. Des exemples seront donnés dans les alternatives aux différentes commandes du zine par la suite. À noter que dans sa version GNU xargs va tout de même exécuter une fois la commande si aucun argument ne lui a été fourni en entrée standard. Je crois que ce comportement n’est pas POSIX. Pour reproduire le comportement plus intuitif d’aucune exécution si find ne trouve aucun fichier il faut lui ajouter l’option -r. La version BSD ne souffre pas du même problème. Pour le reste de cet article j’omet le -r.

Mauvaise gestion des cas où aucun fichier ne correspond au critère de recherche

Aussi surprenant que cela puisse paraître4 lorsque le shell développe un glob mais ne trouve aucun fichier correspondant le glob lui même sera renvoyé :

$ ls *.truc
ls: impossible d'accéder à '*.truc': Aucun fichier ou dossier de ce type
$ touch machin.truc
$ ls *.truc
machin.truc

C’est inoffensif pour un ls mais cela pourrait mener à l’exécution de commandes que l’on ne souhaite pas. Lorsqu’on utilise les globs pour amorcer une boucle la solution est de vérifier à chaque fois si le fichier sur lequel on tente de lancer la commande existe bel et bien5 :

for file in ./*.pdf;do
    if [ -e "$file" ];then
        cmd "$file"
    fi
done

Que l’on peut aussi écrire :

for file in ./*.pdf;do
    [ -e "$file" ] && cmd "$file"
done

Dans certains shells il est possible de modifier ce comportement. Par exemple dans bash et zsh :

shopt -s nullglob # Pour BASH
setopt NULL_GLOB  # Pour ZSH
for file in ./*.pdf;do
    cmd "$file"
done

Revient au même mais est plus efficace puisque l’on a plus à vérifier l’existence du fichier à chaque itération de la boucle.

Manque de portabilité

Le monde du shell est, encore en 2025, très morcelé. On a l’impression que toutes les personnes faisant du shell partagent plus ou moins le même environnement technique mais c’est loin d’être le cas. Le shell par défaut sur les linux est traditionnellement bash. Sur MacOS c’est zsh depuis quelques temps. Sur OpenBSD c’est ksh. Les scripts sont souvent écrits pour fonctionner avec /bin/sh qui est généralement un lien symbolique vers un autre shell - par exemple dash sous debian. Autant dire qu’il est difficile de garantir qu’un script shell s’exécute correctement sur la plupart des machines. A ce sujet voir le portage de catium vers OpenBSD et MacOS ou le portage de catium vers debian 1.3.

Puisque ce zine est sur le web et qu’il s’adresse à une communauté ayant une diversité de machine assez grande je pense qu’il est utile de proposer des alternatives portables de chaque commande. Il n’y a pas une seule technique magique permettant d’assurer la portabilité d’un script. L’idéal reste de le tester sur les systèmes sur lesquels on veut qu’il fonctionne.

Cela dit l’outil shellcheck détecte tout un tas de soucis dans les scripts shell dont des problèmes de portabilité. De plus il existe un standard mi-descriptif mi-prescriptif nommé POSIX qui spécifie, entre autre, le fonctionnement des commandes traditionnelles du monde Unix. Dans l’ensemble se limiter à ces commandes et aux options spécifiées dans ce standard est un bon moyen d’augmenter les chances qu’un script soit portable. Le shell depuis lequel je teste mes scripts est dash. Il est, en théorie, fait pour être conforme au standard POSIX mais dans la pratique ce n’est pas tout à fait le cas. Le fait qu’un script fonctionne sous dash ne garantit donc pas que sa syntaxe soit strictement POSIX.

Dans l’ensemble les règles de base vont être :

Les scripts

J’aurais tendance à dire qu’étant donné la manière dont ils sont présentés, ce qui constitue cette partie sont plutôt des commandes que des scripts. Habituellement les scripts vont être une ou plusieurs commandes inscrites dans un fichier que l’on éxecute comme une seule commande par la suite. C’est du détail mais si une section au sujet de la “scriptisation” des commandes devait être ajoutée ça rendrait les choses plus claires. Je propose une ébauche ici.

Convertir des formats d’image

La version du zine :

Deux alternatives plus robustes de cette commande sont, dans l’ordre de préférence :

mogrify -format formatfinal ./*.formatàconvertir
mogrify -format formatfinal -- *.formatàconvertir

Une version recursive pourrait être :

find . -name '*.formatàconvertir' -type f -print0 | xargs -0 mogrify -format formatfinal

Diffusion d’erreur

Modifier une seule image

RAS

Modifier les images par lot

La version du zine :

Une version plus robuste et portable serait :

for img in ./*.jpg; do
    if [ -e "$img" ];then
        convert "$img" -colorspace Gray -dither FloydSteinberg -colors 4 "dither_$img";
    fi
done

Une version récursive pourrait être :

find . -name '*.jpg' -type f -print0 |
    xargs -0 -n1 sh -c 'convert "$1" -colorspace Gray -dither FloydSteinberg -colors 4 "dither_${1#./}"' --

Redimensionner les images pour le web

La version du zine :

Une version plus robuste et portable serait donc :

for img in ./*.jpg; do
    if [ -e "$img" ];then
        convert "$img" -resize 3000x2000 -strip -quality 86 "$img";
    fi
done

Une version récursive pourrait être :

find . -name '*.jpg' -type f -print0 |
    xargs -0 -I{} convert "{}" -resize 3000x2000 -strip -quality 86 "{}"

Seam Carving

RAS

Convertir des fichiers .mov en .mp4

RAS

Convertir des fichiers m4a en mp3

La version du zine :

Une version plus robuste et portable serait donc :

for vid in ./*.m4a; do
    if [ -e "$vid" ];then
        ffmpeg -i "$vid" -codec:v copy -codec:a libmp3lame -q:a 2 "${vid%.m4a}.mp3"
    fi
done

Une version récursive pourrait être :

find . -name '*.m4a' -type f -print0 |
    xargs -0 -n1 sh -c 'ffmpeg -i "$1" -codec:v copy -codec:a libmp3lame
    -q:a 2 "${1%.m4a}.mp3"' --

Au passage petite explication du renommage. Il se fait avec la syntaxe "${f%.m4a}.mp3". En shell il est possible d’invoquer la valeur d’une variable f en écrivant $f ou ${f}. La syntaxe avec les {} permet deux choses :

${f%.m4a}.mp3 est donc un moyen de prendre une variable $f dans laquelle on a, par exemple, truc.m4a, lui retirer son extension pour avoir truc et y ajouter une nouvelle extension .mp3 pour obtenir truc.mp3.

Exporter chaque glyphe d’une fonte en fichier svg

C’est du python. Je regarderai peut-être si c’est possible sans. En attendant je passe.

Supprimer les espaces dans les noms de fichier

La version du zine :

Une version plus robuste et portable serait donc :

for f in ./*\ *;do
    [ -e "$f" ] && mv "$f" "$(echo "$f" | tr ' ' '_')"
done

La capture de commande introduite avec $() permet d’exécuter une commande dans notre commande. Elle sera évaluée avant la commande générale. En l’occurence elle émet le contenu de $f dans la commande tr ' ' '_' qui va convertir tous les espaces par des _. Ainsi, une fois la capture de commande évaluée, la commande que l’on exécute vraiment ressemblera à :

[ -e "./test truc" ] && mv "./test truc" "./test_truc"

C’est pas terriblement performant parce qu’il faut, pour chaque fichier ou dossier, exécuter une sous commande pour avoir une version sans les espaces. Cela dit la version bash est pas immensément plus rapide.

Une version récursive pourrait être :

find . -name '* *' -print0 |
    xargs -0 -n1 sh -c 'mv "$1" "$(echo "$1" | tr " " "_")"' --

Si l’on accepte une dépendance on peut utiliser le très bon rename. Il permet de renommer des fichiers via l’utilisation de commandes perl, qu’on peut, ici seulement et pour faire simple, considérer comme des commandes sed. Notre besoin est donc satisfait avec :

$ rename 'y/ /_/' ./*

C’est super rapide et propre. En mode interactif je recommande l’utilisation de rename. Dans des scripts je recommande l’une des versions précédentes.

Renommer des fichiers par lot

La version du zine :

Une version plus robuste et portable serait donc :

for file in ./*.jpg; do
    i=$(( $i + 1 ))
    printf "renommé '%s' -> '%s'\n" "$file" "image_$i.jpg"
    mv "$file" "image_$i.jpg"
done

Les raisons d’utiliser printf plutôt qu’echo sont nombreuses et documentées ici par Stéphane Chazelas. let et l’option -v de mv ne sont pas POSIX.

Je ne propose pas de version récursive parce que ça ne fait pas bien sens et risquerait d’être un peu trop dangereux.

Compresser un PDF

RAS

Lancer une impression automatiquement

La version du zine :

Une version plus robuste et portable serait :

#! /bin/sh

printer=Canon_LBP7100C_7110C
archivebox="archivebox/"
printinbox="printbox/"
interval="20"

mkdir -p "$archivebox" "$printinbox"

while true; do
    for file in ./$printinbox*.pdf ;do
        if [ -e "$file" ];then
            printf "on copie %s dans %s\n" "$file" "$archivebox"
            mv "$file" "$archivebox" # copy in outbox (archives)
        fi
    done

    i="$interval"
    while [ "$i" -gt "0" ];do
        printf "                 \r"
        printf "next try in $i s \r"
        sleep 1
        i=$(( $i - 1 ))
    done
done

L’option POSIX -p de mkdir permet à mkdir de créer un chemin complet truc/machin/bidule sans avoir à créer chaque dossier et sous-dossier un à un. Aussi elle permet à mkdir de ne pas renvoyer d’erreur si le dossier existe déjà. Dans de nombreux cas le comportement induit par -p est celui souhaité.

Travailler avec des fichiers cachés

Si l’on a des fichiers cachés les globs type ./*.pdf ne vont pas fonctionner. Le glob * veut certes dire “n’importe quel caractère autant de fois que nécessaire” sauf pour le . qui est spécial et, en début de fichier, dénote les fichiers “cachés” :

$ ls -a
. .. fichier_pas_secret .fichier_secret
$ for file in ./*;do printf "%s\n" "$file"; done
./fichier_pas_secret

Pour inclure les fichiers cachés il faut ajouter d’autres globs :

$ for file in ./* ./.[\!.]* ./..?* ; do
    [ -e "$file" ] && printf "%s\n" "$file"
  done
./fichier_pas_secret
./.fichier_secret

Faire de ces scripts des commande à part entière

Chaque commande que l’on a vu jusque là peut être rendue reproductible et appelable depuis n’importe où dans votre système sous forme de script. Pour créer un script nommé rmspace depuis une commande il faut :

  1. Créer un fichier texte nommé rmspace
  2. Y mettre, en toute première ligne, #!/bin/sh puis la commande que l’on souhaite exécuter
  3. Enregistrer le fichier texte
  4. Le rendre exécutable avec chmod +x /chemin/vers/le/fichier

A ce stade le script peut être appelé en appelant directement son chemin de manière non ambigue. Si le script se trouve dans le dossier courant :

$ ./script

Si vous voulez y avoir accès n’importe où comme si c’était une commande à part entière il faut l’"installer” dans un dossier de votre $PATH. Dans l’immense majorité des cas le copier dans /usr/local/bin convient :

$ cp ./rmspace /usr/local/bin

Vous pouvez ensuite l’appeler directement avec son nom :

$ rmspace

Au sujet du nom du zine

Juste une remarque au sujet du fait que l’on évite pas de logiciel mais on substitut du logiciel. Il est intéressant de se demander pourquoi on considère rarement les commandes/scripts comme des logiciels à proprement parler.


  1. https://mastodon.design/@timotheegoguely/114478679053770184 

  2. https://unix.stackexchange.com/questions/171346/security-implications-of-forgetting-to-quote-a-variable-in-bash-posix-shells/171347#171347 

  3. Oui, le caractère de retour à la ligne peut exister dans le nom d’un fichier 

  4. https://unix.stackexchange.com/questions/204803/why-is-nullglob-not-default/204944#204944 

  5. retrouver le texte de Chazelas qui explique que ! -e ne garantit par que le fichier n’existe pas 🤯