Accueil > Dossiers > UNIX

UNIX/LINUX
(tutorial 3 - filtres)

But de ce tutorial

Nous allons aborder la notion de filtre : un programme qui modifie l'entrée qu'il reçoit. En utilisant des infoducs, nous allons construit des associations de filtres permettant de faire de nouvelles opérations.


Commandes utilisées dans le présent tutorial, par ordre d'apparition

echo ; more ; less ; uniq ; cut ; fold ; tr ; grep ; wc ; awk. Soit les 10 commandes les plus utiles pour débuter dans les filtres.

SOMMAIRE DU TUTORIAL 3
a) Commande echo et re-directions
b) Afficher le contenu d'un fichier
c) Afficher une partie d'un fichier
d) Autour des commandes : infoduc, expressions régulières
e) Principaux filtres : uniq, cut, fold, tr
f) Rechercher une séquence dans des fichiers
g) Compter
h) Introduction à awk


a) Commande echo et re-directions >> vers le sommaire

La commande echo affiche par défaut sur la sortie standard (l'écran) ce qui la suit. Par exemple, echo Bonjour ! affichera Bonjour !. On peut effectuer des redirections sur les commandes ; ainsi, on peut modifier l'entrée (ce que vont recevoir les commandes) et la sortie (leur résultat). A la base, il y a 3 canaux utilisés par les commandes : 0 pour l'entrée (stdin en langage C), 1 pour la sortie classique (stdout en langage C), 2 pour la sortie d'erreur (stderr en langage C).

Pour effectuer une redirection, on place le caractère après la commande. Voici les redirections importantes :
> fichier, qui va créer un fichier et y stocker le résultat de la sortie standard. Si le fichier n'existe pas, il est créée. S'il existe, il est écrasé (c'est-à-dire remplacé par ce qu'on y fait à présent).
>> fichier, qui rajoute à un fichier le résultat de la sortie standard. Si le fichier n'existe pas, il est créée. S'il existe, ce qu'on y fait à présent est rajouté (on dit "concaténé").
n> fichier, qui utilise l'opérateur > avec un canal particulier (1 ou 2).
n>> fichier, qui utilise l'opérateur >> avec un canal particulier (1 ou 2).
< fichier, utilise le fichier comme entrée standard. Il doit exister.

Voici pour bien comprendre un ensemble de commandes qu'on tape, et des commentaires précédés de # :
echo Youhou !
# affiche Youhou !
echo "Youhou !"
# affiche un problème. En effet, la syntaxe est incorrecte, il ne faut pas de " "
echo Youhou ! > you.txt
# on a maintenant un fichier you.txt contenant Youhou
echo Yeah... > you.txt
# notre fichier you.txt contient maintenant Yeah... car il a été écrasé
echo "Hmm..." 2> /dev/null
# 2> fait une redirection de la sortie d'erreur. /dev/null est une sorte de poubelle où on jette les messages inutiles
echo Fin 1>> you.txt
# strictement équivalent à faire >> you.txt dans ce cas. On rajoute Fin au fichier you.txt (redirige sortie standard).

On peut utiliser quelques caractères spéciaux à l'intérieur du texte, comme \n qui passe à la ligne, ou \r qui revient au début de la ligne. Voici donc le texte que nous voulons :
"Monsieur,
Un monsieur du nom de Van Huik est entré ce matin.
Il a acheté un Van chez un ami à nous. Ce van est gris."

Ce texte est stupide mais il nous servira pour l'occurence des mots (nombre de fois qu'on trouve un mot). Ainsi, pour le mettre dans un fichier nommé texte.txt, rien de plus simple :
echo Monsieur,\nUn monsieur du nom de Van Huik est entré ce matin.\nIl a acheté un Van chez un ami à nous. Ce van est gris. > texte.txt

