Cette question est posée par un court script que j'ai trouvé dans un magazine Linux. Pour prouver que je ne l'ai pas inventé, voici une photo:
J'aimerais écrire à l'éditeur de cette publication pour lui expliquer ce qui ne va pas et comment mieux l'écrire.
Le script tente de capturer les fichiers jpeg dans une variable, de sorte que quelque chose (compression utilisant lepton
) puisse être fait avec eux.
for jpeg in `echo "$(file $(find ./ ) |
grep JPEG | cut -f 1 -d ':')"`
do
/path/to/command "$jpeg"
...
Apparemment, dans ce cas, nous ne pouvons pas faire confiance aux fichiers portant l’extension .jpg
, nous ne pouvons donc pas les attraper avec quelque chose comme:
for f in *.JPG *.jpg *.JPEG *.jpeg ; do ...
parce que l'écrivain a utilisé file
pour vérifier leur type, mais si les noms de fichiers ne peuvent pas avoir une extension raisonnable, alors je ne vois pas comment nous pouvons leur faire confiance pour ne pas être -rf *
ou (; \ $!|
ou avoir des nouvelles lignes ou autre chose.
Comment puis-je capturer correctement des fichiers dans une variable par type avec for
ou while
, ou peut-être éviter de le faire en utilisant find
avec -exec
, ou une autre méthode?
Bonus pour avoir un aperçu et une démonstration de ce qui ne va pas avec le code dans l'image.
J'ai étiqueté cette question avec [bash] puisqu'il s'agit d'un script bash, mais si vous avez envie de répondre à une façon de le faire qui ne l'utilise pas, n'hésitez pas à le faire.
Faisons ceci avec les globs spéciaux de Bash et une boucle for
:
#!/bin/bash
shopt -s globstar dotglob
for f in ./** ; do
if file -b -- "$f" | grep -q '^JPEG image data,' ; then
# do whatever you want with the JPEG file "$f" in here:
md5sum -- "$f"
fi
done
Tout d’abord, nous devons rendre les globes Bash plus utiles en activant les options de shell globstar
et dotglob
Shell. Voici leur description de man bash
dans la section Shell BUILTIN COMMANDS sur shopt
:
dotglob
If set, bash includes filenames beginning with a `.' in the results of
pathname expansion.
globstar
If set, the pattern ** used in a pathname expansion context will match
all files and zero or more directories and subdirectories. If the pattern
is followed by a /, only directories and subdirectories match.
Ensuite, nous utilisons ce nouveau "glob récursif" ./**
dans une boucle for
pour parcourir tous les fichiers et dossiers du répertoire en cours et tous ses sous-répertoires. Veuillez toujours utiliser des chemins absolus ou des chemins relatifs explicites commençant par ./
ou ../
dans vos globs, pas seulement **
, pour éviter les problèmes de noms de fichiers spéciaux tels que ~
.
Maintenant, nous testons chaque nom de fichier (et de dossier) avec la commande file
pour son contenu. L'option -b
l'empêche d'imprimer à nouveau le nom du fichier avant la chaîne d'informations sur le contenu, ce qui rend le filtrage plus sûr.
Nous savons maintenant que les informations de contenu de tous les fichiers JPG/JPEG valides doivent commencer par JPEG image data,
, ce qui correspond au test de la sortie de file
avec grep
. Nous utilisons l'option -q
pour supprimer toute sortie, car nous ne nous intéressons qu'au code de sortie de grep
, qui indique si le motif correspond ou non.
Si cela correspond, le code à l'intérieur du bloc if
/then
sera exécuté. Nous pouvons faire tout ce que nous voulons ici. Le nom de fichier JPEG actuel est disponible dans la variable shell $f
. Nous devons simplement nous assurer de toujours le mettre entre guillemets pour éviter l’évaluation accidentelle de noms de fichiers contenant des caractères spéciaux tels que des espaces, des nouvelles lignes ou des symboles. Il est également généralement préférable de le séparer des autres arguments en le plaçant après --
, ce qui oblige la plupart des commandes à l’interpréter comme un nom de fichier, même s’il ressemble à -v
ou --help
autrement. être interprété comme une option.
Il est temps de faire sauter du code, pour la science! Voici la version de votre question/livre:
for jpeg in `echo "$(file $(find ./ )
| grep JPEG | cut -f 1 -d ':')"`
do
/path/to/command "$jpeg"
done
Tout d’abord, permettez-moi de mentionner la complexité de leur rédaction. Nous avons 4 niveaux de sous-shell imbriqués, utilisant des syntaxes de substitution de commandes mixtes (``
et $()
), qui sont simplement nécessaires en raison de l'utilisation incorrecte/sous-optimale de find
.
Ici, find
répertorie uniquement tous les fichiers et affiche leur nom, un par ligne. Ensuite, la sortie complète est passée à file
pour examiner chacune d’elles. Mais attendez! Un nom de fichier par ligne? Qu'en est-il des noms de fichiers contenant des nouvelles lignes? Bon, ceux qui vont le casser!
$ ls --escape ne*ne
new\nline
$ file $(find . -name 'ne*ne' )
./new: cannot open `./new' (No such file or directory)
line: cannot open `line' (No such file or directory)
En fait, même des espaces simples le séparent aussi, car ceux-ci sont également traités comme des séparateurs par file
. Vous ne pouvez même pas citer la "$(find ./ )"
ici comme solution, car cela indiquerait alors la sortie multiligne entière comme un argument de nom de fichier unique.
$ ls simple*
simple spaces.jpg
$ file $(find ./ -name 'simple*')
./simple: cannot open `./simple' (No such file or directory)
spaces.jpg: cannot open `spaces.jpg' (No such file or directory)
Ensuite, la sortie file
est analysée avec grep JPEG
. Ne pensez-vous pas qu'il est un peu facile de tromper un motif aussi simple, d'autant plus que la sortie de plain file
contient toujours le nom du fichier? Fondamentalement, tout ce qui contient "JPEG" dans son nom de fichier déclenchera une correspondance, quoi qu’il contienne.
$ echo "to be or not to be" > IAmNoJPEG.txt
$ file IAmNoJPEG.txt | grep JPEG
IAmNoJPEG.txt: ASCII text
Donc, nous avons la sortie file
de tous les fichiers JPEG (ou ceux qui prétendent en être un), maintenant ils traitent toutes les lignes avec cut
pour extraire le nom de fichier original de la première colonne, séparés par deux points ... Devinez quoi, essayons ceci sur un fichier avec deux points dans son nom:
$ ls colon*
colons:evil.jpeg
$ file colon* | grep JPEG | cut -f 1 -d ':'
colons
Donc, pour conclure, l’approche de votre livre fonctionne, mais seulement si tous les fichiers qu’il vérifie ne contiennent aucun espace, retour à la ligne, point-virgule et probablement d’autres caractères spéciaux et ne contiennent pas la chaîne "JPEG" où que ce soit dans leurs noms de fichiers. C’est aussi un peu moche, mais comme la beauté est dans l’œil du spectateur, je ne vais pas en parler.
Vous avez find
et vérifiez également avec la commande file
pour son type mime.
find . -type f -exec file --mime-type -b '{}' +
Ou pour le rendre complet comme suit:
find . -type f -exec sh -c '
file --mime-type -b "$0" | grep -q "aPATTERN" && printf "$0\n"
' {} \;
Ou l'option identify
des packages ImageMagic .
find -type f -print0 | xargs -0 identify