2023-12-03 22:10:18 +04:00
|
|
|
<?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;
|
|
|
|
}
|
|
|
|
|
2024-04-22 14:12:32 +04:00
|
|
|
/**
|
2024-04-22 23:41:19 +04:00
|
|
|
* 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é
|
|
|
|
*
|
2024-04-22 14:12:32 +04:00
|
|
|
* @param string $path
|
|
|
|
* @param string $new_ext
|
|
|
|
* @param string|array|null $replace_ext
|
|
|
|
* @return string
|
|
|
|
*/
|
2024-04-22 23:41:19 +04:00
|
|
|
static final function ensure_ext(string $path, string $new_ext, $replace_ext=null, bool $strict=false): string {
|
2023-12-03 22:10:18 +04:00
|
|
|
[$dir, $filename] = self::split($path);
|
2024-04-22 14:12:32 +04:00
|
|
|
$ext = self::ext($filename);
|
2024-04-22 23:41:19 +04:00
|
|
|
if (is_string($replace_ext)) $replace_ext = [$replace_ext];
|
|
|
|
if (!$strict) $replace_ext[] = $new_ext;
|
2024-04-22 14:12:32 +04:00
|
|
|
if ($ext !== null && $replace_ext !== null) {
|
|
|
|
foreach ($replace_ext as $old_ext) {
|
|
|
|
if ($ext === $old_ext) {
|
|
|
|
$filename = self::basename($filename);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2023-12-03 22:10:18 +04:00
|
|
|
}
|
|
|
|
$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));
|
|
|
|
}
|
|
|
|
}
|