15.2. Commandes complexes

Commandes pour utilisateurs plus expérimentés

find

-exec COMMANDE \;

Exécute COMMANDE sur chaque fichier trouvé par find. La séquence de commandes se termine par un ; (le « ; » est échappé pour être certain que le shell le passe de façon littérale à find, sans l'interpréter comme un caractère spécial).

bash$ find ~/ -name '*.txt'
/home/bozo/.kde/share/apps/karm/karmdata.txt
/home/bozo/misc/irmeyc.txt
/home/bozo/test-scripts/1.txt
              

Si COMMAND contient {}, alors find substitue le chemin complet du fichier sélectionné à « {} ».

find ~/ -name 'core*' -exec rm {} \;
# Supprime tous les fichiers core à partir du répertoire de l'utilisateur.
find /home/bozo/projects -mtime 1
#  Liste tous les fichiers situés dans le répertoire /home/bozo/projects
#+ qui ont été modifiés il y a, au plus tard, 24 heures.
#
#  mtime = date de dernière modification du fichier cible
#  ctime = date de dernier changement d'état (via 'chmod' ou autre)
#  atime = date du dernier accès

REP=/home/bozo/fichiers_bidons
find "$REP" -type f -atime +5 -exec rm {} \;
#                                      ^^
#  Les accolades sont un indicateur pour le chemin trouvé par "find."
#
#  Efface tous les fichiers contenus dans "/home/bozo/fichiers_bidons"
#+ qui n'ont pas été accédés depuis au moins 5 jours.
#
#  "-type typefichier", où
#  f = fichier classique
#  d = répertoire
#  l = lien symbolique, etc.
#  (La page de manuel et la page info de 'find' en ont une liste complète.)
find /etc -exec grep '[0-9][0-9]*[.][0-9][0-9]*[.][0-9][0-9]*[.][0-9][0-9]*' {} \;

#  Trouve toutes les adresses IP (xxx.xxx.xxx.xxx) contenues dans les fichiers
#+ situés dans le répertoire /etc .
#  Quelques correspondances n'auront rien à voir - Peuvent-elles être
#+ éliminées ?

# Peut-être en faisant:

find /etc -type f -exec cat '{}' \; | tr -c '.[:digit:]' '\n' \
 | grep '^[^.][^.]*\.[^.][^.]*\.[^.][^.]*\.[^.][^.]*$'
#
#  [:digit:] est un ensemble de caractères POSIX 1003.2
#+ introduit avec le standard POSIX 1003.2.

# Merci, Stéphane Chazelas.
[Note]

Note

L'option -exec de find ne doit pas être confondue avec la commande intégrée du shell exec.

Exemple 15.3. Badname élimine dans le répertoire courant les fichiers dont le nom contient des caractères incorrects et des espaces blancs.

#!/bin/bash
# badname.sh

# Efface les fichiers du répertoire courant contenant des mauvais caractères.

for nomfichier in *
do
  mauvaisnom=`echo "$nomfichier" | sed -n /[\+\{\;\"\\\=\?~\(\)\<\>\&\*\|\$]/p`
# mauvaisnom=`echo "$nomfichier" | sed -n '/[+{;"\=?~()&lt;&gt;&*|$]/p'`  fonctionne aussi.
#  Supprime les fichiers contenant les "mauvais" caractères :
#+   + { ; " \ = ? ~ ( ) < > & * | $
#
  rm $mauvaisnom 2>/dev/null
#                ^^^^^^^^^^^ Supression des messages d'erreur.
done

# Maintenant, faire attention aux noms de fichiers contenant des espaces blancs.
find . -name "* *" -exec rm -f {} \;
# Le chemin du fichier trouvé par _find_ remplace "{}".
#  Le '\' nous assure que le ';' est interprété littéralement, c'est-à-dire comme une fin
#+ de commande.

exit 0

#---------------------------------------------------------------------
# Les commandes ci-dessous ne seront pas exécutées à cause de la commande
# _exit_ au dessus.

# Voici une alternative au script ci-dessus:
find . -name '*[+{;"\\=?~()&lt;&gt;&*|$ ]*' -maxdepth 0 \
-exec rm -f '{}' \;
#  L'option "-maxdepth 0" nous assure que _find_ ne cherchera pas dans les
#+ sous-répertoires de $PWD.

# (Merci, S.C.)

Exemple 15.4. Effacer un fichier par son numéro d'inode

#!/bin/bash
# idelete.sh : Effacer un fichier grâce à son inode.

#  C'est très utile quand un nom de fichier commence avec un caractère illégal,
#+ comme un ? ou -.

NBARGS=1          # L'argument du nom de fichier doit être passé au script.
E_MAUVAISARGS=70
E_FICHIER_INEXISTANT=71
E_CHANGE_D_ESPRIT=72

if [ $# -ne "$NBARGS" ]
then
  echo "Usage: `basename $0` nomfichier"
  exit $E_MAUVAISARGS
fi  

if [ ! -e "$1" ]
then
  echo "Le fichier \""$1"\" n'existe pas."
  exit $E_FICHIER_INEXISTANT
fi  

inum=`ls -i | grep "$1" | awk '{print $1}'`
# inum = inode (NdT : index node) numéro de fichier
# --------------------------------------------------------------------------
#  Chaque fichier possède un inode contenant ses informations d'adresses
#+ physiques.
# --------------------------------------------------------------------------

echo; echo -n "Effacer vraiment \"$1\" (o/n)? "
# L'option '-v' de 'rm' pose la même question.
read reponse
case "$reponse" in
[nN]) echo "Vous avez changé d'avis."
      exit $E_CHANGE_D_ESPRIT
      ;;
*)    echo "Effacement en cours du fichier \"$1\".";;
esac

