This PR splits some low-hanging fruit out of `Init.Data.String.Basic`: basic material about `String.Pos.Raw`, `String.Substrig`, and `String.Iterator`. More splitting required and the remaining material is quite unorganized, but it's a start.
287 lines
9.5 KiB
Text
287 lines
9.5 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
|
|
public import Init.Data.String.Basic
|
|
import Init.Data.String.Iterator
|
|
|
|
public section
|
|
|
|
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.front.isLower && String.Pos.Raw.get p.toString ⟨1⟩ == ':' then
|
|
p := ⟨p.toString.capitalize⟩
|
|
-- 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.Raw :=
|
|
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 <$> String.Pos.Raw.extract p.toString {} <$> 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 == some (String.Pos.Raw.mk (lengthOfRootDirectory - 1)) then
|
|
-- `p` is a direct child of the root
|
|
some ⟨String.Pos.Raw.extract p.toString 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 => String.Pos.Raw.extract p.toString (sepPos + '/') p.toString.rawEndPos
|
|
| 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 => String.Pos.Raw.extract fname 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 => some <| String.Pos.Raw.extract fname (pos + '.') fname.rawEndPos
|
|
| 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.splitToList (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
|