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