D'autres essais peuvent être amusants. Ainsi, essayez echo Mon beau sapin est lourd\rMon gros. On a aussi d'autres possibilités pour la commande echo, comme : \b (reculer d'un caractère), \xxx (afficher le caractère de code ASCII en octal xxx, exemple 068), \\ (faire simplement un anti slash).

Enfin, une remarque importante sur les redirections : selon votre shell (ce qui interprète les commandes de la fenêtre du terminal), et la configuration, elles ne réagissent pas de la même manière. En effet, dans zsh, on a un ! pour forcer l'écriture du fichier, et des protections de base. Nous reviendrons sur le sujet ultérieurement.

 

b) Afficher le contenu d'un fichier >> vers le sommaire

Il existe deux commandes pour cela : more et less. Exemple pour afficher le fichier précédent : more texte.txt ou less texte.txt. Pour afficher la ligne suivante, il suffit d'appuyer sur entrée ; pour la page suivante, on appuie sur espace.

Quelques options possibles pour more :
- more -c Texte.txt : efface l'écran avant d'afficher la page suivante
- more -s Texte.txt : regroupe les lignes blanches en une seule
- more +n Texte.txt : sauter n lignes avant d'afficher le fichier

Pour ce qui est des commandes interactives dans more, elles sont assez pauvres : s permet desauter une ligne, f de sauter un écran ; il y en a quelques autres, guère plus utile...

Comme la plupart des outils rudimentaires, more se passe de vérification et est donc assez robuste dans le sens où il permet d'afficher n'importe quoi. En revanche, less peut refuser d'afficher le contenu d'un fichier .png (une image)... Cependant, less a beaucoup plus d'options pour le piloter, et en particulier il peut utiliser une liste de fichiers. Par exemple, on peut faire less fichier1.txt fichier2.txt fichier3.txt, il va commencer par ouvrir fichier1.txt et nous propose de naviguer entre ces différents fichiers, qui constituent une liste.

Les possibilités pour piloter la commande less sont assez intéressantes :
- avec d on peut avancer, et b reculer
- r permet de rafraîchir l'écran : si le fichier est modifié alors qu'il est visualisé, on actualise l'affichage
- on peut marquer un passage du texte avec m suivi d'une lettre ; pour y revenir, on utilise alors ' suivi de la lettre qu'on a utilisé. On peut aussi faire suivre ' de $ (fin du fichier) ou ^ (début du fichier).
- avec /motif on peut rechercher un motif dans la suite du fichier ( ?motif permet de rechercher en arrière)
- avec :e on demande à examiner un nouveau fichier, on spécifie alors son nom. Il est affiché et ajouté à la liste des fichiers (si au départ un seul fichier avait été ouvert, une ligne apparait en bas, se terminant par (file n of m)).
- avec :p on passe au fichier précédent (previous) et :n au fichier suivant (next)
- :d supprimer le fichier courant de la liste des fichiers
- = donne des informations sur le fichier

 

c) Afficher une partie d'un fichier >> vers le sommaire

Avec head (tête) et tail (queue), on peut afficher le début et la fin d'un fichier. Les options suivantes sont valables pour les deux commandes :
- -n x. Affiche les x premières/dernières lignes. Exemple : head texte.txt -n 10 (affiche les 10 premières lignes).
- -c x. Afficher les x premiers/derniers blocs, en spécifiant l'unité par b (512 octets), k (1 Ko), m (1 Mo). Exemple : tail texte.txt -c 3b (afficher les derniers 1,5 Ko : 3 blocs de 512 octets).

Soit long.txt un texte dont on veut les 3 premières lignes et les 16 dernières. On éxécute les commandes suivantes afin d'extraire les lignes qui nous intéresse et de les mettre dans un nouveau fichier resume.txt :
head long.txt -n 3 > resume.txt     ; va créer un fichier resume.txt dans lequel on mettra les 3 premières lignes
tail long.txt -n 16 >> resume.txt    ; ajouter les 16 dernières lignes à resume.txt

 

d) Autour des commandes : infoduc, expressions régulières >> vers le sommaire

