arthur.bebou

Le site arthur.bebou.netlib.re - retour accueil

git clone git://bebou.netlib.re/arthur.bebou

Log | Files | Refs |

index.sh (30409B)


      1 #! page
      2 title: Un commentaire à propos de \"Avoid Software\"
      3 author: Arthur Pons
      4 description: Avoid software ? Embrace shell scripts
      5 publication: 2025-05-14
      6 
      7 section: main
      8 
      9 La plupart des informations intéressantes de cet article sont directement tirées
     10 de [cet article](https://dwheeler.com/essays/filenames-in-shell.html) vraiment
     11 détaillé. Merci à David A. Wheller qui se trouve avoir le même nom que [le
     12 premier détenteur d'un doctorat en
     13 informatique](https://fr.wikipedia.org/wiki/David_Wheeler_(informaticien)).
     14 Merci aussi au goat Stéphane Chazelas et toutes ses réponses sur
     15 [stackexchange](https://unix.stackexchange.com/users/22565/st%c3%a9phane-chazelas?tab=answers).
     16 
     17 ## Introduction
     18 
     19 [Timothée](https://timothee.goguely.com/) a récemment fait un cours avec des
     20 airs de [missing semester](https://missing.csail.mit.edu/) pour parler de JS,
     21 de CLI et de vie professionnelle en tant que graphiste[^1]. A cette occasion là
     22 il a trouvé une ressource parlant de CLI, faite par et pour des graphistes,
     23 nommée [Avoid Software](https://avoidsoftware.sarahgarcin.com/index.html).
     24 
     25 Cet article a pour but d'être une contribution à ce fanzine :
     26 
     27   * en apportant des précisions sur le fonctionnement des scripts
     28   * en proposant des alternatives aux scripts lorsqu'ils ne fonctionnent pas
     29     dans certains cas
     30   * en garantissant que tout fonctionne sous OpenBSD
     31   * en proposant une partie expliquant comment transformer les commandes en
     32     scripts qui peuvent prendre des arguments
     33 
     34 ## Les limites fréquemment rencontrées
     35 
     36 Les scripts de ce zine contiennent des limites récurrentes, à savoir :
     37 
     38   * La mauvaise gestion des fichiers commençant par un tiret `-`
     39   * La mauvaise gestion des fichiers contenant des espaces
     40   * La mauvaise gestion des cas où aucun fichier ne correspond au critère de
     41     recherche
     42   * Ne pas être "portable", c'est-à-dire ne pas pouvoir fonctionner sur un large
     43     ensemble de shells et de systèmes d'exploitation
     44 
     45 Avant de rentrer dans les détails de ces limites il faut être clair. Tous les
     46 logiciels, et en particulier les scripts de ce zine, n'ont pas vocation à être
     47 parfaitement corrects. Leur usage est généralement situé, sur un OS particulier
     48 pour une personne particulière dans un dossier particulier. Dans la plupart des
     49 contextes les limites de ces scripts ne sont pas gênants. Il n'importe pas
     50 qu'un script ne gère pas les fichiers dont les noms comportent des espaces si
     51 l'on *sait* qu'on l'utilise sur des fichiers dont les noms ne comportent pas
     52 d'espace. Cela dit puisque ce zine est public et puisque j'aime apprendre des
     53 choses sur le shell je pense qu'il est opportun de partager des alternatives
     54 plus robustes.
     55 
     56 ### La mauvaise gestion des fichiers commençant par un tiret `-`
     57 
     58 Imaginons que nous voulions lancer une commande sur plusieurs fichiers d'un
     59 dossier à la fois. Par exemple, lister tous les jpeg. Pour cela on peut
     60 utiliser les "globs" (ou "Pattern Shells") :
     61 
     62     $ ls *.jpeg
     63     machin.jpeg
     64     truc.jpeg
     65 
     66 Dans les globs le caractère `*` veut dire "n'importe quel caractère autant de
     67 fois que nécessaire". Donc `*.jpeg` veut dire "tous les fichiers se terminant
     68 par `.jpeg`. On peut penser les globs comme des expressions régulières beaucoup
     69 moins puissantes.
     70 
     71 D'autres caractères significatifs des globs :
     72 
     73   * `?` : "n'importe quel caractère une fois"
     74   * `[]` : introduit ce que l'on appelle une "classe de caractère". En écrivant
     75     `[abc]` on dit "une fois le caractère a, b ou c". Il est également possible
     76     d'utiliser le caractère `-` à l'intérieur d'une classe de caractère pour
     77     décrire une étendue de caractère. `[3-8]` ou `[d-h]` voudront dire "une
     78     fois un entier entre 3 et 8 inclus" et "une fois une lettre minuscule entre
     79     d et h inclus dans l'ordre alphabétique".
     80   * `!` : dans une classe de caractère permet de prendre le complément des
     81     caractères. `[!abc]` veut dire "une fois n'importe quel caractère *sauf* a,
     82     b ou c".
     83 
     84 On peut donc étendre la commande précédente pour lui faire des choses plus
     85 précises et alambiquées comme :
     86 
     87     $ ls *-[!1][0-9][0-9]-??.jpeg
     88 
     89 lister tous les fichiers commençant n'importe comment, suivi d'un tiret,
     90 suivi de n'importe quel caractère n'étant pas un `1` suivi de deux entiers suivi
     91 d'un tiret suivi d'exactement deux caractères suivi de `.jpeg`.
     92 
     93 En arrière plan le shell "développe" le glob en le remplaçant par tous les
     94 chemins auxquels il correspond. Si l'on a trois fichiers dans notre dossier
     95 `a.jpg`, `b.jpg` et `machin.jpg` la commande
     96 
     97     ls ?.jpg
     98 
     99 va s'étendre en
    100 
    101     ls "a.jpg" "b.jpg"
    102 
    103 avant de s'éxecuter. Il est possible dans certains shells, à l'écriture de la
    104 commande, d'obtenir un retour des fichiers qui correspondent en appuyant sur la
    105 touche tabulation comme si l'on voulait compléter le glob. Avec ma configuration
    106 de `zsh` le développement se fait carrément en direct dans le prompt.
    107 
    108 Si vous êtes particulièrement alertes ce fonctionnement par réécriture de
    109 commande devrait vous donner envie de tester un truc. Que se passe-t-il si un
    110 le nom d'un fichier commence par `-` ? Admettons qu'il existe un fichier nommé
    111 `-azerty.jpg` et que l'on utilise le glob `*.jpg` en argument de la fonction
    112 `ls` :
    113 
    114     $ ls *.jpg
    115     # devient
    116     $ ls "-azerty.jpg"
    117     ls : option invalide -- 'z'
    118 
    119 On se retrouve avec une erreur. Et pour cause, la commande a cru que
    120 `-azerty.jpg` était la déclaration d'options de la commande `ls`.
    121 
    122 Il y a deux manières de se prémunir de cette erreur. La manière la plus
    123 universelle est d'ajouter `./` devant le glob. `./` voulant dire "le dossier
    124 courant" le glob se développera sur exactement les mêmes fichier qu'auparavant
    125 mais les chemins démarreront tous `./` :
    126 
    127     $ ls ./-azerty.jpg
    128     -azerty.jpg
    129 
    130 Il n'y a donc plus d'ambiguité entre les options et les noms des fichiers. Une
    131 deuxième solution est d'ajouter `--` entre les options et les arguments de la
    132 commande :
    133 
    134     $ ls -larth -- *.jpg
    135     # devient
    136     $ ls -larth -- -azerty.jpg
    137     -rw-r--r-- 1 arthur arthur 0 10 mai   11:27  -azerty.jpg
    138 
    139 Ici `--` permet à la commande de savoir que tout ce qui suit doit être
    140 interprété comme des arguments et non pas comme de potentielles options. Bien
    141 que [cette syntaxe soit
    142 POSIX](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_02)
    143 toutes les commandes ne respectent pas ce principe, en particulier les
    144 commandes qui ne sont pas "de base" sur les systèmes Unix. La première solution
    145 est donc préférable.
    146 
    147 ### La mauvaise gestion des fichiers dont le nom contient des espaces
    148 
    149 Dans les systèmes UNIX supportant UTF-8 les noms des fichiers peuvent contenir
    150 tous les caractères sauf le caractère nul (`\0`). Écrire des scripts qui gèrent
    151 correctement les fichiers en toutes circonstances nécessite de faire attention
    152 à la manière dont le shell interprète les blancs (espace, tabulation, retour à
    153 la ligne etc). Les blancs, dans la pratique presque toujours des espaces,
    154 servent au shell à distinguer les éléments les un des autres. C'est la raison
    155 pour laquelle il est important de toujours mettre un ou plusieurs espaces entre
    156 les noms des commandes et leurs options et arguments :
    157 
    158     $ ls -la fichier.pdf
    159 
    160 Comme on le voit dans la commande ci-dessus il peut y avoir des exceptions. Les
    161 options peuvent être combinées (`-la`). Lorsque les options peuvent prendre des
    162 arguments on peut généralement coller les deux puisqu'il n'y a pas d'ambiguité
    163 sur la découpe. Par exemple l'option `-i` de `sed` qui est nécessairement suivi
    164 d'un suffixe, espace ou pas :
    165 
    166     $ sed -i.bak 's/machin/truc/' *.txt
    167 
    168 Imaginons maitenant que l'on a deux pdf `fichier.pdf` et `fichier 2.pdf`. Si
    169 l'on veut afficher des informations à leurs propos on risque de tomber sur une
    170 erreur :
    171 
    172     $ ls -la fichier.pdf fichier 2.pdf
    173     ls: impossible d'accéder à 'fichier': Aucun fichier ou dossier de ce type
    174     ls: impossible d'accéder à '2.pdf': Aucun fichier ou dossier de ce type
    175     -rw-r--r-- 1 arthur arthur 365368  9 avril 15:00 fichier.pdf
    176 
    177 On voit que le shell a découpé les arguments en trois fichier au lieu de deux.
    178 Il n'est pas assez sophistiqué pour tester si `fichier` et `(1).pdf` sont en
    179 réalité deux parties du nom d'un même fichier. Pour faire comprendre au shell
    180 que cet espace ne démarque pas la frontière entre deux éléments différents il
    181 faut ajouter un autre marqueur. L'objectif est de faire en sorte que l'espace
    182 soit compris littéralement. Il y a deux manière de le faire :
    183 
    184   * Englober le chemin entre deux guillemets `"` ou apostrophes `'` : `"fichier
    185     2.pdf"`
    186   * Echapper l'espace avec un `\` : `fichier\ 2.pdf`
    187 
    188 Employer l'une ou l'autre permet de découper les arguments correctement :
    189 
    190     $ ls -la fichier.pdf "fichier 2.pdf"
    191     -rw-r--r-- 1 arthur arthur 365368  9 avril 15:00 fichier.pdf
    192     -rw-r--r-- 1 arthur arthur 365368  9 avril 15:00 fichier 2.pdf
    193 
    194 Lors d'un usage interactif du shell, en écrivant directement après le prompt, ce
    195 problème est rarement rencontré puisque l'opérateurice un peu expérimentée
    196 utilisera l'auto-complétion et/ou les globs. Par exemple l'auto-complétion de
    197 zsh pré-echappe les espaces et affichera les résultats :
    198 
    199     $ ls fich<TAB>
    200     fichier\ 1.pdf  fichier.pdf
    201     $ ls fichier*
    202     'fichier 1.pdf'   fichier.pdf
    203 
    204 On remarque ici que le glob est immunisé à la malédiction des espaces dans les
    205 noms de fichier. Ce n'est pas par hasard que dans le titre précédent j'ai
    206 remplacé `ls ?.jpg` par `ls "a.jpg" "b.jpg"`. La raison pour laquelle les globs
    207 ne rencontrent pas ce problème est parce qu'ils sont interprétés *après* le
    208 découpage des éléments par le shell. Le shell découpe `ls fichier*` en deux
    209 éléments. Il détecte que le second est un glob qu'il développe en `fichier.pdf`
    210 et `fichier 1.pdf`. A ce stade le shell a déjà découpé les éléments de la
    211 commande, il n'y a donc plus de risque de croire que l'espace dans le deuxième
    212 fichier sépare deux fichiers différents. Finalement il exécute `ls` avec les deux
    213 bons arguments.
    214 
    215 Alors si les globs sont immunisé et si l'on fait rarement l'erreur d'écrire à la
    216 main des chemins contenant des espaces, quand est-ce que cela pose problèmes ?
    217 Lorsque l'on dépasse la "simple" commande et que l'on se met à utiliser des
    218 variables ou à combiner des sorties de commandes avec d'autres éléments de
    219 syntaxe. Dès que l'on script un peu quoi.
    220 
    221 Imaginons vouloir mettre la valeur `fichier 1.pdf` dans une variable :
    222 
    223     $ file=fichier 1.pdf
    224     sh: 1: 1.pdf: not found
    225     $ echo $file
    226      
    227 
    228 Le shell a découpé cette commande en deux parties : la déclaration de la
    229 variable `file` et la tentative d'exécution de la commande inexistante `1.pdf`.
    230 Plutôt cohérent avec ce que l'on a appris jusque là. Ce qui est peut-être plus
    231 surprenant c'est que la variable "$file" est vide. Et pour cause la syntaxe
    232 `var=valeur cmd` est particulière. Elle ne déclare la variable `var` que pour
    233 l'exécution de la commande `cmd`. Elle n'existera plus directement après. Cette
    234 syntaxe est utilisée pour configurer des variables d'environnement pour des
    235 commandes qui en ont besoin. Elle n'est donc pas à confondre avec
    236 `var=valeur;cmd` qui fera les deux opérations successivement. Bref c'était pas
    237 le propos.
    238 
    239 On peut corriger notre affaire très simplement en faisant `file="fichier
    240 1.pdf"`. Maintenant on veut utiliser la variable plus tard dans le script :
    241 
    242     $ file="fichier 1.pdf"
    243     $ ls $file
    244     ls: impossible d'accéder à 'fichier': Aucun fichier ou dossier de ce type
    245     ls: impossible d'accéder à '1.pdf': Aucun fichier ou dossier de ce type
    246 
    247 On retombe sur notre souci de découpage ! Contrairement au développement des
    248 globs le développement des variables se fait *avant* le découpage des
    249 commandes. `ls $file` devient alors `ls fichier 1.pdf` et la commande `ls` sera
    250 donc exécutée avec deux arguments séparés. Pour y remédier il faut
    251 préemptivement protéger la variable avec des guillemets - et non pas des
    252 apostrophes qui ont pour comportement de ne pas étendre les variables à
    253 l'intérieur mais de considérer tout littéralement :
    254 
    255     $ ls "$file"
    256     # deviendra
    257     $ ls "fichier 1.pdf"
    258     'fichier 1.pdf'
    259 
    260 C'est le premier grand enseignement de toute cette affaire. Il faut *toujours*
    261 englober ses variables avec des `"` par défaut[^3] et, dans de rares cas, ne pas
    262 le faire lorsque l'on veut que leurs contenus soient découpés en plusieurs
    263 éléments par le shell. Il y a potentiellement une infinité de situations
    264 distinctes les une des autres dans lesquelles ce genre de situations
    265 surviennent. Je ne vais en évoquer une seule autre, celle de l'utilisation de
    266 `find`.
    267 
    268 Les globs que l'on a vu précédemment ne permettent pas, dans POSIX du moins, de
    269 faire des recherches récursives. `./*.pdf` ne correspondra qu'aux fichiers du
    270 *dossier courant* terminant par `.pdf`. Si l'on veut descendre dans les
    271 sous-répertoires *et* avoir tout un tas d'autres filtres à notre disposition -
    272 la taille du fichier, sa date de dernière modification etc - il faut
    273 avoir recours à `find`. `find` renvoie une liste des fichiers correspondant aux
    274 filtres qu'on lui a indiqué. En admettant que l'on a un dossier dans lequel un
    275 fichier `fichier 2.pdf` se trouve :
    276 
    277     $ find -name '*.pdf'
    278     ./fichier.pdf
    279     ./dossier/fichier 2.pdf
    280     ./fichier 1.pdf
    281 
    282 On peut supposer qu'il sera possible d'itérer sur cette liste à l'aide d'une
    283 boucle `for` pour effectuer une opération sur chacun de ces fichiers :
    284 
    285     $ for file in $(find -name '*.pdf');do
    286          printf "on traite le fichier %s\n" "$file"
    287       done
    288     on traite le fichier ./fichier.pdf
    289     on traite le fichier ./dossier/fichier
    290     on traite le fichier 2.pdf
    291     on traite le fichier ./fichier
    292     on traite le fichier 1.pdf
    293 
    294 Mais patatra, *encore* notre souci de découpage. Le développement de la capture
    295 de commande `$(find -name '*.pdf')` se fait avant le découpage. On a donc en
    296 réalité exécuté :
    297 
    298     $ for file in ./fichier.pdf ./dossier/fichier 2.pdf ./fichier 1.pdf;do
    299     ...
    300 
    301 Il y a [tout un tas](https://dwheeler.com/essays/filenames-in-shell.html) de
    302 manière de contourner ce problème mais je vais en proposer une seule ici, celle
    303 qui me semble la plus portable et flexible.
    304 
    305 Cette solution consiste à utiliser l'option `-print0` de `find` conjointement
    306 avec l'option `-0` d'`xargs`. Cela permettra à `find` de délimiter les
    307 différents fichiers trouvés par le caractère nul et à `xargs` de délimiter les
    308 arguments sur ce même caractère nul. Cela résout notre problème puisque le seul
    309 caractère qui ne peut *pas* être dans le nom d'un fichier est justement le
    310 caractère nul[^2]. Depuis 2023 POSIX inclus les options nécessaires dans `find`
    311 et `xargs` permettant de le faire. Si l'on ne peut pas garantir que tous les
    312 `find` et `xargs` du monde l'implémentent du fait que la spécification est
    313 récente c'est tout de même la solution que je privilégie.
    314 
    315 L'idée générale est de trouver les fichiers que l'on veut, par exemple tous les
    316 fichiers terminant par `.pdf` (`find -type f -name '*.pdf'`) et de les passer à
    317 `xargs` pour lancer une commande avec ces fichiers en arguments :
    318 
    319     $ find -type f -name '*.pdf' -print0 | xargs -0 ls -la
    320     -rw-r--r-- 1 arthur arthur 0 15 mai   10:27 './dossier/fichier 2.pdf'
    321     -rw-r--r-- 1 arthur arthur 0 15 mai   10:27 './fichier 1.pdf'
    322     -rw-r--r-- 1 arthur arthur 0 15 mai   10:27  ./fichier.pdf
    323 
    324 On constate qu'aucun `ls` n'a été tenté sur un fichier n'existant pas. Le
    325 découpage a été fait correctement. La commande `xargs` est assez complexe et je
    326 ne vais pas plus en parler ici. Des exemples seront donnés dans les
    327 alternatives aux différentes commandes du zine par la suite. À noter que dans
    328 sa version GNU `xargs` va tout de même exécuter une fois la commande si aucun
    329 argument ne lui a été fourni en entrée standard. Je *crois* que ce comportement
    330 n'est pas POSIX. Pour reproduire le comportement plus intuitif d'aucune
    331 exécution si `find` ne trouve aucun fichier il faut lui ajouter l'option `-r`.
    332 La version BSD ne souffre pas du même problème. Pour le reste de cet article
    333 j'omet le `-r`.
    334 
    335 ### Mauvaise gestion des cas où aucun fichier ne correspond au critère de recherche
    336 
    337 Aussi surprenant que cela puisse paraître[^5] lorsque le shell développe un glob
    338 mais ne trouve aucun fichier correspondant le glob lui même sera renvoyé :
    339 
    340     $ ls *.truc
    341     ls: impossible d'accéder à '*.truc': Aucun fichier ou dossier de ce type
    342     $ touch machin.truc
    343     $ ls *.truc
    344     machin.truc
    345 
    346 C'est inoffensif pour un `ls` mais cela pourrait mener à l'exécution de
    347 commandes que l'on ne souhaite pas. Lorsqu'on utilise les globs pour amorcer
    348 une boucle la solution est de vérifier à chaque fois si le fichier sur lequel
    349 on tente de lancer la commande existe bel et bien[^4] :
    350 
    351     for file in ./*.pdf;do
    352         if [ -e "$file" ];then
    353             cmd "$file"
    354         fi
    355     done
    356 
    357 Que l'on peut aussi écrire :
    358 
    359 
    360     for file in ./*.pdf;do
    361         [ -e "$file" ] && cmd "$file"
    362     done
    363 
    364 Dans certains shells il est possible de modifier ce comportement. Par exemple
    365 dans bash et zsh :
    366 
    367     shopt -s nullglob # Pour BASH
    368     setopt NULL_GLOB  # Pour ZSH
    369     for file in ./*.pdf;do
    370         cmd "$file"
    371     done
    372 
    373 Revient au même mais est plus efficace puisque l'on a plus à vérifier
    374 l'existence du fichier à chaque itération de la boucle.
    375 
    376 ### Manque de portabilité
    377 
    378 Le monde du shell est, encore en 2025, très morcelé. On a l'impression que
    379 toutes les personnes faisant du shell partagent plus ou moins le même
    380 environnement technique mais c'est loin d'être le cas. Le shell par défaut sur
    381 les linux est traditionnellement `bash`. Sur MacOS c'est `zsh` depuis quelques
    382 temps. Sur OpenBSD c'est `ksh`. Les scripts sont souvent écrits pour fonctionner
    383 avec `/bin/sh` qui est généralement un lien symbolique vers un autre shell - par
    384 exemple `dash` sous debian. Autant dire qu'il est difficile de garantir qu'un
    385 script shell s'exécute correctement sur la plupart des machines. A ce sujet voir
    386 [le portage de catium vers OpenBSD et MacOS](/portabilite/) ou [le portage de
    387 catium vers debian 1.3](/catium-archeo/).
    388 
    389 Puisque ce zine est sur le web et qu'il s'adresse à une communauté ayant une
    390 diversité de machine assez grande je pense qu'il est utile de proposer des
    391 alternatives portables de chaque commande. Il n'y a pas une seule technique
    392 magique permettant d'assurer la portabilité d'un script. L'idéal reste de le
    393 tester sur les systèmes sur lesquels on veut qu'il fonctionne.
    394 
    395 Cela dit l'outil [shellcheck](https://www.shellcheck.net/) détecte tout un tas
    396 de soucis dans les scripts shell dont des [problèmes de
    397 portabilité](https://github.com/koalaman/shellcheck?tab=readme-ov-file#portability).
    398 De plus il existe un standard mi-descriptif mi-prescriptif nommé POSIX qui
    399 spécifie, entre autre, le fonctionnement des [commandes traditionnelles du monde
    400 Unix](https://pubs.opengroup.org/onlinepubs/9799919799/idx/utilities.html). Dans
    401 l'ensemble se limiter à ces commandes et aux options spécifiées dans ce standard
    402 est un bon moyen d'augmenter les chances qu'un script soit portable. Le shell
    403 depuis lequel je teste mes scripts est `dash`. Il est, en théorie, fait pour
    404 être conforme au standard POSIX mais dans la pratique ce n'est pas tout à fait
    405 le cas. Le fait qu'un script fonctionne sous `dash` ne garantit donc pas que sa
    406 syntaxe soit strictement POSIX.
    407 
    408 Dans l'ensemble les règles de base vont être :
    409 
    410   * Utiliser le shebang `#!/bin/sh` en début de script
    411   * Ne pas utiliser d'option non POSIX
    412   * Ne pas utiliser de commande non POSIX autre que celles clairement déclarées
    413     en dépendance
    414   * Vérifier que le script est correctement exécuté par `dash`
    415   * Vérifier que `shellcheck` ne renvoit aucune erreur
    416 
    417 
    418 ## Les scripts
    419 
    420 J'aurais tendance à dire qu'étant donné la manière dont ils sont présentés, ce
    421 qui constitue cette partie sont plutôt des *commandes* que des *scripts*.
    422 Habituellement les *scripts* vont être une ou plusieurs commandes inscrites
    423 dans un fichier que l'on éxecute comme une seule commande par la suite. C'est
    424 du détail mais si une section au sujet de la "scriptisation" des commandes
    425 devait être ajoutée ça rendrait les choses plus claires. Je propose une ébauche
    426 [ici](/avoid-software-commentaires/#faire-des-scripts-et-en-faire-une-commande).
    427 
    428 ### Convertir des formats d'image
    429 
    430 La version du zine :
    431 
    432   * ne gère pas correctement les fichiers commençant par un tiret `-`
    433 
    434 Deux alternatives plus robustes de cette commande sont, dans l'ordre de
    435 préférence :
    436 
    437     mogrify -format formatfinal ./*.formatàconvertir
    438     mogrify -format formatfinal -- *.formatàconvertir
    439 
    440 Une version recursive pourrait être :
    441 
    442     find . -name '*.formatàconvertir' -type f -print0 | xargs -0 mogrify -format formatfinal
    443 
    444 
    445 ### Diffusion d'erreur
    446 
    447 #### Modifier une seule image
    448 
    449 RAS
    450 
    451 #### Modifier les images par lot
    452 
    453 La version du zine :
    454 
    455   * ne gère pas correctement les fichiers commençant par un tiret `-`
    456   * ne gère pas correctement l'absence de fichier correspondant au critère de
    457     recherche
    458 
    459 Une version plus robuste et portable serait :
    460 
    461     for img in ./*.jpg; do
    462         if [ -e "$img" ];then
    463             convert "$img" -colorspace Gray -dither FloydSteinberg -colors 4 "dither_$img";
    464         fi
    465     done
    466 
    467 Une version récursive pourrait être :
    468 
    469     find . -name '*.jpg' -type f -print0 |
    470         xargs -0 -n1 sh -c 'convert "$1" -colorspace Gray -dither FloydSteinberg -colors 4 "dither_${1#./}"' --
    471 
    472 ### Redimensionner les images pour le web
    473 
    474 La version du zine :
    475 
    476   * ne gère pas correctement les fichiers commençant par un tiret `-`
    477   * ne gère pas correctement l'absence de fichier correspondant au critère de
    478     recherche
    479 
    480 Une version plus robuste et portable serait donc :
    481 
    482     for img in ./*.jpg; do
    483         if [ -e "$img" ];then
    484             convert "$img" -resize 3000x2000 -strip -quality 86 "$img";
    485         fi
    486     done
    487 
    488 Une version récursive pourrait être :
    489 
    490     find . -name '*.jpg' -type f -print0 |
    491         xargs -0 -I{} convert "{}" -resize 3000x2000 -strip -quality 86 "{}"
    492 
    493 ### Seam Carving
    494 
    495 RAS
    496 
    497 ### Convertir des fichiers .mov en .mp4
    498 
    499 RAS
    500 
    501 ### Convertir des fichiers m4a en mp3
    502 
    503 La version du zine :
    504 
    505   * ne gère pas correctement les fichiers commençant par un tiret `-`
    506   * ne gère pas correctement l'absence de fichier correspondant au critère de
    507     recherche
    508 
    509 Une version plus robuste et portable serait donc :
    510 
    511     for vid in ./*.m4a; do
    512         if [ -e "$vid" ];then
    513             ffmpeg -i "$vid" -codec:v copy -codec:a libmp3lame -q:a 2 "${vid%.m4a}.mp3"
    514         fi
    515     done
    516 
    517 Une version récursive pourrait être :
    518 
    519     find . -name '*.m4a' -type f -print0 |
    520         xargs -0 -n1 sh -c 'ffmpeg -i "$1" -codec:v copy -codec:a libmp3lame
    521         -q:a 2 "${1%.m4a}.mp3"' --
    522 
    523 Au passage petite explication du renommage. Il se fait avec la syntaxe
    524 `"${f%.m4a}.mp3"`. En shell il est possible d'invoquer la valeur d'une variable
    525 `f` en écrivant `$f` ou `${f}`. La syntaxe avec les `{}` permet deux choses :
    526 
    527   * coller la valeur de la variable à une chaîne de caractère. En faisant `echo
    528     "$ftruc"` le shell n'aurait pas la capacité de savoir que l'on veut ce qu'il
    529     y a dans la variable `f` suivi de `truc`. Il penserait que l'on veut la
    530     variable `ftruc`. La solution est d'écrire `echo "${f}truc"`.
    531   * le retrait de suffixe et préfixe. `${f%.m4a}` renvoie la valeur de la
    532     variable `f` avec le plus petit suffixe `.m4a` retiré. Autrement dit c'est
    533     un moyen de retirer l'extension `.m4a` d'un nom de fichier. Il existe des
    534     syntaxes similaires pour retirer le plus grand suffixe possible et les
    535     préfixes les plus petits et grands.
    536 
    537 `${f%.m4a}.mp3` est donc un moyen de prendre une variable `$f` dans laquelle on
    538 a, par exemple, `truc.m4a`, lui retirer son extension pour avoir `truc` et y
    539 ajouter une nouvelle extension `.mp3` pour obtenir `truc.mp3`.
    540 
    541 ### Exporter chaque glyphe d’une fonte en fichier svg
    542 
    543 C'est du python. Je regarderai peut-être si c'est possible sans. En attendant
    544 je passe.
    545 
    546 ### Supprimer les espaces dans les noms de fichier
    547 
    548 La version du zine :
    549 
    550   * ne gère pas correctement les fichiers commençant par un tiret `-`
    551   * ne gère pas correctement l'absence de fichier correspondant au critère de
    552     recherche
    553   * n'est pas portable
    554 
    555 Une version plus robuste et portable serait donc :
    556 
    557     for f in ./*\ *;do
    558         [ -e "$f" ] && mv "$f" "$(echo "$f" | tr ' ' '_')"
    559     done
    560 
    561 La *capture de commande* introduite avec `$()` permet d'exécuter une commande
    562 dans notre commande. Elle sera évaluée avant la commande générale. En
    563 l'occurence elle émet le contenu de `$f` dans la commande `tr ' ' '_'` qui va
    564 convertir tous les espaces par des `_`. Ainsi, une fois la capture de commande
    565 évaluée, la commande que l'on exécute vraiment ressemblera à :
    566 
    567     [ -e "./test truc" ] && mv "./test truc" "./test_truc"
    568 
    569 C'est pas terriblement performant parce qu'il faut, pour chaque fichier ou
    570 dossier, exécuter une sous commande pour avoir une version sans les espaces.
    571 Cela dit la version bash est pas immensément plus rapide.
    572 
    573 Une version récursive pourrait être :
    574 
    575     find . -name '* *' -print0 |
    576         xargs -0 -n1 sh -c 'mv "$1" "$(echo "$1" | tr " " "_")"' --
    577 
    578 Si l'on accepte une dépendance on peut utiliser le très bon `rename`. Il permet
    579 de renommer des fichiers via l'utilisation de commandes perl, qu'on peut, ici
    580 seulement et pour faire simple, considérer comme des commandes `sed`.  Notre
    581 besoin est donc satisfait avec :
    582 
    583     $ rename 'y/ /_/' ./*
    584 
    585 C'est super rapide et propre. En mode interactif je recommande l'utilisation de
    586 `rename`. Dans des scripts je recommande l'une des versions précédentes.
    587 
    588 ### Renommer des fichiers par lot
    589 
    590 La version du zine :
    591 
    592   * ne gère pas correctement les fichiers commençant par un tiret `-`
    593   * ne gère pas correctement l'absence de fichier correspondant au critère de
    594     recherche
    595   * n'est pas portable
    596 
    597 Une version plus robuste et portable serait donc :
    598 
    599     for file in ./*.jpg; do
    600         i=$(( $i + 1 ))
    601         printf "renommé '%s' -> '%s'\n" "$file" "image_$i.jpg"
    602         mv "$file" "image_$i.jpg"
    603     done
    604 
    605 Les raisons d'utiliser `printf` plutôt qu'`echo` sont nombreuses et documentées
    606 [ici](https://unix.stackexchange.com/questions/65803/why-is-printf-better-than-echo)
    607 par Stéphane Chazelas. `let` et l'option `-v` de `mv` ne sont pas POSIX.
    608 
    609 Je ne propose pas de version récursive parce que ça ne fait pas bien sens et
    610 risquerait d'être un peu trop dangereux.
    611 
    612 ### Compresser un PDF
    613 
    614 RAS
    615 
    616 ### Lancer une impression automatiquement
    617 
    618 La version du zine :
    619 
    620   * ne gère pas correctement les fichiers commençant par un tiret `-`
    621   * ne gère pas correctment les espaces dans les noms des fichiers
    622   * n'est pas portable
    623 
    624 Une version plus robuste et portable serait :
    625 
    626     #! /bin/sh
    627 
    628     printer=Canon_LBP7100C_7110C
    629     archivebox="archivebox/"
    630     printinbox="printbox/"
    631     interval="20"
    632 
    633     mkdir -p "$archivebox" "$printinbox"
    634 
    635     while true; do
    636         for file in ./$printinbox*.pdf ;do
    637             if [ -e "$file" ];then
    638                 printf "on copie %s dans %s\n" "$file" "$archivebox"
    639                 mv "$file" "$archivebox" # copy in outbox (archives)
    640             fi
    641         done
    642 
    643         i="$interval"
    644         while [ "$i" -gt "0" ];do
    645             printf "                 \r"
    646             printf "next try in $i s \r"
    647             sleep 1
    648             i=$(( $i - 1 ))
    649         done
    650     done
    651 
    652 L'option POSIX `-p` de `mkdir` permet à `mkdir` de créer un chemin complet
    653 `truc/machin/bidule` sans avoir à créer chaque dossier et sous-dossier un à un.
    654 Aussi elle permet à `mkdir` de ne pas renvoyer d'erreur si le dossier existe
    655 déjà. Dans de nombreux cas le comportement induit par `-p` est celui souhaité.
    656 
    657 ## Travailler avec des fichiers cachés
    658 
    659 Si l'on a des fichiers cachés les globs type `./*.pdf` ne vont pas fonctionner.
    660 Le glob `*` veut certes dire "n'importe quel caractère autant de fois que
    661 nécessaire" *sauf* pour le `.` qui est spécial et, en début de fichier, dénote
    662 les fichiers "cachés" :
    663 
    664     $ ls -a
    665     . .. fichier_pas_secret .fichier_secret
    666     $ for file in ./*;do printf "%s\n" "$file"; done
    667     ./fichier_pas_secret
    668 
    669 Pour inclure les fichiers cachés il faut ajouter d'autres globs :
    670 
    671     $ for file in ./* ./.[\!.]* ./..?* ; do
    672         [ -e "$file" ] && printf "%s\n" "$file"
    673       done
    674     ./fichier_pas_secret
    675     ./.fichier_secret
    676 
    677 ## Faire de ces scripts des commande à part entière
    678 
    679 Chaque commande que l'on a vu jusque là peut être rendue reproductible et
    680 appelable depuis n'importe où dans votre système sous forme de script. Pour
    681 créer un script nommé `rmspace` depuis une commande il faut :
    682 
    683   1. Créer un fichier texte nommé `rmspace`
    684   2. Y mettre, *en toute première ligne*, `#!/bin/sh` puis la commande que l'on
    685      souhaite exécuter
    686   3. Enregistrer le fichier texte
    687   4. Le rendre exécutable avec `chmod +x /chemin/vers/le/fichier`
    688 
    689 A ce stade le script peut être appelé en appelant directement son chemin de
    690 manière non ambigue. Si le script se trouve dans le dossier courant :
    691 
    692     $ ./script
    693 
    694 Si vous voulez y avoir accès n'importe où comme si c'était une commande à part
    695 entière il faut l'"installer" dans un dossier de votre `$PATH`. Dans l'immense
    696 majorité des cas le copier dans `/usr/local/bin` convient :
    697 
    698     $ cp ./rmspace /usr/local/bin
    699 
    700 Vous pouvez ensuite l'appeler directement avec son nom :
    701 
    702     $ rmspace
    703 
    704 ## Au sujet du nom du zine
    705 
    706 Juste une remarque au sujet du fait que l'on évite pas de logiciel mais on
    707 substitut du logiciel. Il est intéressant de se demander pourquoi on considère
    708 rarement les commandes/scripts comme des logiciels à proprement parler.
    709 
    710 [^1]: https://mastodon.design/@timotheegoguely/114478679053770184
    711 [^2]: Oui, le caractère de retour à la ligne peut exister dans le nom d'un
    712     fichier
    713 [^3]: https://unix.stackexchange.com/questions/171346/security-implications-of-forgetting-to-quote-a-variable-in-bash-posix-shells/171347#171347
    714 [^4]: retrouver le texte de Chazelas qui explique que `! -e` ne garantit par que
    715     le fichier n'existe pas 🤯
    716 [^5]: https://unix.stackexchange.com/questions/204803/why-is-nullglob-not-default/204944#204944