lean4-htt/src/Init/System/FilePath.lean
2025-04-23 17:21:33 +00:00

285 lines
9.4 KiB
Text

/-
Copyright (c) 2019 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura, Sebastian Ullrich
-/
module
prelude
import Init.System.Platform
import Init.Data.ToString.Basic
namespace System
open Platform
/--
A path on the file system.
Paths consist of a sequence of directories followed by the name of a file or directory. They are
delimited by a platform-dependent separator character (see `System.FilePath.pathSeparator`).
-/
structure FilePath where
/-- The string representation of the path. -/
toString : String
deriving Inhabited, DecidableEq, Hashable
instance : Repr FilePath where
reprPrec p := Repr.addAppParen ("FilePath.mk " ++ repr p.toString)
instance : ToString FilePath where
toString p := p.toString
namespace FilePath
/--
The character that separates directories.
On platforms that support multiple separators, `System.FilePath.pathSeparator` is the “ideal” one expected by users
on the platform. `System.FilePath.pathSeparators` lists all supported separators.
-/
def pathSeparator : Char :=
if isWindows then '\\' else '/'
/--
The list of all path separator characters supported on the current platform.
On platforms that support multiple separators, `System.FilePath.pathSeparator` is the “ideal” one
expected by users on the platform.
-/
def pathSeparators : List Char :=
if isWindows then ['\\', '/'] else ['/']
/--
The character that separates file extensions from file names.
-/
def extSeparator : Char := '.'
/--
The file extension expected for executable binaries on the current platform, or `""` if there is no
such extension.
-/
def exeExtension : String :=
if isWindows then "exe" else ""
/--
Normalizes a path, returning an equivalent path that may better follow platform conventions.
In particular:
* On Windows, drive letters are made uppercase.
* On platforms that support multiple path separators (that is, where
`System.FilePath.pathSeparators` has length greater than one), alternative path separators are
replaced with the preferred path separator.
There is no guarantee that two equivalent paths normalize to the same path.
-/
-- TODO: normalize `a/`, `a//b`, etc.
def normalize (p : FilePath) : FilePath := Id.run do
let mut p := p
-- normalize drive letter
if isWindows && p.toString.length >= 2 && (p.toString.get 0).isLower && p.toString.get ⟨1⟩ == ':' then
p := ⟨p.toString.set 0 (p.toString.get 0).toUpper⟩
-- normalize separator
unless pathSeparators.length == 1 do
p := ⟨p.toString.map fun c => if pathSeparators.contains c then pathSeparator else c⟩
return p
-- the following functions follow the names and semantics from Rust's `std::path::Path`
/--
An absolute path starts at the root directory or a drive letter. Accessing files through an absolute
path does not depend on the current working directory.
-/
def isAbsolute (p : FilePath) : Bool :=
pathSeparators.contains p.toString.front || (isWindows && p.toString.length > 1 && p.toString.iter.next.curr == ':')
/--
A relative path is one that depends on the current working directory for interpretation. Relative
paths do not start with the root directory or a drive letter.
-/
def isRelative (p : FilePath) : Bool :=
!p.isAbsolute
/--
Appends two paths, taking absolute paths into account. This operation is also accessible via the `/`
operator.
If `sub` is an absolute path, then `p` is discarded and `sub` is returned. If `sub` is a relative
path, then it is attached to `p` with the platform-specific path separator.
-/
def join (p sub : FilePath) : FilePath :=
if sub.isAbsolute then
sub
else
⟨p.toString ++ pathSeparator.toString ++ sub.toString⟩
instance : Div FilePath where
div := FilePath.join
instance : HDiv FilePath String FilePath where
hDiv p sub := FilePath.join p ⟨sub⟩
private def posOfLastSep (p : FilePath) : Option String.Pos :=
p.toString.revFind pathSeparators.contains
/--
Returns the parent directory of a path, if there is one.
If the path is that of the root directory or the root of a drive letter, `none` is returned.
Otherwise, the path's parent directory is returned.
-/
def parent (p : FilePath) : Option FilePath :=
let extractParentPath := FilePath.mk <$> p.toString.extract {} <$> posOfLastSep p
if p.isAbsolute then
let lengthOfRootDirectory := if pathSeparators.contains p.toString.front then 1 else 3
if p.toString.length == lengthOfRootDirectory then
-- `p` is a root directory
none
else if posOfLastSep p == String.Pos.mk (lengthOfRootDirectory - 1) then
-- `p` is a direct child of the root
some ⟨p.toString.extract 0 ⟨lengthOfRootDirectory⟩⟩
else
-- `p` is an absolute path with at least two subdirectories
extractParentPath
else
-- `p` is a relative path
extractParentPath
/--
Extracts the last element of a path if it is a file or directory name.
Returns `none ` if the last entry is a special name (such as `.` or `..`) or if the path is the root
directory.
-/
def fileName (p : FilePath) : Option String :=
let lastPart := match posOfLastSep p with
| some sepPos => p.toString.extract (sepPos + '/') p.toString.endPos
| none => p.toString
if lastPart.isEmpty || lastPart == "." || lastPart == ".." then none else some lastPart
/--
Extracts the stem (non-extension) part of `p.fileName`.
If the filename contains multiple extensions, then only the last one is removed. Returns `none` if
there is no file name at the end of the path.
Examples:
* `("app.exe" : System.FilePath).fileStem = some "app"`
* `("file.tar.gz" : System.FilePath).fileStem = some "file.tar"`
* `("files/" : System.FilePath).fileStem = none`
* `("files/picture.jpg" : System.FilePath).fileStem = some "picture"`
-/
def fileStem (p : FilePath) : Option String :=
p.fileName.map fun fname =>
match fname.revPosOf '.' with
| some ⟨0⟩ => fname
| some pos => fname.extract 0 pos
| none => fname
/--
Extracts the extension part of `p.fileName`.
If the filename contains multiple extensions, then only the last one is extracted. Returns `none` if
there is no file name at the end of the path.
Examples:
* `("app.exe" : System.FilePath).extension = some "exe"`
* `("file.tar.gz" : System.FilePath).extension = some "gz"`
* `("files/" : System.FilePath).extension = none`
* `("files/picture.jpg" : System.FilePath).extension = some "jpg"`
-/
def extension (p : FilePath) : Option String :=
p.fileName.bind fun fname =>
match fname.revPosOf '.' with
| some 0 => none
| some pos => fname.extract (pos + '.') fname.endPos
| none => none
/--
Replaces the file name at the end of the path `p` with `fname`, placing `fname` in the parent
directory of `p`.
If `p` has no parent directory, then `fname` is returned unmodified.
-/
def withFileName (p : FilePath) (fname : String) : FilePath :=
match p.parent with
| none => ⟨fname⟩
| some p => p / fname
/--
Appends the extension `ext` to a path `p`.
`ext` should not have leading `.`, as this function adds one. If `ext` is the empty string, no
`.` is added.
Unlike `System.FilePath.withExtension`, this does not remove any existing extension.
-/
def addExtension (p : FilePath) (ext : String) : FilePath :=
match p.fileName with
| none => p
| some fname => p.withFileName (if ext.isEmpty then fname else fname ++ "." ++ ext)
/--
Replaces the current extension in a path `p` with `ext`, adding it if there is no extension. If the
path has multiple file extensions, only the last one is replaced. If the path has no filename, or if
`ext` is the empty string, then the filename is returned unmodified.
`ext` should not have a leading `.`, as this function adds one.
Examples:
* `("files/picture.jpeg" : System.FilePath).withExtension "jpg" = ⟨"files/picture.jpg"⟩`
* `("files/" : System.FilePath).withExtension "zip" = ⟨"files/"⟩`
* `("files" : System.FilePath).withExtension "zip" = ⟨"files.zip"⟩`
* `("files/archive.tar.gz" : System.FilePath).withExtension "xz" = ⟨"files.tar.xz"⟩`
-/
def withExtension (p : FilePath) (ext : String) : FilePath :=
match p.fileStem with
| none => p
| some stem => p.withFileName (if ext.isEmpty then stem else stem ++ "." ++ ext)
/--
Splits a path into a list of individual file names at the platform-specific path separator.
-/
def components (p : FilePath) : List String :=
p.normalize |>.toString.splitOn pathSeparator.toString
end FilePath
/--
Constructs a path from a list of file names by interspersing them with the current platform's path
separator.
-/
def mkFilePath (parts : List String) : FilePath :=
⟨String.intercalate FilePath.pathSeparator.toString parts⟩
instance : Coe String FilePath where
coe := FilePath.mk
abbrev SearchPath := List FilePath
namespace SearchPath
/--
The character that is used to separate the entries in the `$PATH` (or `%PATH%`) environment variable.
This value is platform dependent.
-/
protected def separator : Char :=
if isWindows then ';' else ':'
/--
Separates the entries in the `$PATH` (or `%PATH%`) environment variable by the current
platform-dependent separator character.
-/
def parse (s : String) : SearchPath :=
s.split (fun c => SearchPath.separator == c) |>.map FilePath.mk
/--
Joins a list of paths into a suitable value for the current platform's `$PATH` (or `%PATH%`)
environment variable.
-/
def toString (path : SearchPath) : String :=
SearchPath.separator.toString.intercalate (path.map FilePath.toString)
end SearchPath
end System