On désire associer plusieurs commandes afin de réaliser une action complexe. Prenons un exemple : on désire afficher l'anté-pénutième ligne d'un fichier (la ligne qui précède l'avant-dernière). Pour cela, il suffit d'afficher les 3 dernières lignes et de prendre la première ainsi obtenue :
tail memo02.pdf -n 3 | head -n 1

Ce | se nomme un 'infoduc' (on parle aussi de tube...) : le résultat de la commande de gauche est envoyé à la commande de droite. Autre exemple : on imagine qu'on est sur un serveur où il y a beaucoup de monde, et on aimerait se déplacer dans la liste des utilisateurs connectés. Rien de plus simple : w | less

Il est aussi possible d'éxécuter un ensemble d'actions. Par exemple, demandons dans une seule ligne à écrire l'heure et la date dans un fichier essai.txt : {time;date} > essai.txt. La structure { } permet de lister un ensemble de commande. Avec ; on demande une exécution séquentielle : time sera fait, puis date. On peut demander à ce que les commande s'exécutent en parallèle, avec &.

Lorsqu'on veut désigner un ensemble d'expressions, on peut utiliser ce qui s'appelle une expression régulière. [] permet de désigner un caractère parmis tous ceux énumérés ; exemple : [a-z] (toutes les lettres minuscules de a à z), [aplfb] (les lettres a, p, l, f et b), [a-fP-O] (les minucules de a à f puis les majuscules de P à O), [0-9] (les chiffres).

On peut aussi demander la négation de l'ensemble désigné avec ^. Par exemple, [^a-zA-Z] correspond à "tout sauf une lettre". A ne pas confondre avec ^ qui représente la position en début de ligne, et $ la position en fin de ligne.

 

e) Principaux filtres : uniq, cut, fold, tr >> vers le sommaire

La commande uniq permet d'exclure des lignes se ressemblant. Par défaut, elle exclut toutes les lignes successives identiques sauf une. Un exemple où elle peut se montrer utile : la commande finger affiche les utilisateurs connectés en prenant une ligne pour chaque de leurs terminaux. On imagine ne pas vouloir connaître tous les terminaux d'où se connectent les utilisateurs, et ainsi purger la liste. On peut le faire de la façon suivante :
finger | uniq -w 10
-w demande de limiter la comparaison entre les lignes aux 10 premiers caracteres. Autres options utiles :
- -s n : ne pas comparer les n premiers caractères
- -f : ignorer la casse des caractères (c'est-à-dire considérer que BoNjOuR est équivalent à Bonjour)

La commande cut, comme son nom l'indique, permet de couper son entrée. Avec -c on spécifie la liste des caractères à afficher. Par exemple :
more texte.txt | cut -c 2-7,15-38         ; va afficher du 2ème au 7ème caractère, puis du 15ème au 38ème

Avec fold on limite la longueur d'une ligne et on replie le surplus ; il y a trois options : -b (compter les octets), -w (compter les colonnes), -s (couper sur les blancs). Exemple :
finger | fold -w 10
Dont le résultat commencera par :
Login
 Name
        Tty
      Idle   Lo

Le filtre tr est plus intéressant : il permet de remplacer ou de supprimer une partie du texte. Avec -d on choisit de détruire, par exemple :
finger | tr -d [a-z]                            ; va supprimer toutes les lettres minuscules. [a-z] signifie "de a à z"
finger | tr -d [a-z][A-Z]                   ; va supprimer toutes les lettres. [a-z][A-Z] signifie "de a à z et de A à Z"
finger | tr -d [a-zA-Z]                      ; même chose écrite différement. Encore une expression régulière.
finger | tr -d [:blank:]                       ; va supprimer tous les espaces (les blancs)
finger | tr [:blank:] '_'                       ; va remplacer tous les espaces par des _
finger | tr [:upper:] [:lower:]             ; va remplacer toutes les majuscules (upper) par des minuscules (lower)
finger | tr ' ' '\n'                                ; va remplacer tous les espaces par des retour à la ligne
finger | tr [pts] '-'                             ; remplace les lettres p, t et s par -

Pour désigner un ensemble de caractères, on dispose de classes de caractères prédéfinies : [:alnum:] (lettres et chiffres), [:alpha:] (lettres), [:blank:] (blancs horizontaux), [:cntrl:] (caractères de contrôl), [:digit:] (chiffres), [:graph:] (caractères imprimables sauf blancs), [:lower:] (lettres minuscules), [:print:] (caractères imprimables y compris les blancs), [:punct:] (ponctuation), [:space:] (espaces), [:upper:] (lettres majuscules), [:xdigit:] (chiffres hexadecimaux).

 

f) Rechercher une séquence dans des fichiers >> vers le sommaire

