Manipuler fichiers et répertoires
Pourquoi
Votre deuxième jour sur sig-app-1. On vous demande trois choses, banales en apparence : faire une copie de sauvegarde de la configuration de Signalements avant que l'équipe la modifie, archiver les journaux du mois dernier pour les envoyer à un client, et faire de la place, parce que la supervision signale un disque rempli à 92 %.
Chacune de ces tâches a sa façon de mal tourner. Une copie faite sans les bonnes options change le propriétaire ou la date du fichier, et l'application ne le lit plus. Un rm lancé avec une variable vide efface autre chose que prévu, et Linux n'a pas de corbeille en ligne de commande : ce qui est supprimé l'est pour de bon. Et le plus déroutant : vous supprimez un journal de 4 Go, ls confirme qu'il n'existe plus, mais le disque reste plein.
Ces surprises ont toutes la même origine : on manipule des noms de fichiers, alors que le système, lui, gère des objets auxquels ces noms font référence. Cette leçon vous donne les commandes, et surtout ce modèle, qui explique leurs comportements étranges.
Les concepts
Un nom n'est pas un fichier
Sous Linux, un fichier est d'abord un objet du système de fichiers, décrit par un inode (index node, nœud d'index). La page de manuel inode(7) en liste le contenu : le type de fichier et ses droits, le propriétaire et le groupe, la taille, les dates d'accès, de modification et de changement d'état, l'emplacement des données sur le disque, et un compteur de liens. Ce qui n'y figure pas, c'est le nom.
Le nom vit dans le répertoire. Un répertoire n'est rien d'autre qu'une table qui associe des noms à des numéros d'inode. Quand vous tapez cat app.conf, le système cherche app.conf dans la table du répertoire courant, en tire un numéro d'inode, puis lit les données de cet inode.
flowchart LR
subgraph R["Répertoire config/"]
n1["app.conf → 25455773"]
n2["dur.conf → 25455773"]
n3["symb.conf → 25455774"]
end
i1["inode 25455773<br/>droits, propriétaire, taille<br/>liens : 2<br/>données : port = 8000..."]
i2["inode 25455774<br/>type : lien symbolique<br/>contenu : « app.conf »"]
n1 --> i1
n2 --> i1
n3 --> i2
i2 -. "le nom app.conf" .-> n1
Cette séparation entre nom et objet explique presque tout ce qui suit :
- renommer un fichier ne fait que modifier une entrée de répertoire, ce qui est instantané même pour un fichier de 100 Go ;
- un même inode peut avoir plusieurs noms (des liens physiques) ;
- supprimer un fichier, c'est retirer un nom ; les données ne disparaissent que quand plus aucun nom et plus aucun programme ne les référencent.
Liens physiques et liens symboliques
Un lien physique (hard link) est simplement un nom supplémentaire pour un inode existant. Les deux noms sont parfaitement égaux : aucun n'est « l'original ». Le compteur de liens de l'inode compte ces noms. La page symlink(7) en donne les deux limites : un lien physique ne peut pas désigner un répertoire (ce qui pourrait créer des boucles dans l'arborescence), et il ne peut pas traverser deux systèmes de fichiers, parce qu'un numéro d'inode n'a de sens qu'à l'intérieur du sien.
Un lien symbolique (symbolic link, symlink) est un fichier à part entière, avec son propre inode, dont le contenu est un chemin. Quand un programme ouvre le lien, le noyau lit ce chemin et le suit. Le lien symbolique peut donc désigner un répertoire, un autre système de fichiers, et même un chemin qui n'existe pas : symlink(7) précise qu'il n'est pas exigé que la cible existe. Un lien dont la cible a disparu s'appelle un lien cassé (dangling link). Dernier détail qui piège souvent : un chemin relatif stocké dans un lien est interprété à partir du répertoire du lien, et non du répertoire où vous vous trouvez.
| Lien physique | Lien symbolique | |
|---|---|---|
| Ce que c'est | Un nom de plus pour le même inode | Un petit fichier qui contient un chemin |
| Commande | ln cible nom | ln -s cible nom |
| Vers un répertoire | Non | Oui |
| Entre deux systèmes de fichiers | Non | Oui |
| Si la cible est supprimée | Les données restent accessibles par l'autre nom | Le lien est cassé |
| Usage typique | Rare à la main ; outils de sauvegarde par instantanés | Versions d'une application (courant -> v1.2.0), configuration activée (sites-enabled/) |
Vous croiserez des liens symboliques partout sur un serveur Ubuntu : /bin est un lien vers usr/bin, les sites activés de Nginx sont des liens vers sites-available/, et /usr/bin/editor désigne l'éditeur par défaut à travers une chaîne de liens (leçon 5).
Supprimer, c'est retirer un nom
La page unlink(2), qui décrit l'appel système utilisé par rm, est très claire : si le nom supprimé était le dernier lien vers le fichier et qu'aucun processus ne l'a ouvert, le fichier est supprimé et son espace est rendu ; si un processus l'a encore ouvert, le fichier continue d'exister jusqu'à ce que le dernier descripteur qui y fait référence soit fermé.
C'est l'explication du disque qui reste plein. Le journal de 4 Go que vous avez supprimé était ouvert par le processus qui y écrivait. Le nom a disparu, l'inode et ses données sont toujours là, et le processus continue même d'y écrire. L'espace ne reviendra qu'au redémarrage ou à l'arrêt de ce processus. La pratique de cette leçon le montre en direct.
Renommer ou déplacer
mv utilise l'appel système rename(2). Quand la source et la destination sont sur le même système de fichiers, c'est une simple modification d'entrées de répertoire, instantanée, et atomique : rename(2) garantit qu'un autre processus qui accède à la destination ne la trouve jamais absente, il voit soit l'ancien fichier, soit le nouveau. C'est la technique de base pour remplacer un fichier de configuration sans qu'un programme lise un fichier à moitié écrit : on écrit le nouveau contenu dans un fichier temporaire, dans le même répertoire, puis on le renomme par-dessus l'ancien.
Quand la destination est sur un autre système de fichiers, rename(2) échoue avec l'erreur EXDEV. mv le détecte et se rabat sur une copie suivie d'une suppression. C'est plus lent, ce n'est plus atomique, et si la copie est interrompue, vous pouvez vous retrouver avec un fichier partiel à la destination. Pour savoir si deux chemins sont sur le même système de fichiers, comparez leur numéro de périphérique avec stat -c '%d'.
En pratique
Les sorties de cette leçon ont été produites sur Ubuntu 24.04 avec LC_ALL=C.UTF-8, pour obtenir les messages en anglais, tels qu'ils apparaissent sur la plupart des serveurs ; le nom d'utilisateur est camille. Faites tout dans un répertoire d'essai, par exemple ~/essais, jamais dans /etc ou /opt.
$ mkdir ~/essais && cd ~/essais
Créer des répertoires et des fichiers
$ mkdir a/b
mkdir: cannot create directory ‘a/b’: No such file or directory
$ mkdir -p signalements/config/archives
$ ls -R signalements
signalements:
config
signalements/config:
archives
signalements/config/archives:
mkdirrefuse de créera/bsian'existe pas. L'option-p(parents) crée tous les répertoires intermédiaires et ne se plaint pas s'ils existent déjà : c'est la forme à utiliser dans les scripts.ls -Rliste récursivement.
touch crée un fichier vide s'il n'existe pas (et, s'il existe, met simplement sa date de modification à l'heure courante). stat affiche ce que contient l'inode :
$ touch signalements/config/app.conf
$ stat signalements/config/app.conf
File: signalements/config/app.conf
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: 259,2 Inode: 25455773 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1000/ camille) Gid: ( 1000/ camille)
Access: 2026-10-05 11:09:08.089166370 +0200
Modify: 2026-10-05 11:09:08.089166370 +0200
Change: 2026-10-05 11:09:08.089166370 +0200
Birth: 2026-10-05 11:09:08.089166370 +0200
Tout y est : le type (regular empty file), le périphérique et le numéro d'inode, le nombre de liens, les droits (leçon 9), le propriétaire, et quatre dates. Modify change quand le contenu change, Change quand l'inode change (droits, propriétaire, nombre de liens), Access à la lecture (avec des nuances de montage), et Birth est la date de création, quand le système de fichiers la conserve.
Copier : cp et ses options
Écrivez un contenu dans le fichier, puis comparez une copie simple et une copie en mode archive :
$ cd signalements/config
$ printf 'port = 8000\nworkers = 2\n' > app.conf
$ cp app.conf copie-simple.conf
$ cp -a app.conf copie-a.conf
$ ls -l --time-style=full-iso app.conf copie-*
-rw-rw-r-- 1 camille camille 24 2026-10-05 11:09:13.491642179 +0200 app.conf
-rw-rw-r-- 1 camille camille 24 2026-10-05 11:09:13.491642179 +0200 copie-a.conf
-rw-rw-r-- 1 camille camille 24 2026-10-05 11:09:18.592109134 +0200 copie-simple.conf
La copie simple porte la date du moment où elle a été faite ; la copie -a a gardé la date de l'original, à la nanoseconde. Ce n'est pas un détail : une copie de sauvegarde qui a perdu sa date ne dit plus de quand date la configuration qu'elle contient.
Les options à connaître, telles que les décrit le manuel de GNU Coreutils :
-a(archive) équivaut à-dR --preserve=all: copie récursive, liens symboliques copiés comme liens (et non suivis), et conservation de tous les attributs (droits, propriétaire, dates, attributs étendus). C'est l'option des sauvegardes. Notez que conserver le propriétaire d'un fichier qui ne vous appartient pas n'est possible qu'enroot.-péquivaut à--preserve=mode,ownership,timestamps: droits, propriétaire et dates, sans la récursivité.-r(ou-R) copie un répertoire et son contenu. Sans elle,cprefuse :
$ cp archives autre
cp: -r not specified; omitting directory 'archives'
-i(interactive) demande confirmation avant d'écraser un fichier existant :
$ cp -i app.conf copie-a.conf
cp: overwrite 'copie-a.conf'?
--backup=numberedgarde l'ancienne version de la destination au lieu de l'écraser, sous un nom numéroté :
$ cp --backup=numbered app.conf copie-a.conf
$ cp --backup=numbered app.conf copie-a.conf
$ ls
app.conf archives copie-a.conf copie-a.conf.~1~ copie-a.conf.~2~ copie-simple.conf
Par défaut, cp écrase la destination sans rien demander. Retenez-le : c'est la cause la plus fréquente de perte de fichier par copie.
La sauvegarde avant modification
La règle d'or de l'administration : avant de modifier un fichier de configuration, on en fait une copie datée, en conservant ses attributs.
$ sudo cp -a /etc/signalements/app.conf /etc/signalements/app.conf.$(date +%F)
$(date +%F) est remplacé par la date du jour au format 2026-10-05 (une substitution de commande, détaillée à la leçon 6). La copie est faite dans le même répertoire, ce qui conserve le contexte, et en -a, ce qui conserve le propriétaire root et les droits restreints. Après la modification, diff -u (leçon 5) montre exactement ce qui a changé, et revenir en arrière tient en une commande.
Deux réserves. D'abord, certains programmes lisent tous les fichiers d'un répertoire de configuration (c'est le cas des répertoires en .d/, comme /etc/sudoers.d/ ou /etc/apt/sources.list.d/) : une copie de sauvegarde posée là peut être chargée comme une configuration supplémentaire. Dans ces répertoires, rangez la copie ailleurs. Ensuite, cette copie n'est pas une sauvegarde au sens du cours Sauvegarde, restauration et PRA : elle protège d'une fausse manœuvre, pas de la perte du serveur. Si la configuration est gérée par un outil (Ansible, cloud-init, Git), la vraie référence est dans cet outil.
Déplacer et renommer : mv
$ mv copie-simple.conf ancienne.conf # renommer
$ mv ancienne.conf archives/ # déplacer dans un répertoire
$ mv -i app.conf copie-a.conf # demander avant d'écraser
$ mv -n app.conf copie-a.conf # ne jamais écraser
Comme cp, mv écrase la destination sans prévenir par défaut ; -i demande, -n (no-clobber) refuse. Pour vérifier si deux chemins sont sur le même système de fichiers, donc si mv sera un simple renommage :
$ stat -c '%d %n' . /dev/shm
66306 .
29 /dev/shm
Les numéros de périphérique diffèrent : déplacer un gros fichier d'ici vers /dev/shm (un système de fichiers en mémoire) serait une copie complète suivie d'une suppression.
Supprimer : rm et rmdir
$ rmdir signalements
rmdir: failed to remove 'signalements': Directory not empty
$ rm signalements
rm: cannot remove 'signalements': Is a directory
$ rm -ri signalements
rm: descend into directory 'signalements'?
rmdirne supprime qu'un répertoire vide : c'est sa sécurité.rmsans option ne supprime que des fichiers.rm -rsupprime un répertoire et tout son contenu, récursivement ;-idemande confirmation pour chaque élément ;-f(force) ne demande jamais rien et ne signale pas les fichiers absents.
rm -rf est donc la commande la plus dangereuse que vous taperez régulièrement. Il n'y a pas de corbeille : William Shotts le rappelle dans The Linux Command Line, Linux n'a pas de commande pour annuler une suppression. Deux habitudes réduisent le risque :
- Remplacez
rmparlsd'abord.ls -d /var/log/signalements/*.gzmontre exactement ce querm /var/log/signalements/*.gzsupprimera. Shotts recommande cette technique, et elle protège en particulier des motifs mal tapés :rm * .gz(avec une espace) supprime tout le répertoire, puis se plaint qu'il n'existe pas de fichier.gz. - Méfiez-vous des variables dans un chemin. Voici ce que voit vraiment le shell quand une variable n'est pas définie :
$ echo rm -rf "$REPERTOIRE_CACHE/"
rm -rf /
Le echo devant la commande affiche ce qui serait exécuté au lieu de l'exécuter. Si REPERTOIRE_CACHE est vide (faute de frappe dans son nom, script lancé dans un autre contexte), la commande devient rm -rf /. GNU rm refuse d'opérer récursivement sur / grâce à l'option --preserve-root, active par défaut, mais cette protection ne vaut que pour / lui-même : rm -rf "$REPERTOIRE_CACHE"/* ou rm -rf "$APP/data" avec une variable vide effaceront bien tout ce qui correspond. La protection fiable est dans le shell : la forme ${VARIABLE:?message} arrête la commande si la variable est vide ou absente.
$ bash -c 'rm -rf "${REPERTOIRE_CACHE:?variable vide}/"'
bash: line 1: REPERTOIRE_CACHE: variable vide
Rien n'a été supprimé. Le cours Bash pour l'automatisation généralise ces protections (set -u, en particulier).
Les liens en pratique
Revenons dans le répertoire de configuration et créons un lien physique et un lien symbolique vers app.conf. L'option -i de ls affiche le numéro d'inode en première colonne :
$ ln app.conf dur.conf
$ ln -s app.conf symb.conf
$ ls -li
total 12
25455773 -rw-rw-r-- 2 camille camille 24 Oct 5 11:09 app.conf
25455771 drwxrwxr-x 2 camille camille 4096 Oct 5 11:09 archives
25455773 -rw-rw-r-- 2 camille camille 24 Oct 5 11:09 dur.conf
25455774 lrwxrwxrwx 1 camille camille 8 Oct 5 11:09 symb.conf -> app.conf
$ readlink symb.conf
app.conf
Lisez ces lignes attentivement :
app.confetdur.confont le même numéro d'inode (25455773) et un compteur de liens à 2 (la deuxième colonne de droits) : ce sont deux noms du même fichier.symb.confa son propre inode, le typelen tête des droits, une taille de 8 octets (la longueur de la chaîneapp.conf), etlsaffiche sa cible après la flèche.readlinklit le contenu du lien ;readlink -fle résout jusqu'au chemin absolu final.
Supprimez maintenant app.conf :
$ rm app.conf
$ ls -li
total 8
25455771 drwxrwxr-x 2 camille camille 4096 Oct 5 11:09 archives
25455773 -rw-rw-r-- 1 camille camille 24 Oct 5 11:09 dur.conf
25455774 lrwxrwxrwx 1 camille camille 8 Oct 5 11:09 symb.conf -> app.conf
$ cat dur.conf
port = 8000
workers = 2
$ cat symb.conf
cat: symb.conf: No such file or directory
L'inode 25455773 a perdu un nom, son compteur est passé à 1, et ses données sont intactes sous le nom dur.conf. Le lien symbolique, lui, contient toujours le chemin app.conf, qui ne mène plus nulle part : il est cassé. Le message d'erreur est trompeur, puisque symb.conf existe bel et bien ; c'est sa cible qui manque. Sur un terminal en couleurs, ls affiche d'ailleurs les liens cassés en rouge.
L'usage le plus courant des liens symboliques en exploitation est le lien de version :
/opt/signalements/
├── 1.1.0/
├── 1.2.0/
└── courant -> 1.2.0Le service démarre toujours /opt/signalements/courant. Pour déployer, on installe 1.3.0/ à côté, puis on bascule le lien ; pour revenir en arrière, on le rebascule. Pour que la bascule soit atomique, on crée le nouveau lien sous un nom temporaire puis on le renomme par-dessus l'ancien (ln -s 1.3.0 courant.tmp && mv -T courant.tmp courant) : c'est le rename(2) vu plus haut. L'option -T empêche mv de traiter courant comme un répertoire dans lequel déplacer le lien.
Le disque plein qui ne se vide pas
Reproduisons le cas du journal supprimé. On crée un fichier de 50 Mo, un processus l'ouvre (ici, un sleep à qui l'on donne le fichier en descripteur 3), puis on supprime le fichier :
$ head -c 50M /dev/zero > gros.log
$ (exec 3<gros.log; exec sleep 300) &
$ rm gros.log
$ ls gros.log
ls: cannot access 'gros.log': No such file or directory
$ lsof -nP +L1 -a -p $!
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
sleep 592020 camille 3r REG 259,2 52428800 0 25456004 /home/camille/essais/gros.log (deleted)
$!contient le numéro du dernier processus lancé en arrière-plan (leçon 10).lsof(list open files) liste les fichiers ouverts.+L1ne garde que ceux dont le nombre de liens est inférieur à 1, c'est-à-dire supprimés mais encore ouverts ;-a -prestreint au processus donné ;-nPévite les résolutions de noms, plus rapides.- La colonne
NLINKvaut 0, la taille est toujours de 52 428 800 octets, et le nom est suivi de(deleted).
On voit la même chose sans lsof, dans le répertoire /proc du processus, qui expose ses descripteurs ouverts :
$ ls -l /proc/$!/fd/3
lr-x------ 1 camille camille 64 Oct 5 11:09 /proc/592020/fd/3 -> /home/camille/essais/gros.log (deleted)
Les 50 Mo restent occupés tant que le processus vit. Terminez-le (kill $!), et l'espace est rendu.
Sur un vrai serveur, la commande de diagnostic est sudo lsof -nP +L1 (sans -p), qui liste tous les fichiers supprimés encore ouverts, avec le processus qui les tient. La correction n'est pas de supprimer encore, mais de faire fermer le fichier : redémarrer le service, ou lui demander de rouvrir ses journaux (beaucoup de démons le font sur réception d'un signal, leçon 10). Et pour vider un journal en cours d'utilisation sans le supprimer, on le tronque : sudo truncate -s 0 /var/log/application.log. L'inode reste le même, le processus continue d'écrire dedans, et l'espace est libéré immédiatement. C'est ce que fait l'option copytruncate de logrotate.
Mesurer la place : df et du
$ df -h .
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p2 937G 518G 372G 59% /
$ du -sh logs signalements
20K logs
32K signalements
$ du -h --max-depth=1 . | sort -h
df(disk free) interroge le système de fichiers : taille, espace utilisé, disponible.-haffiche des unités lisibles. Donnez-lui un chemin pour ne voir que le système de fichiers qui le contient.du(disk usage) additionne la taille des fichiers qu'il trouve en parcourant l'arborescence.-sdonne un total par argument ;--max-depth=1détaille un niveau de sous-répertoires, quesort -htrie dans l'ordre des tailles lisibles.
Quand df annonce bien plus d'espace utilisé que du n'en trouve, pensez immédiatement aux fichiers supprimés encore ouverts : du ne les voit pas, puisqu'ils n'ont plus de nom, mais df compte leurs blocs. Autre cause d'un disque « plein » alors qu'il reste de la place : l'épuisement des inodes, quand des millions de petits fichiers ont consommé tous les inodes disponibles. df -i affiche cette ressource à part, et le message d'erreur est alors le même : No space left on device.
Chercher et agir en masse : find
find parcourt une arborescence et sélectionne les fichiers selon des critères. Créons trois journaux datés de septembre et un fichier au nom contenant une espace :
$ mkdir logs
$ for j in 01 02 03; do
> echo ligne > logs/signalements-2026-09-$j.log
> touch -d "2026-09-$j 12:00" logs/signalements-2026-09-$j.log
> done
$ echo x > "logs/rapport final.txt"
$ find logs -name '*.log' -mtime +7
logs/signalements-2026-09-03.log
logs/signalements-2026-09-01.log
logs/signalements-2026-09-02.log
-name '*.log': le nom correspond au motif. Les apostrophes sont indispensables : sans elles, le shell remplacerait*.logpar les fichiers.logdu répertoire courant avant même quefindne démarre.-mtime +7: modifié il y a plus de 7 jours. La pagefind(1)précise que la partie fractionnaire est ignorée :-mtime +1ne retient que les fichiers modifiés il y a au moins deux jours.-mtime -7retient les moins de sept jours.-type fne garde que les fichiers ordinaires,-type dles répertoires,-type lles liens symboliques.-size +100M: plus de 100 Mio.
Attention à -size avec de petites valeurs. La taille est arrondie à l'unité supérieure avant la comparaison, ce que find(1) signale explicitement :
$ find logs -type f -size -1k | wc -l
0
$ find logs -type f -size -2k | wc -l
4
Nos fichiers font quelques octets, mais -size -1k ne trouve rien : chaque fichier non vide est arrondi à 1 kio, et « moins de 1 » ne retient que les fichiers vides. Pour une limite précise, comptez en octets (-size -1024c).
Agir sur le résultat. -exec lance une commande pour les fichiers trouvés, {} étant remplacé par leur nom. Il existe deux formes, et la différence se voit :
$ find logs -type f -exec echo traite {} \;
traite logs/rapport final.txt
traite logs/signalements-2026-09-03.log
traite logs/signalements-2026-09-01.log
traite logs/signalements-2026-09-02.log
$ find logs -type f -exec echo traite {} +
traite logs/rapport final.txt logs/signalements-2026-09-03.log logs/signalements-2026-09-01.log logs/signalements-2026-09-02.log
Avec \; (le point-virgule échappé pour que le shell ne l'interprète pas), la commande est lancée une fois par fichier. Avec +, elle reçoit autant de fichiers que possible à la fois, ce qui, sur des milliers de fichiers, fait la différence entre quelques secondes et plusieurs minutes. Dans les deux cas, chaque nom est passé comme un argument entier, espaces comprises : rapport final.txt n'a pas été coupé en deux.
-delete supprime les fichiers trouvés. Elle est pratique et dangereuse : find(1) rappelle qu'elle est une action évaluée à sa place dans l'expression, et que placer -delete en premier fait supprimer tout ce qui se trouve sous le point de départ, avant même que les critères suivants soient examinés. Méthode sûre : écrivez la commande sans -delete, relisez la liste, puis ajoutez -delete à la fin.
$ find /var/log/signalements -name '*.gz' -mtime +90 -print # d'abord, regarder
$ find /var/log/signalements -name '*.gz' -mtime +90 -delete # ensuite, supprimer
Les noms qui contiennent des espaces
La boucle suivante a l'air raisonnable, et elle est fausse :
$ for f in $(find logs -name '*.txt'); do ls "$f"; done
ls: cannot access 'logs/rapport': No such file or directory
ls: cannot access 'final.txt': No such file or directory
$(...) produit un texte, que le shell découpe sur les espaces avant de le donner à for : le nom logs/rapport final.txt devient deux mots. Les noms de fichiers sous Linux peuvent contenir des espaces, des tabulations et même des retours à la ligne ; le seul caractère interdit, avec la barre oblique, est l'octet nul. D'où la forme robuste, où find sépare les noms par cet octet nul (-print0) et où xargs les lit de la même façon (-0) :
$ find logs -name '*.txt' -print0 | xargs -0 ls -l
-rw-rw-r-- 1 camille camille 2 Oct 5 11:09 logs/rapport final.txt
find(1) décrit -print0 exactement pour ce cas : permettre aux programmes qui lisent la sortie de find d'interpréter correctement des noms qui contiennent des espaces ou des retours à la ligne. Dans un terminal, pour taper un tel nom, entourez-le d'apostrophes ou de guillemets ("rapport final.txt"), ou échappez l'espace (rapport\ final.txt) ; la complétion par la touche Tab le fait pour vous. Et un nom qui commence par un tiret (-f) se traite après --, qui signale la fin des options : rm -- -f.
Les archives : tar
tar (tape archive, le format date des bandes magnétiques) rassemble une arborescence dans un seul fichier, en conservant noms, droits, propriétaires et dates. Il ne compresse pas lui-même : il passe l'archive à un compresseur, gzip avec l'option -z.
$ tar czf archive-logs.tar.gz logs
$ tar tzvf archive-logs.tar.gz
drwxrwxr-x camille/camille 0 2026-10-05 11:09 logs/
-rw-rw-r-- camille/camille 2 2026-10-05 11:09 logs/rapport final.txt
-rw-rw-r-- camille/camille 6 2026-09-03 12:00 logs/signalements-2026-09-03.log
-rw-rw-r-- camille/camille 6 2026-09-01 12:00 logs/signalements-2026-09-01.log
-rw-rw-r-- camille/camille 6 2026-09-02 12:00 logs/signalements-2026-09-02.log
Les lettres se lisent comme une phrase :
- une lettre d'action :
c(create, créer),t(list, lister le contenu),x(extract, extraire) ; z: passer pargzip(jpourbzip2,Jpourxz,--zstdpourzstd) ; en lecture, GNU tar reconnaît d'ailleurs seul la compression ;v: afficher chaque fichier traité ;fsuivi du nom de l'archive : toujours en dernier dans le groupe, puisque l'argument suivant est son nom.
L'archive a gardé les dates de septembre : tar conserve les métadonnées. Pour extraire ailleurs que dans le répertoire courant, -C : tar xzf archive-logs.tar.gz -C /srv/restauration.
Extraire une archive dont on ne connaît pas l'origine
Une archive reçue d'un tiers peut contenir des noms dangereux : un chemin absolu (/etc/cron.d/tache), ou un chemin qui remonte (../../.bashrc). Extraite naïvement, elle écrirait hors du répertoire prévu. En 2018, l'équipe de sécurité de Snyk a recensé ce défaut, baptisé Zip Slip, dans des milliers de projets qui extrayaient des archives (zip, tar, jar et d'autres) sans vérifier les chemins : l'écrasement de fichiers arbitraires qui en résulte mène souvent à l'exécution de code.
GNU tar se protège par défaut, et l'on peut le voir. Une archive créée avec un chemin absolu perd sa barre oblique initiale, à la création comme à l'extraction :
$ tar cPf abs.tar "/home/camille/essais/logs/rapport final.txt"
$ mkdir ex2 && cd ex2 && tar xf ../abs.tar
tar: Removing leading `/' from member names
$ find . -type f
./home/camille/essais/logs/rapport final.txt
L'option -P (--absolute-names) a forcé l'enregistrement du chemin absolu, mais à l'extraction, sans -P, tar a retiré le / initial et créé le fichier sous le répertoire courant. Et une archive dont un membre contient .. est refusée. Pour le vérifier, fabriquez-en une : l'option --transform de tar renomme les membres à la création, ce qui suffit à y glisser un chemin qui remonte d'un niveau.
$ cd .. && echo essai > evil
$ tar cf piege.tar --transform='s#^evil#../evil-sorti#' evil
tar: Removing leading `../' from member names
$ cd ex2
$ tar tf ../piege.tar
../evil-sorti
$ tar xf ../piege.tar
tar: Removing leading `../' from member names
tar: ../evil-sorti: Member name contains '..'
tar: Exiting with failure status due to previous errors
$ echo $?
2
Ces protections ne dispensent pas de bonnes habitudes, d'autant que d'autres outils d'extraction n'ont pas les mêmes :
- Listez avant d'extraire (
tar tvf), et regardez les chemins. - Extrayez dans un répertoire vide créé pour l'occasion, jamais directement dans
/,/etcou votre répertoire personnel. - N'extrayez pas en
rootune archive d'origine inconnue : enroot, tar restaure aussi les propriétaires et les droits enregistrés, ce qui peut créer des fichiers appartenant à n'importe qui. - N'utilisez jamais
-Pà l'extraction d'une archive que vous n'avez pas créée.
Sous le capot
Chaque commande de cette leçon est une mince couche au-dessus de quelques appels système. Les connaître permet de prévoir leur comportement :
| Commande | Appels système principaux | Conséquence |
|---|---|---|
mkdir | mkdir(2) | Échoue si le parent n'existe pas ; -p boucle sur chaque niveau |
touch | openat(2) avec création, utimensat(2) | Crée ou met à jour les dates |
ln | link(2) | Ajoute une entrée de répertoire vers un inode existant |
ln -s | symlink(2) | Crée un nouvel inode de type lien, qui contient un chemin |
mv | rename(2), sinon copie puis unlink(2) | Atomique sur un même système de fichiers ; copie sinon |
rm | unlink(2) ; unlinkat(2) et rmdir(2) en récursif | Retire un nom ; les données partent quand les liens et les ouvertures tombent à zéro |
cp | open, read/write (ou copie accélérée par le noyau), fchmod, fchown, utimensat selon les options | Une copie est un nouvel inode : rien ne suit automatiquement l'original |
Le compteur que le noyau consulte avant de libérer un inode a donc deux composantes : le nombre de noms (le compteur de liens, visible dans ls -l) et le nombre de descripteurs ouverts (visible par lsof). Tant que l'un des deux n'est pas nul, les données restent. C'est pour la même raison qu'un programme peut être mis à jour pendant qu'il tourne : le gestionnaire de paquets remplace le fichier (un nouvel inode prend le nom), et le processus en cours garde l'ancien inode ouvert jusqu'à son redémarrage (leçon 12).
Les droits nécessaires suivent la même logique, et surprennent souvent : supprimer ou renommer un fichier modifie son répertoire, pas le fichier. C'est donc le droit d'écriture sur le répertoire qui compte, et non sur le fichier lui-même. La leçon 9 y revient, avec l'exception du sticky bit de /tmp.
Pièges courants
cp et mv écrasent sans prévenir. Ni l'un ni l'autre ne demande confirmation par défaut. Utilisez -i à la main, -n ou --backup dans les scripts. Certaines distributions définissent des alias cp='cp -i' pour root ; ne comptez pas dessus, ils n'existent pas partout et pas dans les scripts.
cp -r au lieu de cp -a pour une sauvegarde. La copie récursive simple change propriétaire, droits et dates selon l'utilisateur qui copie, et suit les liens symboliques. Restaurée, elle peut empêcher un service de démarrer parce que ses fichiers n'appartiennent plus au bon utilisateur.
La barre oblique finale qui change le sens. cp -a source/ dest et cp -a source dest ne font pas la même chose si dest existe déjà : dans le second cas, on obtient dest/source/. Vérifiez le résultat avec ls ; l'outil rsync, qu'on rencontre plus loin dans le parcours, a des règles encore plus subtiles sur ce point.
Un motif qui ne correspond à rien. Si aucun fichier ne correspond à *.gz, Bash laisse le motif tel quel, et rm *.gz cherche un fichier nommé littéralement *.gz : rm: cannot remove '*.gz': No such file or directory. C'est inoffensif ici, mais un script qui enchaîne sur ce résultat peut se tromper.
rm d'un journal ouvert. Voir la pratique : l'espace ne revient pas. Diagnostic par lsof +L1, correction par redémarrage ou truncate.
Un lien symbolique relatif déplacé. Un lien courant -> 1.2.0 déplacé dans un autre répertoire cherchera 1.2.0 à côté de sa nouvelle position. Pour un lien qui doit pouvoir bouger, utilisez un chemin absolu ; pour un ensemble qui bouge en bloc (une application et ses versions), un chemin relatif est au contraire préférable.
find ... -delete mal placé, ou find sans apostrophes. find . -delete -name '*.log' supprime tout. find . -name *.log fonctionne quand le répertoire courant ne contient aucun .log, et échoue (ou cherche autre chose) dès qu'il en contient un.
No space left on device alors que df -h montre de la place. Regardez df -i : les inodes sont peut-être épuisés, typiquement par un répertoire de cache ou de sessions qui a accumulé des millions de petits fichiers.
Sécurité
- Un
rm -rfdans un script est une arme chargée. Protégez chaque variable d'un chemin par${VAR:?}, préférez des chemins absolus et explicites, et faites relire les scripts qui suppriment. Une commande qui supprime en masse se teste d'abord en remplaçant l'action parechoouls. - Supprimer ne détruit pas les données.
rmretire un nom ; les blocs restent sur le disque jusqu'à leur réutilisation, et des outils de récupération peuvent les lire. Pour des données sensibles, la réponse fiable est le chiffrement du disque : sur un SSD ou un volume cloud, l'effacement par réécriture (shred) n'offre aucune garantie, parce que le contrôleur décide où il écrit vraiment. - Les copies de sauvegarde héritent des données sensibles.
app.conf.2026-10-05contient le même mot de passe queapp.conf.cp -aconserve heureusement ses droits restreints ; une copie faite aveccpsimple, par un autre utilisateur, ou vers/tmp, peut les perdre. - Les archives traversent les frontières. Une archive de journaux envoyée à un client contient peut-être des adresses IP, des identifiants de session ou des données personnelles : relisez son contenu (
tar tvf) et ce que contiennent les fichiers avant de l'envoyer. Dans l'autre sens, une archive reçue s'extrait comme décrit plus haut. - Les liens symboliques peuvent tromper un programme privilégié. Un attaquant qui peut créer un lien dans un répertoire où écrit un programme
rootpeut lui faire écrire ailleurs. C'est l'une des raisons pour lesquelles/tmpa des protections particulières (leçon 9) et pour lesquelles les programmes sérieux créent leurs fichiers temporaires avecmktemp.
En production
- On ne fait pas de ménage à la main sur un serveur. Les journaux sont tournés et compressés par
logrotateou par journald (leçon 11), les fichiers temporaires nettoyés parsystemd-tmpfiles. Un disque qui se remplit est un symptôme : trouvez ce qui écrit (du -h --max-depth=1 / -x | sort -h, enroot,-xpour rester sur le même système de fichiers) et corrigez la cause, puis ajoutez une alerte avant 80 %. - Les déploiements utilisent des répertoires de version et un lien symbolique, ou des images de conteneur : jamais d'écrasement de fichiers en place pendant que le service tourne.
- Les sauvegardes ne se font pas avec
cp.cp -aest l'outil de la copie de précaution avant une modification ; une politique de sauvegarde demande des copies hors du serveur, versionnées et testées. - Chez Lyneko, les applications tournent dans des conteneurs sur Kubernetes : on y modifie rarement un fichier sur un nœud. Mais les réflexes de cette leçon servent dès qu'on intervient sur une machine, qu'il s'agisse d'un nœud dont le disque se remplit, d'un serveur de client ou d'un volume monté dans un pod.
Exercices
1. Inodes et liens (niveau 100). Dans votre répertoire d'essai, créez un fichier version.txt contenant 1.2.0, un lien physique v-dur.txt et un lien symbolique v-symb.txt vers lui. Prévoyez, avant de lancer chaque commande, ce que vous verrez : (a) ls -li ; (b) après echo 1.3.0 > version.txt, le contenu des deux liens ; (c) après mv version.txt ancienne.txt, le contenu des deux liens. Vérifiez.
Solution
(a) version.txt et v-dur.txt ont le même inode et un compteur de liens à 2 ; v-symb.txt a son propre inode, le type l et la flèche -> version.txt. (b) Les deux liens affichent 1.3.0 : > a réécrit le contenu du même inode (même s'il le vide d'abord), et le lien symbolique y mène par le nom. (c) v-dur.txt affiche toujours 1.3.0 (c'est le même inode, qui a simplement un autre second nom) ; v-symb.txt est cassé, puisque le nom version.txt n'existe plus : cat répond No such file or directory.
2. L'espace fantôme (niveau 100). Un serveur affiche df -h / à 97 %, mais sudo du -sh /var/log ne trouve que 300 Mo, alors que la veille, un collègue a supprimé un journal de 12 Go dans /var/log/signalements/. Que se passe-t-il, comment le confirmez-vous, et comment récupérez-vous l'espace sans perdre de journaux à l'avenir ?
Solution
Le fichier supprimé est encore ouvert par le processus qui y écrivait : son nom a disparu (donc du ne le voit plus), mais l'inode et ses blocs sont toujours là (donc df les compte). Confirmation : sudo lsof -nP +L1, qui montre le processus, le descripteur, la taille et la mention (deleted). Récupération : redémarrer le service, ou lui faire rouvrir ses journaux s'il sait le faire. En attendant, sudo truncate -s 0 /proc/<pid>/fd/<n> vide le fichier sans redémarrage. Pour l'avenir : confier la rotation à logrotate (avec copytruncate si l'application ne sait pas rouvrir ses fichiers) ou à journald, et ne jamais supprimer un journal actif.
3. Ménage sûr (niveau 100). Écrivez la commande qui supprime, dans /var/log/signalements/, les archives .gz de plus de 90 jours, y compris celles dont le nom contient des espaces. Donnez la démarche en deux temps, et expliquez pourquoi rm $(find ...) serait une mauvaise idée.
Solution
D'abord regarder : sudo find /var/log/signalements -type f -name '*.gz' -mtime +90 -print. Puis, la liste relue, remplacer -print par -delete, placé en dernier. Une variante équivalente : -exec rm -- {} +. rm $(find ...) découpe la sortie de find sur les espaces (un fichier export final.gz deviendrait deux arguments, dont l'un pourrait correspondre à un autre fichier), et dépasse la longueur maximale d'une ligne de commande s'il y a beaucoup de fichiers. -delete et -exec ... + passent chaque nom intact.
4. L'archive suspecte (niveau 100). Un client vous envoie export.tar.gz contenant, dit-il, « les photos des signalements ». Décrivez précisément vos commandes pour l'examiner puis l'extraire sans risque.
Solution
- Lister sans extraire :
tar tzvf export.tar.gz | less, en cherchant des chemins absolus, des.., des liens symboliques (typel, avec->), des fichiers exécutables ou des propriétaires inattendus. 2. Créer un répertoire vide dédié :mkdir ~/import-client && cd ~/import-client. 3. Extraire sanssudoet sans-P:tar xzf ../export.tar.gz. GNU tar retire alors les/initiaux et refuse les membres qui contiennent... 4. Vérifier le résultat (find . -type l,find . -type f | head) avant de déplacer les fichiers vers leur destination.
Récapitulatif
- Un fichier est un inode (métadonnées et données) ; les noms vivent dans les répertoires.
statetls -imontrent l'inode. cpetmvécrasent sans prévenir ;-i,-net--backupprotègent. Pour une copie de précaution :cp -a fichier fichier.$(date +%F).mvsur un même système de fichiers est unrename(2)atomique ; ailleurs, c'est une copie suivie d'une suppression.rmretire un nom. Pas de corbeille.lsavantrm, et${VAR:?}dans les chemins des scripts.--preserve-rootne protège que/.- Un lien physique est un nom de plus pour le même inode ; un lien symbolique est un fichier qui contient un chemin, et peut être cassé.
- Un fichier supprimé mais encore ouvert occupe toujours le disque :
lsof +L1le trouve, le redémarrage du processus outruncatelibère l'espace.dfcompte ces blocs,dunon. findsélectionne (-name,-type,-mtime,-size), agit (-exec ... +,-deleteen dernier) et gère les noms difficiles avec-print0 | xargs -0.tar czf,tar tzvf,tar xzf -C; une archive inconnue se liste d'abord et s'extrait dans un répertoire vide, sansroot.
Pour aller plus loin
- Les pages
unlink(2),rename(2)etsymlink(7)du projet Linux man-pages : quelques paragraphes chacune, qui expliquent tous les comportements de cette leçon. - Le manuel de GNU Coreutils, pour la liste complète des options de
cp(--reflink,--sparse) et derm. - La page
find(1), en particulier ses exemples et la section sur les actions, et le chapitre Manipulating Files de The Linux Command Line de William Shotts. - La leçon suivante, qui ouvre ces fichiers : les lire, les filtrer et les modifier sans casser la configuration de Signalements.
Sources
- GNU Coreutils, manuel (cp, mv, rm, ln, mkdir, du, df)
- Linux man-pages, unlink(2)
- Linux man-pages, rename(2)
- Linux man-pages, inode(7)
- Linux man-pages, symlink(7)
- Linux man-pages, find(1)
- Linux man-pages, tar(1)
- William Shotts, The Linux Command Line : Manipulating Files
- Snyk, Zip Slip Vulnerability (juin 2018)