🔝 Retour au Sommaire
La section 19.1 a présenté les bases de fs::path : construction, décomposition et concaténation. Cette section approfondit la manipulation des chemins avec les techniques avancées nécessaires en programmation système : normalisation et résolution, transformation entre chemins absolus et relatifs, modification dynamique des composantes, conversion entre représentations, et les pièges subtils que tout développeur rencontre sur Linux.
La maîtrise de fs::path est un prérequis pour écrire du code filesystem robuste. Un chemin mal construit, mal normalisé ou mal résolu est la source d'erreurs silencieuses qui ne se manifestent qu'en production : fichier écrit au mauvais endroit, lien symbolique suivi de manière inattendue, chemin qui fonctionne dans le répertoire de build mais pas après installation.
Avant de manipuler les chemins programmatiquement, il faut comprendre comment fs::path les décompose. Un chemin Linux est constitué de composantes séparées par /. La classe path identifie les parties suivantes :
/home/user/projet/build/app.tar.gz
│ │
├── root_name() : "" │ (vide sur Linux, "C:" sur Windows)
├── root_directory() : "/" │
├── root_path() : "/" │ (root_name + root_directory)
├── relative_path() : "home/user/projet/build/app.tar.gz"
├── parent_path() : "/home/user/projet/build"
├── filename() : "app.tar.gz"
│ ├── stem() : "app.tar"
│ └── extension() : ".gz"
Le chemin est aussi itérable composante par composante :
#include <filesystem>
#include <print>
namespace fs = std::filesystem;
int main() {
fs::path p = "/home/user/projet/src/main.cpp";
std::println("Composantes de {} :", p.string());
for (const auto& composante : p) {
std::println(" [{}]", composante.string());
}
}Sortie :
Composantes de /home/user/projet/src/main.cpp :
[/]
[home]
[user]
[projet]
[src]
[main.cpp]
Cette itération produit des fs::path pour chaque segment. Le / initial apparaît comme une composante à part entière, ce qui permet de distinguer un chemin absolu d'un chemin relatif.
#include <filesystem>
#include <print>
namespace fs = std::filesystem;
int main() {
fs::path absolu = "/var/log/syslog";
fs::path relatif = "build/release/app";
fs::path relatif_dot = "./config/settings.yaml";
std::println("{} est absolu : {}", absolu.string(), absolu.is_absolute());
std::println("{} est relatif : {}", relatif.string(), relatif.is_relative());
std::println("{} est relatif : {}", relatif_dot.string(), relatif_dot.is_relative());
}Sur Linux, un chemin est absolu s'il commence par /, relatif dans tous les autres cas. Le préfixe ./ ne rend pas un chemin absolu : il est relatif au répertoire courant, mais il reste syntaxiquement relatif.
#include <filesystem>
#include <print>
namespace fs = std::filesystem;
int main() {
fs::path relatif = "src/main.cpp";
// absolute() préfixe avec le répertoire courant sans normaliser
fs::path abs = fs::absolute(relatif);
std::println("absolute() : {}", abs.string());
// Ex : /home/user/projet/src/main.cpp
// canonical() préfixe, normalise ET résout les symlinks
// Le chemin DOIT exister
try {
fs::path canon = fs::canonical(relatif);
std::println("canonical() : {}", canon.string());
} catch (const fs::filesystem_error& e) {
std::println("canonical() a échoué : {}", e.what());
}
// weakly_canonical() : résout ce qui existe, normalise le reste
// Le chemin n'a PAS besoin d'exister entièrement
fs::path wcanon = fs::weakly_canonical("src/../build/output.bin");
std::println("weakly_canonical() : {}", wcanon.string());
}La distinction entre ces trois fonctions est cruciale :
| Fonction | Accès disque | Chemin doit exister | Résout les symlinks | Normalise . et .. |
|---|---|---|---|---|
absolute() |
Oui (lit CWD) | Non | Non | Non |
canonical() |
Oui | Oui (intégralement) | Oui | Oui |
weakly_canonical() |
Oui | Non | Partiellement | Oui |
En pratique, weakly_canonical() est le meilleur compromis pour la plupart des situations : il produit un chemin propre sans exiger que chaque composante existe déjà sur le disque. C'est particulièrement utile pour calculer un chemin de destination avant de créer le fichier.
La normalisation est un sujet sur lequel les erreurs sont fréquentes car il existe deux types de normalisation très différents.
lexically_normal() transforme le chemin sans aucun accès au disque. Elle élimine les ., résout les .. de manière purement textuelle et supprime les séparateurs redondants :
#include <filesystem>
#include <print>
namespace fs = std::filesystem;
int main() {
// Redondances et composantes superflues
fs::path p1 = "/home/user/./projet/../projet/src//main.cpp";
std::println("{}", p1.lexically_normal().string());
// /home/user/projet/src/main.cpp
// Trailing slash
fs::path p2 = "/var/log/";
std::println("{}", p2.lexically_normal().string());
// /var/log/ (le trailing slash est préservé, indiquant un répertoire)
// Remontées au-delà de la racine : tronquées
fs::path p3 = "/home/../../etc/passwd";
std::println("{}", p3.lexically_normal().string());
// /etc/passwd
// Sur un chemin relatif, les .. initiaux sont préservés
fs::path p4 = "../../src/main.cpp";
std::println("{}", p4.lexically_normal().string());
// ../../src/main.cpp
}La normalisation syntaxique ne connaît pas les liens symboliques. C'est une source de bugs subtils :
Filesystem réel :
/home/user/current_project -> /opt/projects/alpha (lien symbolique)
/opt/projects/alpha/src/main.cpp (fichier réel)
namespace fs = std::filesystem;
fs::path p = "/home/user/current_project/src/../config/app.yaml";
// Normalisation syntaxique : résout .. textuellement
auto syntaxique = p.lexically_normal();
// Résultat : /home/user/current_project/config/app.yaml
// Ce chemin est FAUX si current_project est un symlink :
// le vrai parent de src/ est /opt/projects/alpha/, pas /home/user/current_project/
// Normalisation physique : résout d'abord les symlinks
auto physique = fs::canonical(p);
// Résultat : /opt/projects/alpha/config/app.yaml
// (en supposant que ce fichier existe)La règle à retenir : utilisez lexically_normal() quand vous travaillez sur des chemins abstraits (affichage, stockage en configuration, comparaison syntaxique). Utilisez canonical() ou weakly_canonical() quand vous devez accéder à un fichier réel et que des liens symboliques peuvent être présents dans le chemin.
#include <filesystem>
#include <print>
namespace fs = std::filesystem;
int main() {
fs::path fichier = "/home/user/projet/src/main.cpp";
fs::path base = "/home/user/projet";
auto rel = fichier.lexically_relative(base);
std::println("{}", rel.string());
// src/main.cpp
// Dans l'autre sens
auto rel2 = base.lexically_relative(fichier);
std::println("{}", rel2.string());
// ../..
// Chemins sans relation (racines différentes sur Windows — rare sur Linux)
fs::path a = "/home/user/docs";
fs::path b = "/var/log";
auto rel3 = a.lexically_relative(b);
std::println("{}", rel3.string());
// ../../home/user/docs
}lexically_proximate() fonctionne comme lexically_relative(), mais retourne le chemin original au lieu d'un path vide si la relation relative ne peut pas être calculée :
#include <filesystem>
#include <print>
namespace fs = std::filesystem;
int main() {
fs::path fichier = "/home/user/projet/src/main.cpp";
fs::path base = "/home/user/projet";
// relative() peut retourner un path vide dans certains cas limites
auto rel = fichier.lexically_relative(base);
std::println("relative : '{}' (vide = {})", rel.string(), rel.empty());
// proximate() retourne le chemin original si relative échoue
auto prox = fichier.lexically_proximate(base);
std::println("proximate : '{}'", prox.string());
}Les fonctions libres fs::relative() et fs::proximate() sont les équivalents physiques (avec accès disque) des méthodes lexicales. Elles résolvent d'abord les chemins via weakly_canonical() avant de calculer la relation :
#include <filesystem>
#include <print>
namespace fs = std::filesystem;
int main() {
// Suppose que /home/user/link -> /opt/real_project
fs::path fichier = "/home/user/link/src/main.cpp";
fs::path base = "/opt/real_project";
// Lexical : ne voit pas le symlink, calcule une relation incorrecte
auto lex = fichier.lexically_relative(base);
std::println("Lexical : {}", lex.string());
// ../../../home/user/link/src/main.cpp (incorrect !)
// Physique : résout le symlink d'abord
auto phys = fs::relative(fichier, base);
std::println("Physique : {}", phys.string());
// src/main.cpp (correct)
}fs::path offre des méthodes pour modifier les composantes d'un chemin en place. Ces méthodes sont essentielles pour construire des chemins dérivés (fichier de sortie à partir d'un fichier d'entrée, fichier de backup, etc.).
Remplace le dernier segment du chemin (le nom de fichier) :
#include <filesystem>
#include <print>
namespace fs = std::filesystem;
int main() {
fs::path source = "/home/user/projet/src/main.cpp";
// Remplacer le nom de fichier
fs::path header = source;
header.replace_filename("main.h");
std::println("{}", header.string());
// /home/user/projet/src/main.h
// Fonctionne aussi pour construire un chemin frère
fs::path sibling = source;
sibling.replace_filename("utils.cpp");
std::println("{}", sibling.string());
// /home/user/projet/src/utils.cpp
}Remplace uniquement l'extension (la partie après le dernier . du filename) :
#include <filesystem>
#include <print>
namespace fs = std::filesystem;
int main() {
fs::path source = "/home/user/projet/src/main.cpp";
// .cpp → .o (fichier objet)
fs::path objet = source;
objet.replace_extension(".o");
std::println("{}", objet.string());
// /home/user/projet/src/main.o
// Supprimer l'extension (passer une chaîne vide)
fs::path sans_ext = source;
sans_ext.replace_extension("");
std::println("{}", sans_ext.string());
// /home/user/projet/src/main
// Ajouter une extension (le point est optionnel)
fs::path backup = source;
backup.replace_extension(".cpp.bak");
std::println("{}", backup.string());
// /home/user/projet/src/main.cpp.bak
// Attention : seule la dernière extension est remplacée
fs::path archive = "/tmp/data.tar.gz";
fs::path changed = archive;
changed.replace_extension(".xz");
std::println("{}", changed.string());
// /tmp/data.tar.xz (le .gz est remplacé, .tar est dans le stem)
}Supprime le dernier segment, laissant le chemin parent avec son séparateur final :
#include <filesystem>
#include <print>
namespace fs = std::filesystem;
int main() {
fs::path p = "/home/user/projet/src/main.cpp";
p.remove_filename();
std::println("Après remove_filename : '{}'", p.string());
// /home/user/projet/src/
// Différence avec parent_path() :
fs::path q = "/home/user/projet/src/main.cpp";
std::println("parent_path() : '{}'", q.parent_path().string());
// /home/user/projet/src (pas de slash final, retourne un nouveau path)
}La différence est subtile mais importante : remove_filename() modifie le path en place et laisse un trailing slash, tandis que parent_path() retourne un nouveau path sans trailing slash. Pour la construction de chemins dérivés, parent_path() / "nouveau_fichier" est généralement plus lisible.
Les opérateurs de comparaison de fs::path effectuent une comparaison lexicographique composante par composante, pas une comparaison de chaînes brutes :
#include <filesystem>
#include <print>
namespace fs = std::filesystem;
int main() {
fs::path a = "/home/user/projet";
fs::path b = "/home/user/projet";
fs::path c = "/home/user/projet/"; // Trailing slash
fs::path d = "/home/user/./projet"; // Composante . superflue
std::println("a == b : {}", a == b); // true
std::println("a == c : {}", a == c); // false ! Le trailing slash diffère
std::println("a == d : {}", a == d); // false ! Le . est une composante distincte
// Pour une comparaison correcte, normaliser d'abord
std::println("a == c (normalisé) : {}",
a.lexically_normal() == c.lexically_normal());
// Attention : peut encore être false selon l'implémentation du trailing slash
}Les opérateurs == et < comparent les composantes dans l'ordre. Cela signifie que deux chemins syntaxiquement différents mais pointant vers le même fichier (/home/user/projet et /home/user/./projet) ne seront pas égaux. Si vous devez vérifier que deux chemins désignent le même fichier, il faut les normaliser ou les résoudre.
Pour savoir si deux chemins pointent vers le même fichier ou répertoire sur le disque (même inode), indépendamment de leur représentation syntaxique :
#include <filesystem>
#include <print>
namespace fs = std::filesystem;
int main() {
// Suppose que /home/user/link -> /home/user/projet (symlink)
fs::path a = "/home/user/projet/src/main.cpp";
fs::path b = "/home/user/link/src/main.cpp";
fs::path c = "/home/user/projet/./src/../src/main.cpp";
// Les DEUX chemins doivent exister, sinon exception
try {
std::println("a ≡ b : {}", fs::equivalent(a, b)); // true (même inode)
std::println("a ≡ c : {}", fs::equivalent(a, c)); // true (même inode)
} catch (const fs::filesystem_error& e) {
std::println("Erreur : {}", e.what());
}
}fs::equivalent() effectue un stat() sur chaque chemin et compare les paires (device, inode). C'est la seule méthode fiable pour déterminer si deux chemins référencent le même objet du filesystem. Elle échoue si l'un des deux chemins n'existe pas.
fs::path stocke le chemin dans un format natif et offre plusieurs méthodes de conversion :
#include <filesystem>
#include <print>
#include <string>
#include <string_view>
namespace fs = std::filesystem;
int main() {
fs::path p = "/home/user/données/rapport.txt";
// Forme native (std::string sur Linux)
std::string natif = p.string();
// Forme native retournée par référence (pas de copie)
const auto& natif_ref = p.native();
// Sur Linux : const std::string&
// Conversion en c_str pour les API C
const char* cstr = p.c_str();
// Conversion implicite en string_view (C++17)
// Non disponible directement, mais :
std::string_view sv(p.native());
std::println("string() : {}", natif);
std::println("c_str() : {}", cstr);
}Sur Linux, path::value_type est char et path::string_type est std::string. Les méthodes string() et native() retournent donc le même type. Sur Windows, value_type est wchar_t, et des méthodes supplémentaires (wstring(), u8string(), u16string(), u32string()) permettent les conversions entre encodages. Dans le cadre de cette formation centrée sur Ubuntu, string() et c_str() couvrent la quasi-totalité des besoins.
Les appels système POSIX attendent des const char*. L'interopérabilité est directe :
#include <filesystem>
#include <fcntl.h>
#include <unistd.h>
#include <print>
namespace fs = std::filesystem;
int main() {
fs::path chemin = "/tmp" / fs::path("test.txt");
// Utilisation directe avec les API POSIX via c_str()
int fd = open(chemin.c_str(), O_RDONLY);
if (fd >= 0) {
std::println("Fichier ouvert : {}", chemin.string());
close(fd);
}
}Un cas d'usage très fréquent en programmation système — construire des chemins à partir de variables d'environnement :
#include <filesystem>
#include <print>
#include <cstdlib>
namespace fs = std::filesystem;
auto get_config_path() -> fs::path {
// $XDG_CONFIG_HOME ou fallback vers ~/.config
const char* xdg = std::getenv("XDG_CONFIG_HOME");
if (xdg && xdg[0] != '\0') {
return fs::path(xdg) / "mon_app";
}
const char* home = std::getenv("HOME");
if (home && home[0] != '\0') {
return fs::path(home) / ".config" / "mon_app";
}
// Dernier recours
return "/tmp/mon_app";
}
auto get_cache_path() -> fs::path {
const char* xdg = std::getenv("XDG_CACHE_HOME");
if (xdg && xdg[0] != '\0') {
return fs::path(xdg) / "mon_app";
}
const char* home = std::getenv("HOME");
if (home && home[0] != '\0') {
return fs::path(home) / ".cache" / "mon_app";
}
return fs::temp_directory_path() / "mon_app";
}
int main() {
std::println("Config : {}", get_config_path().string());
std::println("Cache : {}", get_cache_path().string());
}Ce pattern respecte la spécification XDG Base Directory qui est le standard de fait sur les systèmes Linux modernes pour l'emplacement des fichiers de configuration, cache et données utilisateur.
La fonction fs::temp_directory_path() retourne le répertoire temporaire du système, déterminé en consultant dans l'ordre les variables d'environnement TMPDIR, TMP, TEMP, puis le fallback /tmp :
#include <filesystem>
#include <print>
namespace fs = std::filesystem;
int main() {
fs::path tmp = fs::temp_directory_path();
std::println("Répertoire temporaire : {}", tmp.string());
// Typiquement : /tmp
// Construction d'un chemin temporaire unique
// (pas de génération automatique de nom — voir section 19.1.3)
fs::path work_dir = tmp / "mon_app_build_cache";
}Notez que temp_directory_path() retourne le répertoire temporaire, mais ne crée pas de fichier ou répertoire unique. Pour créer des fichiers temporaires avec un nom unique, il faut soit utiliser l'API POSIX mkstemp() (section 19.2), soit construire un nom basé sur le PID ou un identifiant unique.
Un fs::path par défaut est vide. Passer un path vide à la plupart des fonctions filesystem produit une erreur :
namespace fs = std::filesystem;
fs::path p; // Path vide
// Vérifiez toujours avant d'utiliser
if (p.empty()) {
std::println("Chemin non défini");
}
// fs::exists("") retourne false (pas d'exception)
// fs::canonical("") lance une exceptionSur Linux, les fichiers commençant par . sont cachés par convention. fs::path les traite comme n'importe quel fichier, mais stem() et extension() ont un comportement qui peut surprendre :
#include <filesystem>
#include <print>
namespace fs = std::filesystem;
int main() {
fs::path dotfile = "/home/user/.bashrc";
std::println("filename() : {}", dotfile.filename().string()); // .bashrc
std::println("stem() : {}", dotfile.stem().string()); // .bashrc
std::println("extension() : {}", dotfile.extension().string()); // "" (vide !)
fs::path dotconf = "/home/user/.config";
std::println("stem() : {}", dotconf.stem().string()); // .config
std::println("extension() : {}", dotconf.extension().string()); // "" (vide !)
// Comparer avec un fichier qui a une vraie extension
fs::path hidden = "/home/user/.app.log";
std::println("stem() : {}", hidden.stem().string()); // .app
std::println("extension() : {}", hidden.extension().string()); // .log
}Le point initial d'un dotfile n'est pas considéré comme un séparateur d'extension. .bashrc a un stem de .bashrc et une extension vide. C'est le comportement correct selon POSIX, mais il surprend souvent les développeurs habitués à d'autres langages.
La présence ou l'absence d'un / final modifie le comportement de certaines méthodes :
#include <filesystem>
#include <print>
namespace fs = std::filesystem;
int main() {
fs::path a = "/home/user/projet";
fs::path b = "/home/user/projet/";
std::println("a.filename() : '{}'", a.filename().string()); // projet
std::println("b.filename() : '{}'", b.filename().string()); // "" (vide !)
std::println("a.parent_path() : '{}'", a.parent_path().string());
// /home/user
std::println("b.parent_path() : '{}'", b.parent_path().string());
// /home/user/projet
}Un trailing slash fait que filename() retourne un path vide et que parent_path() retourne le chemin entier sans le slash. Ce comportement est logique (le slash indique "le contenu de ce répertoire"), mais il peut casser du code qui s'attend à ce que filename() retourne toujours quelque chose. Utilisez lexically_normal() pour supprimer les trailing slashes ambigus avant d'appeler ces méthodes si l'entrée n'est pas sous votre contrôle.
Sur les systèmes Linux modernes, les noms de fichiers sont des séquences d'octets (tout sauf \0 et /). La convention est UTF-8, mais le filesystem ne l'impose pas. fs::path sur Linux stocke les chemins tels quels, sans conversion d'encodage :
#include <filesystem>
#include <print>
namespace fs = std::filesystem;
int main() {
// Les caractères accentués fonctionnent si le système est en UTF-8
fs::path p = "/home/user/données/résumé.txt";
std::println("{}", p.string());
// Espaces dans les chemins : aucun problème avec fs::path
// (contrairement aux scripts shell où il faut échapper)
fs::path espaces = "/home/user/Mon Projet/mes fichiers/doc.pdf";
std::println("Existe : {}", fs::exists(espaces));
}Les espaces et caractères spéciaux dans les chemins sont gérés correctement par fs::path. C'est un avantage majeur par rapport à la construction manuelle de chaînes, où l'oubli d'un échappement ou d'un guillemet est une source classique de bugs.
| Catégorie | Méthodes |
|---|---|
| Décomposition | root_name(), root_directory(), root_path(), relative_path(), parent_path(), filename(), stem(), extension() |
| Test | empty(), is_absolute(), is_relative(), has_root_path(), has_filename(), has_stem(), has_extension() |
| Modification | replace_filename(), replace_extension(), remove_filename(), operator/=, operator+= |
| Normalisation | lexically_normal(), lexically_relative(), lexically_proximate() |
| Conversion | string(), native(), c_str(), u8string(), wstring() |
| Itération | begin(), end() (composantes du chemin) |
| Comparaison | ==, !=, <, <=, >, >=, compare() |
| Catégorie | Fonctions libres |
|---|---|
| Résolution | absolute(), canonical(), weakly_canonical() |
| Relation | relative(), proximate() |
| Équivalence | equivalent() |
| Système | current_path(), temp_directory_path() |
💡 Note — La manipulation de chemins semble triviale dans les exemples isolés, mais elle devient rapidement complexe dans un vrai projet : chemins provenant de l'utilisateur (potentiellement malformés), liens symboliques, variables d'environnement absentes, caractères spéciaux, différences entre l'arbre de build et l'arbre d'installation. La rigueur acquise ici — normaliser avant de comparer, résoudre avant d'accéder, vérifier les paths vides — est ce qui sépare un outil fiable d'un prototype fragile. Le réflexe de toujours utiliser
fs::pathplutôt que desstd::stringpour représenter des chemins est le premier pas vers cette robustesse.