La commande grep est très utile : elle permet de chercher une chaîne de caractères dans un ensemble de fichiers, et d'afficher les lignes concernées. Exemple :
echo Nous allons rechercher\nune séquence.\non cherche ... > essai.txt          ; on se fait un fichier
grep 'cherche' essai.txt                 ; va afficher la première ligne et la troisième : elles contiennent 'cherche'

On peut faire cette recherche sur plusieurs fichiers, par exemple : grep 'bonjour' fichier1.txt fichier2.txt. Dans ce cas, si la chaîne a été trouvée on nous indiquera dans quel fichier, comme fichier1.txt:bonjour tout le monde.

On dispose de nombreuses options :
- -A n pour afficher en plus les n lignes après la ligne qui contient le motif
- -B n pour précéder la ligne contenant le motif de n lignes
- -h lorsqu'on donne une liste de fichier, ne pas précéder le résultat du nom de fichier
- -i ignorer la casse
- -L affiche simplement le nom des fichiers dans lequel rien n'a été trouvé
- -l affiche simplement le nom des fichiers où la séquence a été trouvée

Exemple :
echo Bonjour tout le monde > fichier1.txt
echo bonjour tout le monde > fichier2.txt
grep -hi 'bonjour' fichier1.txt fichier2.txt
                   ; affiche"Bonjour tout le monde" et "bonjour tout le monde"

Comme avec tr, on peut utilise des classes de caractères prédéfinies ou des expressions. Par exemple :
echo 123456 > f1.txt
echo 789 > f2.txt
echo 5 > f3.txt
grep '[1-6]' f1.txt f2.txt f3.txt
          ; il y a des chiffres entre 1 et 6 dans f1 et dans f3
grep '[7-9]' f1.txt f2.txt f3.txt          ; il y a des chiffres enter 7 et 9 seulement dans f2
grep '[a-z]' f1.txt f2.txt f3.txt           ; il n'y a des caractères dans aucun fichier

Une dernière illustration :
echo Bonjour à tous\nBienvenue sur ce site\net à bientôt > bienvenue.txt
echo 7h. Dîner\nMerci d'etre a l'heure\n12hRepas\nFroid ou chaud > menu.txt
echo Bonus entre 1h et 3 > miam.txt
grep '^[^1-9]' bienvenue.txt menu.txt miam.txt
        ; n'affichera pas menu.txt : il commence par un chiffre
grep '^[A-Z]' bienvenue.txt menu.txt miam.txt          ; n'affichera pas menu.txt pour la même raison
grep '^[A-Z]?' bienvenue.txt menu.txt miam.txt        ; n'affichera rien. '?' demande du même type que [A-Z]...
grep '^[A-Z].' bienvenue.txt menu.txt miam.txt         ; n'affichera pas menu.txt. Il ne commence par pas une lettre

 

g) Compter >> vers le sommaire

La commande wc compte le nombre de lignes, mots ou caractères sur son entrée standard. Avec -l on demande le nombre de lignes, avec -w le nombre de mots, et avec -c le nombre de caractères. On peut par exemple demander combien de lignes contient un texte : wc -l texte.txt

Cependant, cette commande est plus souvent utilisée dans un infoduc. On peut arriver à s'en servir pour compter le nombre d'occurences d'un mot dans un texte. Le principe est d'utiliser la commande grep et de compter le nombre de lignes retenues, mais avec un traitement avant. En effet, en utilisant la commande grep directement, si le mot apparait plusieurs fois nous n'obtenons pourtant qu'une ligne (ce qui compte donc pour 1 seulement).
D'où : more texte.txt | tr [:space:] '\n' | grep -i 'mot' | wc -l
On envoit le texte à tr qui remplace tous les espaces par un retour à la ligne. Ainsi chaque mot est sur une ligne, et si grep trouve une fois le mot alors il sélectionnera une ligne complète. Au final, on compte le nombre de lignes.
Evidemment, on peut faire ceci en se passant de wc et en utilisant l'option -c qui permet à grep de compter !