find . -inum $inum -exec rm {} \;
#                           ^^
#        Les accolades sont des emplacements réservés
#+       pour la sortie de texte par "find".
echo "Fichier "\"$1"\" effacé !"

exit 0

La commande find fonctionne aussi sans l'option -exec.

#!/bin/bash
#  Trouve les fichiers suid root.
#  Un fichier suid étrange pourrait indiquer une faille de sécurité
#+ voire même une intrusion dans le système.

repertoire="/usr/sbin"
# Essayer aussi /sbin, /bin, /usr/bin, /usr/local/bin, etc.
droits="+4000"  # suid root (dangerous!)


for fichier in $( find "$repertoire" -perm "$droits" )
do
  ls -ltF --author "$fichier"
done

Voir l'Exemple 15.28, « Utiliser cpio pour déplacer un répertoire complet », l'Exemple 3.4, « Sauvegarde de tous les fichiers modifiés dans les dernières 24 heures » et l'Exemple 10.9, « Rechercher les auteurs de tous les binaires d'un répertoire » pour des exemples de scripts utilisant find. La page de manuel de cette commande, complexe et puissante, apporte des détails supplémentaires.

xargs

Un filtre qui sert à passer des paramètres à une commande, et aussi un outil pour réunir les commandes elles-mêmes. Il découpe un flux de données en des morceaux suffisamment petits pour laisser les filtres et les commandes opérer. Considérez-le comme une puissante alternative aux guillemets inversés. Dans les situations où la substitution de commandes échoue avec une erreur too many arguments (trop d'arguments), utiliser xargs règle souvent les problèmes. [49] Habituellement, xargs lit depuis stdin ou depuis un tube mais il accepte aussi de lire dans la sortie d'un fichier.

La commande par défaut d'xargs est echo. Cela signifie que tout flux entrant transmis via un tube vers xargs peut voir ses sauts de ligne et caractères d'espacements supprimés.

bash$ ls -l
total 0
 -rw-rw-r--    1 bozo  bozo         0 Jan 29 23:58 fichier1
 -rw-rw-r--    1 bozo  bozo         0 Jan 29 23:58 fichier2


bash$ ls -l | xargs
total 0 -rw-rw-r-- 1 bozo bozo 0 Jan 29 23:58 fichier1 -rw-rw-r-- 1 bozo bozo 0 Jan...



bash$ find ~/mail -type f | xargs grep "Linux"
./misc:User-Agent: slrn/0.9.8.1 (Linux)
 ./sent-mail-jul-2005: hosted by the Linux Documentation Project.
 ./sent-mail-jul-2005: (Linux Documentation Project Site, rtf version)
 ./sent-mail-jul-2005: Subject: Criticism of Bozo's Windows/Linux article
 ./sent-mail-jul-2005: while mentioning that the Linux ext2/ext3 filesystem
 . . .
              

ls | xargs -p -l gzip : Compresse avec gzip tous les fichiers du répertoire courant, un à un, et demande confirmation avant chaque opération.

[Astuce]

Astuce

Une option intéressante d'xargs est -n NN, qui limite à NN le nombre d'arguments passés.

ls | xargs -n 8 echo : Affiche le contenu du répertoire courant sur 8 colonnes.

[Astuce]

Astuce

Une autre option utile est -0, combinée avec find -print0 ou grep -lZ. Ceci permet de manipuler les arguments contenant des espaces ou des quotes.

find / -type f -print0 | xargs -0 grep -liwZ GUI | xargs -0 rm -f

grep -rliwZ GUI / | xargs -0 rm -f

N'importe laquelle des commande ci-dessus effacera tout fichier contenant « GUI ». (Merci, S.C.)

Or:

cat /proc/"$pid"/"$OPTION" | xargs -0 echo
#  Formate la sortie :     ^^^^^^^^^^^^^^^
#  À partir de la correction de Han Holl sur le script "get-commandline.sh"
#+ du chapitre "/dev et /proc".

Exemple 15.5. Fichier de traces utilisant xargs pour surveiller les journaux système

#!/bin/bash

# Génère un journal de traces dans le répertoire courant à partir de la fin de
# /var/log/messages.

# Note : /var/log/messages doit être lisible par tout le monde si le script
# est appelé par un utilisateur simple.
#         #root chmod 644 /var/log/messages

LIGNES=5

( date; uname -a ) >>fichiertraces
# Date, heure et nom de l'ordinateur
echo --------------------------------------------------------------------- >>fichiertraces
tail -$n LIGNES /var/log/messages | xargs |  fmt -s >>fichiertraces
echo >>fichiertraces
echo >>fichiertraces

exit 0

#  Note:
#  ----
#  Frank Wang précise que les guillemets qui ne se suivent pas (soit des
#+ simples soit des doubles) dans le fichier source pourraient donner
#+ une indigestion à xargs.
#
#  Il suggère d'utiliser la substitution suivante pour la ligne 15 :
#     tail -n $LIGNES /var/log/messages | tr -d "\"'" | xargs | fmt -s >>logfile



#  Exercice:
#  --------
#  Modifier ce script pour tracer les modifications dans /var/log/messages
#+ à des intervalles de 20 minutes.
#  Astuce : utilisez la commande "watch".

Comme avec find, une paire d'accolades sert à indiquer un texte à remplacer.

Exemple 15.6. Copier les fichiers du répertoire courant vers un autre répertoire en utilisant xargs

#!/bin/bash
# copydir.sh

#  Copie verbeusement tous les fichiers du répertoire courant ($PWD)
#+ dans le répertoire spécifié sur la ligne de commande

E_NOARGS=65

if [ -z "$1" ]   # Quitte si aucun paramètre n'est fourni.
then
  echo "Usage: `basename $0` rep-destination"
  exit $E_NOARGS
fi  

ls . | xargs -i -t cp ./{} $1
#            ^^ ^^      ^^
#  -t est l'option "verbeuse" (affiche la ligne de commande sur stderr).
#  -i est l'option de "remplacement des chaînes".
#  {} est un emplacement réservé pour le texte en sortie.
#  C'est similaire en utilisation à une paire d'accolades pour "find."
#
#  Liste les fichiers du répertoire courant (ls .),
#+ utilise la sortie de "ls" comme argument pour "xargs" (options -i -t),
#+ puis copie (cp) ces arguments ({}) vers le nouveau répertoire ($1).  
#
#  Le résultat net est l'équivalent exact de
#+   cp * $1
#+ sauf si un des noms de fichiers contient des espaces blancs.

exit 0

Exemple 15.7. Tuer des processus par leur nom

#!/bin/bash
# kill-byname.sh: Tuer des processus suivant leur nom.
# Comparez ce script avec kill-process.sh.

#  Par exemple,
#+ essayez "./kill-byname.sh xterm" --
#+ et regardez toutes les xterm disparaître de votre bureau.

#  Attention :
#  -----------
#  C'est un script assez dangereux.
#  Lancez-le avec précaution (spécialement en tant que root)
#+ car il peut causer des pertes de données et d'autres effets indésirables.

E_MAUVAISARGUMENTS=66

if test -z "$1"  # Aucun argument n'a été fourni en ligne de commande ?
then
  echo "Usage: `basename $0` Processus_à_tuer"
  exit $E_MAUVAISARGUMENTS
fi


NOM_PROCESSUS="$1"
ps ax | grep "$NOM_PROCESSUS" | awk '{print $1}' | xargs -i kill {} 2&>/dev/null
#                                                       ^^      ^^

# -------------------------------------------------------------------------
# Notes:
# -i est l'option des chaînes de remplacement d'xargs.
# Les accolades sont l'emplacement du remplacement.
# 2&>/dev/null supprime les messages d'erreurs non souhaités.
#
# grep "$NOM_PROCESSUS" peut-il être remplacé par pidof "$NOM_PROCESSUS" ?
# -------------------------------------------------------------------------

exit $?

#  La commande "killall" a le même effet que ce script,
#+ mais l'utiliser n'a pas le même effect éducatif.

Exemple 15.8. Analyse de la fréquence des mots en utilisant xargs

#!/bin/bash
# wf2.sh : Analyse crue de la fréquence des mots sur un fichier texte.

# Utilise 'xargs' pour décomposer les lignes de texte en des mots simples.
# Comparez cet exemple avec le script "wf.sh" qui suit.


# Vérification du fichier en entrée sur la ligne de commande.
ARGS=1
E_MAUVAISARG=65
E_FICHIERINEXISTANT=66

if [ $# -ne "$ARGS" ]
# Est-ce que le bon nombre d'arguments a été passé au script ?
then
  echo "Usage: `basename $0` nomfichier"
  exit $E_MAUVAISARG
fi

if [ ! -f "$1" ]       # Vérifie si le fichier existe.
then
  echo "Le fichier \"$1\" n'existe pas."
  exit $E_FICHIERINEXISTANT
fi



#######################################################
cat "$1" | xargs -n1 | \
#  Liste le fichier, un mot par ligne.
tr A-Z a-z | \
# Transforme les caractères en minuscule.
sed -e 's/\.//g'  -e 's/\,//g' -e 's/ /\
/g' | \
#  Filtre les points et les virgules
#+ et remplace l'espace entre les mots par des retours chariot,
  sort | uniq -c | sort -nr
# Finalement, ajoute en préfixe le nombre d'occurence et le trie.
#######################################################

#  Ceci fait le même travail que l'exemple "wf.sh" qui va suivre,
#+ mais il est un peu plus lourd et fonctionne moins rapidement (pourquoi ?).

exit 0

expr

Évaluateur d'expression : Concatène et évalue les arguments suivant l'opération souhaitée (les arguments doivent être séparés par des espaces). Les opérations peuvent être arithmétiques, comparatives, chaînes de caractères ou logiques.

expr 3 + 5

renvoie 8

expr 5 % 3

renvoie 2

expr 1 / 0

renvoie le message d'erreur, expr: division by zero

Opérations arithmétiques illégales non autorisées.

expr 5 \* 3

renvoie 15

L'opérateur de multiplication doit être échappé lorsqu'il est utilisé dans une expression arithmétique avec expr.

y=`expr $y + 1`

Incrémente une variable, de la même manière que let y=y+1 et y=$(($y+1)). Ceci est un exemple d'expansion arithmétique.

z=`expr substr $chaine $position $longueur`

Extrait une sous-chaîne de caractères de $longueur caractères, en partant de $position.

Exemple 15.9. Utiliser expr

#!/bin/bash

# Démonstration des possibilités de 'expr'
# ========================================

echo

# Opérations arithmétiques
# ------------------------

echo "Opérations arithmétique"
echo
a=`expr 5 + 3`
echo "5 + 3 = $a"

a=`expr $a + 1`
echo
echo "a + 1 = $a"
echo "(incrémentation d'une variable)"

a=`expr 5 % 3`
# modulo
echo
echo "5 mod 3 = $a"

echo
echo

# Opérations logiques
# -------------------

#  Retourne 1 si vrai, 0 si faux,
#+ à l'opposé des conventions de Bash.

echo "Opérations logiques"
echo

x=24
y=25
b=`expr $x = $y`         # Test d'égalité.
echo "b = $b"            # 0  ( $x -ne $y )
echo

a=3
b=`expr $a \> 10`
echo 'b=`expr $a \> 10`, donc...'
echo "If a > 10, b = 0 (faux)"
echo "b = $b"            # 0  ( 3 ! -gt 10 )
echo

b=`expr $a \< 10`
echo "If a < 10, b = 1 (vrai)"
echo "b = $b"            # 1  ( 3 -lt 10 )
echo
# Notez l'échappement des opérations.

b=`expr $a \<= 3`
echo "If a <= 3, b = 1 (vrai)"
echo "b = $b"            # 1  ( 3 -le 3 )
# Il y a aussi l'opérande "\>=" (plus grand que ou égal à).


echo
echo



# Opérateur de chaine de caractères
# ---------------------------------

echo "Opérateur de chaînes de caractères"
echo

a=1234zipper43231
echo "Voici \"$a\"."

# length : longueur de la chaine
b=`expr length $a`
echo "La taille de \"$a\" est $b."

# index : position du premier caractère dans une sous-chaine
#         qui correspond à un caractère dans la chaine.
b=`expr index $a 23`
echo "La position du premier \"2\" dans \"$a\" est \"$b\"."

#  substr : extrait une sous-chaîne, en spécifiant la position de départ et la
#+          taille
b=`expr substr $a 2 6`
echo "La sous-chaîne de \"$a\", commençant à la position 2,\
et long de 6 caractère est \"$b\"."


#  L'attitude par défaut de l'opérateur 'match' est de
#+ chercher une occurence à partir ***du début*** de la chaîne.
#
#        utilisation des expressions régulières
b=`expr match "$a" '[0-9]*'`               #  Comptage numérique.
echo Le nombre de chiffres au début de \"$a\" est $b.
b=`expr match "$a" '\([0-9]*\)'`           #  Notez que les parenthèses échappées
#                   ==      ==              + déclenchent une reconnaissance de
#                                           + sous-chaîne.
echo "Les chiffres au début de \"$a\" sont \"$b\"."

echo

exit 0

[Important]

Important

L'opérateur : est équivalent à match. Par exemple, b=`expr $a : [0-9]*` est l'équivalent exact de b=`expr match $a [0-9]*` du listing précédent.

#!/bin/bash

echo
echo "Opérations avec des chaînes utilisant la construction \"expr \$string : \"
echo "======================================================================="
echo

a=1234zipper5FLIPPER43231

echo "La chaîne en cours est     \"`expr "$a" : '\(.*\)'`\"."
# Parenthèses échappées groupant l'opérateur.    ==  ==

#       *********************************
#+          Les parenthèses échappées
#+       correspondent à une sous-chaîne.
#       *********************************


#  Si les parenthèses ne sont pas échappées...
#+ alors 'expr' convertit la chaîne en un entier.

echo "La taille de \"$a\" est `expr "$a" : '.*'`."   # Taille de la chaîne

echo "Le nombre de chiffre au début de \"$a\" est `expr "$a" : '[0-9]*'`."

# ------------------------------------------------------------------------- #

echo

echo "Les chiffres au début de \"$a\" sont `expr "$a" : '\([0-9]*\)'`."
#                                                             ==      ==
echo "Les sept premières lettres de \"$a\" sont `expr "$a" : '\(.......\)'`."
#           =========                                      ==       ==
# Encore, les parenthèses échappées forcent une correspondance de sous-chaîne
#
echo "Les sept dernières lettres de \"$a\" sont `expr "$a" : '.*\(.......\)'`."
#           =========           fin de l'opérateur chaîne  ^^
#  (en fait, ça signifie qu'il saute un ou plus jusqu'à une sous-chaîne
#+  spécifiée)

echo

exit 0

Le script ci-dessus illustre comment expr utilise les opérateurs groupant appelés parenthèses echappées -- \( ... \) -- en tandem avec une analyse basée sur les expressions rationnelles pour faire coïncider une sous-chaîne de caractères. Voici un autre exemple, cette fois provenant de la « vie réelle. »

# Supprimer les espaces en début et en fin.
LRFDATE=`expr "$LRFDATE" : '[[:space:]]*\(.*\)[[:space:]]*$'`

#  Provient du script "booklistgen.sh" de Peter Knowles,
#+ script convertissant des fichiers au format Librie de Sony.
#  (http://booklistgensh.peterknowles.com)

Perl, sed et awk ont des capacités d'analyse de chaînes de caractères très largement supérieures. Une petite sous-routine sed ou awk dans un script (voir la Section 33.3, « Scripts d'appel ») est aussi une bonne alternative à l'utilisation d'expr.

Voir la Section 9.2, « Manipuler les chaînes de caractères » pour en savoir plus sur l'utilisation d'expr dans les manipulations des chaînes de caractères.



[49] Et même quand xargs n'est pas strictement nécessaire, il peut accélérer l'exécution d'une commande impliquant le traitement en flot de plusieurs fichiers.