- Introduction
- Les limites fréquemment rencontrées
- Les scripts
- Convertir des formats d’image
- Diffusion d’erreur
- Redimensionner les images pour le web
- Seam Carving
- Convertir des fichiers .mov en .mp4
- Convertir des fichiers m4a en mp3
- Exporter chaque glyphe d’une fonte en fichier svg
- Supprimer les espaces dans les noms de fichier
- Renommer des fichiers par lot
- Compresser un PDF
- Lancer une impression automatiquement
- Travailler avec des fichiers cachés
- Faire de ces scripts des commande à part entière
- Au sujet du nom du zine
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 :
- en apportant des précisions sur le fonctionnement des scripts
- en proposant des alternatives aux scripts lorsqu’ils ne fonctionnent pas dans certains cas
- en garantissant que tout fonctionne sous OpenBSD
- en proposant une partie expliquant comment transformer les commandes en scripts qui peuvent prendre des arguments
Les limites fréquemment rencontrées
Les scripts de ce zine contiennent des limites récurrentes, à savoir :
- La mauvaise gestion des fichiers commençant par un tiret
-
- La mauvaise gestion des fichiers contenant des espaces
- La mauvaise gestion des cas où aucun fichier ne correspond au critère de recherche
- Ne pas être “portable”, c’est-à-dire ne pas pouvoir fonctionner sur un large ensemble de shells et de systèmes d’exploitation
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 :
?
: “n’importe quel caractère une fois”[]
: introduit ce que l’on appelle une “classe de caractère”. En écrivant[abc]
on dit “une fois le caractère a, b ou c”. Il est également possible d’utiliser le caractère-
à l’intérieur d’une classe de caractère pour décrire une étendue de caractère.[3-8]
ou[d-h]
voudront dire “une fois un entier entre 3 et 8 inclus” et “une fois une lettre minuscule entre d et h inclus dans l’ordre alphabétique”.!
: dans une classe de caractère permet de prendre le complément des caractères.[!abc]
veut dire “une fois n’importe quel caractère sauf a, b ou c”.
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 :
- Englober le chemin entre deux guillemets
"
ou apostrophes'
:"fichier 2.pdf"
- Echapper l’espace avec un
\
:fichier\ 2.pdf
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 :
- Utiliser le shebang
#!/bin/sh
en début de script - Ne pas utiliser d’option non POSIX
- Ne pas utiliser de commande non POSIX autre que celles clairement déclarées en dépendance
- Vérifier que le script est correctement exécuté par
dash
- Vérifier que
shellcheck
ne renvoit aucune erreur
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 :
- ne gère pas correctement les fichiers commençant par un tiret
-
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 :
- ne gère pas correctement les fichiers commençant par un tiret
-
- ne gère pas correctement l’absence de fichier correspondant au critère de recherche
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 :
- ne gère pas correctement les fichiers commençant par un tiret
-
- ne gère pas correctement l’absence de fichier correspondant au critère de recherche
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 :
- ne gère pas correctement les fichiers commençant par un tiret
-
- ne gère pas correctement l’absence de fichier correspondant au critère de recherche
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 :
- coller la valeur de la variable à une chaîne de caractère. En faisant
echo "$ftruc"
le shell n’aurait pas la capacité de savoir que l’on veut ce qu’il y a dans la variablef
suivi detruc
. Il penserait que l’on veut la variableftruc
. La solution est d’écrireecho "${f}truc"
. - le retrait de suffixe et préfixe.
${f%.m4a}
renvoie la valeur de la variablef
avec le plus petit suffixe.m4a
retiré. Autrement dit c’est un moyen de retirer l’extension.m4a
d’un nom de fichier. Il existe des syntaxes similaires pour retirer le plus grand suffixe possible et les préfixes les plus petits et grands.
${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 :
- ne gère pas correctement les fichiers commençant par un tiret
-
- ne gère pas correctement l’absence de fichier correspondant au critère de recherche
- n’est pas portable
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 :
- ne gère pas correctement les fichiers commençant par un tiret
-
- ne gère pas correctement l’absence de fichier correspondant au critère de recherche
- n’est pas portable
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 :
- ne gère pas correctement les fichiers commençant par un tiret
-
- ne gère pas correctment les espaces dans les noms des fichiers
- n’est pas portable
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 :
- Créer un fichier texte nommé
rmspace
- Y mettre, en toute première ligne,
#!/bin/sh
puis la commande que l’on souhaite exécuter - Enregistrer le fichier texte
- 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.
-
https://mastodon.design/@timotheegoguely/114478679053770184 ↩
-
https://unix.stackexchange.com/questions/171346/security-implications-of-forgetting-to-quote-a-variable-in-bash-posix-shells/171347#171347 ↩
-
Oui, le caractère de retour à la ligne peut exister dans le nom d’un fichier ↩
-
https://unix.stackexchange.com/questions/204803/why-is-nullglob-not-default/204944#204944 ↩
-
retrouver le texte de Chazelas qui explique que
! -e
ne garantit par que le fichier n’existe pas 🤯 ↩