Une utilisation plus simple peut-être pour compter le nombre de fichiers dans un répertoire. On sait que ls -1 affiche uniquement un fichier par ligne ; on compte les lignes : ls -1 | wc -l.

 

h) Introduction à awk >> vers le sommaire

Plus qu'un filtre, awk est un véritable petit langage. Nous allons commencer par l'utiliser comme filtre, comme grep. awk structure ce qu'il reçoit sous forme de mots : $0 représente l'intrégralité de la phrase, $1 le premier mot, $2 le second mot, etc. On considère comme mot les chaînes de caractéres séparées par des espaces.

Un programme awk est une succession de sélecteurs et de traitement. Les sélecteurs sont des expressions décrivant les lignes que l'on recherche ; les traitements sont structurés d'une façon proche du C où on peut utiliser des variables, boucles, tests...

Commencons par voir le découpage en mots, un exemple :
echo Bo n jou r > test.txt                    ; on se donne un petit fichier dont on voit clairement le découpage
more test.txt | awk '{print $0}'            ; On sélectionne l'ensemble de la phrase avec $0, et on l'affiche : print
more test.txt | awk '{print $1}'            ; On affiche ici le premier mot, qui est 'Bo'
more test.txt | awk '{print $2}'            ; On affiche le second mot qui est 'n'
more test.txt | awk '{print $389}'        ; Il n'y a pas de 389ème mot, on affiche donc... du vide.

On va maintenant s'attacher aux sélecteurs. On peut utiliser des expressions régulières avec le principe suivant : l'expression est entre '/ /', le * est un caractère spécialisé (i.e. signification particulière). Imaginons un fichier fictif id.txt, faisons quelques essais :
cat hip.txt | awk '/a/'            ; Rappel : cat affiche le contenu du fichier. Ici, on filtre les lignes qui contiennent un a.
cat hip.txt | awk '/a.*a/'       ; On veut une ligne qui ait 2 a. Le '.' représente un caractère quelconque, et le '*'                                             signifie "un nombre quelconque de fois ce qui précède" (peut-être nul)
cat hip.txt | awk '/^a/'          ; On veut une ligne qui commence par un a
cat hip.txt | awk '/a$/'          ; On veut une ligne qui se finisse par un a
cat hip.txt | awk '/^\*a$/'     ; On veut une ligne qui commence par un * et finisse par un a. Attention, * est                                             spécialisé : pour le considérer comme un simple caractère, on le fait précéder de \
cat hip.txt | awk '/^\*.*\*$/' ; On veut que la ligne commence et se finisse par une *
cat hip.txt | awk '/\$\$*$/'    ; On veut une ligne qui ne contienne que des $

En utilisant la fonction printf, on peut créer rapidement des variables pour son affichage. Par exemple, si on veut afficher des mots en particulier, on va faire des variables chaînes de caractère %s (pour String) :
ls -l | awk '{printf("Premier mot : %s \n Troisieme mot : %s \n",$1,$3)}'

Le modèle est printf("message %s %s %s",$1,$2,$3) où ce qui est entre " " sera affiché, et ce qui suit sert de variable à appeler lors de l'affichage. Chaque % est donc associé à une variable. On peut maintenant définir des chiffres selon la même idée :
ls -l | awk '{printf("La taille imaginaire de %s est %.3f \n",$9,$5/100)}'

%.3f signale qu'on a un nombre dont on affiche la précision jusqu'à 3 chiffres après la virgule. Notre nombre correspond à $5/100 c'est-à-dire le 5ème champ de ls -l (la taille du fichier) divisé par 100. awk arrive sans problème à faire cette opération. Si ce qu'on lui donne n'est pas un nombre, par exemple $1 (le champ des permissions), il ne va pas s'arrêter brutalement mais afficher simplement 0,000 à chaque fois.

Petit bilan d'exemple :
ls -l | awk '/c/ {printf("%s contient bien un c et mesure 10 fois %.1f\n",$9,$5/10)}'
Ceci affiche les fichiers dont le nom contient un c, accompagné de "et mesure 10 fois" avec la taille divisée par 10.