<?php # -*- coding: utf-8 mode: php -*- vim:sw=2:sts=2:et:ai:si:sta:fenc=utf-8 namespace nur; use nur\b\io\IOException; /** * Class path: manipulation de chemins du système de fichiers */ class path { /** * Normaliser le chemin spécifié: * - supprimer si possible les ocurrences de ./ et ../ * - supprimer les slash multiples, sauf s'il y en a exactement 2 au début de * la chaine * - "~/path" est transformé en "$HOME/path" * * cas particuliers: * - retourner la valeur null inchangée * - retourner '.' pour un chemin vide */ static final function normalize(?string $path): ?string { if ($path === null) return null; if ($path !== "") { if (substr($path, 0, 2) == "~/") { $path = os::homedir().substr($path, 1); } $initial_slashes = strpos($path, "/") === 0; if ($initial_slashes && strpos($path, "//") === 0 && strpos($path, "///") === false) { $initial_slashes = 2; } $initial_slashes = intval($initial_slashes); $comps = explode("/", $path); $new_comps = array(); foreach ($comps as $comp) { if ($comp === "" || $comp === ".") continue; if ($comp != ".." || (!$initial_slashes && !$new_comps) || ($new_comps && end($new_comps) == "..")) { array_push($new_comps, $comp); } elseif ($new_comps) { array_pop($new_comps); } } $comps = $new_comps; $path = implode("/", $comps); if ($initial_slashes) $path = str_repeat("/", $initial_slashes) . $path; } return $path !== ""? $path: "."; } /** Comme normalize() mais retourner inchangée la valeur false */ static final function with($path) { if ($path === null || $path === false) return $path; else return self::normalize(strval($path)); } /** obtenir le chemin absolu normalisé correspondant à $path */ static final function abspath($path, ?string $cwd=null) { if ($path === null || $path === false) return $path; $path = strval($path); if (substr($path, 0, 1) !== "/") { if ($cwd === null) $cwd = getcwd(); $path = "$cwd/$path"; } return self::normalize($path); } /** * obtenir le chemin $path exprimé relativement à $basedir. * si $basedir est relatif, il est exprimé par rapport à $cwd qui vaut par * défaut le chemin courant. */ static final function relpath($path, string $basedir="", ?string $cwd=null): string { $path = self::abspath($path, $cwd); $basedir = self::abspath($basedir, $cwd); if ($path === $basedir) return ""; $initial_slashes = strpos($path, "//") === 0? 2: 1; $path = substr($path, $initial_slashes); $initial_slashes = strpos($basedir, "//") === 0? 2: 1; $basedir = substr($basedir, $initial_slashes); if ($basedir === "") return $path; $pparts = $path? explode("/", $path): []; $pcount = count($pparts); $bparts = explode("/", $basedir); $bcount = count($bparts); $i = 0; while ($i < $pcount && $i < $bcount && $pparts[$i] === $bparts[$i]) { $i++; } $ups = array_fill(0, $bcount - $i, ".."); $relparts = array_merge($ups, array_slice($pparts, $i)); return implode("/", $relparts); } /** * obtenir un chemin visuellement agréable: * - $HOME est remplacé par "~" * - $CWD/ est remplacé par "" */ static final function ppath($path, ?string $cwd=null): string { $path = self::abspath($path, $cwd); $homedir = os::homedir(); $cwd = getcwd()."/"; if (str::del_prefix($path, $cwd)) { return $path; } if (str::_starts_with($homedir, $path)) { str::del_prefix($path, $homedir); $path ="~$path"; } return $path; } /** * obtenir le chemin absolu canonique correspondant à $path * * @throws IOException si le chemin n'existe pas ou si une autre erreur se * produit */ static final function realpath($path) { if ($path === null || $path === false) return $path; $path = strval($path); $realpath = realpath($path); if ($realpath === false) throw IOException::last_error(); return $realpath; } /** * obtenir le chemin parent de $path * * cas particuliers: * dirname("/") === "/"; * dirname("filename") === "."; */ static final function dirname($path): ?string { if ($path === null || $path === false) return $path; else return dirname(strval($path)); } /** obtenir le nom du fichier sans son chemin */ static final function filename($path): ?string { if ($path === null || $path === false) return $path; $index = strrpos($path, "/"); if ($index !== false) $path = substr($path, $index + 1); return $path; } /** obtenir le nom de base du fichier (sans son chemin et sans l'extension) */ static final function basename($path): ?string { if ($path === null || $path === false) return $path; $basename = self::filename($path); $index = strrpos($basename, "."); if ($index !== false) $basename = substr($basename, 0, $index); return $basename; } /** obtenir l'extension du fichier. l'extension est retournée avec le '.' */ static final function ext($path): ?string { if ($path === null || $path === false) return $path; $ext = self::filename($path); $index = strrpos($ext, "."); if ($index === false) $ext = ""; else $ext = substr($ext, $index); return $ext; } /** découper le chemin entre sa partie "répertoire" et "fichier" */ static final function split($path): array { if ($path === null || $path === false) return [$path, $path]; elseif ($path === "") return ["", ""]; $index = strrpos($path, "/"); if ($index !== false) { if ($index == 0) $dir = "/"; else $dir = substr($path, 0, $index); $file = substr($path, $index + 1); } else { $dir = ""; $file = $path; } return [$dir, $file]; } /** * joindre les composantes pour faire un seul chemin normalisé. les composantes * sont joints inconditionnellements */ static final function join(...$parts): ?string { $path = null; foreach ($parts as $part) { if ($part === null || $part === false) continue; if ($path && substr($path, -1) !== "/" && substr($part, 0, 1) !== "/") $path .= "/"; if ($path === null) $path = ""; $path .= $part; } return self::normalize($path); } /** * joindre les composantes pour faire un seul chemin normalisé. chaque * composante relative s'ajoute au chemin. chaque composante absolue relance * le calcul de puis le début * e.g reljoin("a", "b", "../c", "d/e") retourne "a/c/d/e" * alors que reljoin("a", "b", "/c", "d/e") retourne "/c/d/e" */ static final function reljoin(...$parts): ?string { $path = null; foreach ($parts as $part) { if ($part === null || $part === false) continue; if (substr($part, 0, 1) == "/") { $path = $part; continue; } if ($path && substr($path, -1) !== "/") $path .= "/"; elseif ($path === null) $path = ""; $path .= $part; } return self::normalize($path); } /** * tester si $dir est situé dans $basedir. * * par défaut, on considère qu'un chemin $dir est situé dans lui-même sauf si * $strict == true, auquel cas "$dir est dans $basedir" implique $dir !== $basedir */ static final function within(string $dir, string $basedir, bool $strict=false): bool { $dir = self::abspath($dir); $basedir = self::abspath($basedir); if ($dir === $basedir) return !$strict; $prefix = $basedir; if ($prefix !== "/") $prefix .= "/"; return substr($dir, 0, strlen($prefix)) === $prefix; } /** * tester si $file a un répertoire, c'est à dire s'il contient le caractère '/' * e.g have_dir('dir/name') est vrai alors que have_dir('name.ext') est faux */ static final function have_dir(string $file): bool { return strpos($file, "/") !== false; } /** * tester si le chemin est qualifié, c'est à dire s'il est absolu ou s'il est * relatif à '.' ou '..' * e.g is_qualified('./a/b') est vrai alors que is_qualified('a/b') est faux */ static final function is_qualified(string $file): bool { if (strpos($file, "/") === false) return false; if (substr($file, 0, 1) == "/") return true; if (substr($file, 0, 2) == "./") return true; if (substr($file, 0, 3) == "../") return true; return false; } /** tester si $file a une extension */ static final function have_ext(string $file): bool { $pos = strrpos($file, "/"); if ($pos === false) $pos = 0; return strpos($file, ".", $pos) !== false; } /** * s'assurer que $path a l'extension $new_ext. si $path a l'une des extensions * de $replace_ext, l'enlever avant de rajouter $new_ext. * * si $strict === false alors $new_ext est ajouté à la liste $replace_ext * c'est à dire que si $path a déjà l'extension $new_ext, elle n'est pas * rajoutée de nouveau. * * si $strict === true alors seules les extensions de $replace_ext sont * enlevées le cas échéant, et $new_ext est systématiquement rajouté * * @param string $path * @param string $new_ext * @param string|array|null $replace_ext * @return string */ static final function ensure_ext(string $path, string $new_ext, $replace_ext=null, bool $strict=false): string { [$dir, $filename] = self::split($path); $ext = self::ext($filename); if (is_string($replace_ext)) $replace_ext = [$replace_ext]; if (!$strict) $replace_ext[] = $new_ext; if ($ext !== null && $replace_ext !== null) { foreach ($replace_ext as $old_ext) { if ($ext === $old_ext) { $filename = self::basename($filename); break; } } } $filename .= $new_ext; return self::join($dir, $filename); } /** tester si le fichier, le répertoire ou le lien symbolique existe */ static final function exists(string $file): bool { return is_link($file) || file_exists($file); } /** tester si $dir est un répertoire (mais pas un lien symbolique) */ static final function is_dir(string $dir, bool $allow_link=false): bool { return is_dir($dir) && ($allow_link || !is_link($dir)); } /** tester si $dir est un fichier (mais pas un lien symbolique) */ static final function is_file(string $file, bool $allow_link=false): bool { return is_file($file) && ($allow_link || !is_link($file)); } }