| author | wenzelm |
| Thu, 21 Apr 2022 10:07:17 +0200 | |
| changeset 75441 | 400e325a5416 |
| parent 75393 | 87ebf5a50283 |
| child 75696 | c79df6dc2803 |
| permissions | -rw-r--r-- |
| 43600 | 1 |
/* Title: Pure/General/path.scala |
2 |
Author: Makarius |
|
3 |
||
| 43601 | 4 |
Algebra of file-system paths: basic POSIX notation, extended by named |
5 |
roots (e.g. //foo) and variables (e.g. $BAR). |
|
| 43600 | 6 |
*/ |
7 |
||
8 |
package isabelle |
|
9 |
||
10 |
||
| 73897 | 11 |
import java.util.{Map => JMap}
|
| 48409 | 12 |
import java.io.{File => JFile}
|
| 73945 | 13 |
import java.nio.file.{Path => JPath}
|
| 48373 | 14 |
|
|
43697
77ce24aa1770
explicit Document.Node.Header, with master_dir and thy_name;
wenzelm
parents:
43670
diff
changeset
|
15 |
import scala.util.matching.Regex |
|
77ce24aa1770
explicit Document.Node.Header, with master_dir and thy_name;
wenzelm
parents:
43670
diff
changeset
|
16 |
|
|
77ce24aa1770
explicit Document.Node.Header, with master_dir and thy_name;
wenzelm
parents:
43670
diff
changeset
|
17 |
|
| 75393 | 18 |
object Path {
|
| 43600 | 19 |
/* path elements */ |
20 |
||
| 45244 | 21 |
sealed abstract class Elem |
| 60215 | 22 |
private case class Root(name: String) extends Elem |
23 |
private case class Basic(name: String) extends Elem |
|
24 |
private case class Variable(name: String) extends Elem |
|
| 43600 | 25 |
private case object Parent extends Elem |
26 |
||
27 |
private def err_elem(msg: String, s: String): Nothing = |
|
| 69547 | 28 |
error(msg + " path element " + quote(s)) |
29 |
||
|
69548
415dc92050a6
more strict check: avoid confusion of Path.basic with Path.current / Path.parent;
wenzelm
parents:
69547
diff
changeset
|
30 |
private val illegal_elem = Set("", "~", "~~", ".", "..")
|
|
69550
57ff523d9008
reject further illegal chars according to https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
wenzelm
parents:
69549
diff
changeset
|
31 |
private val illegal_char = "/\\$:\"'<>|?*" |
| 43600 | 32 |
|
33 |
private def check_elem(s: String): String = |
|
| 69547 | 34 |
if (illegal_elem.contains(s)) err_elem("Illegal", s)
|
| 52106 | 35 |
else {
|
|
69550
57ff523d9008
reject further illegal chars according to https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
wenzelm
parents:
69549
diff
changeset
|
36 |
for (c <- s) {
|
|
57ff523d9008
reject further illegal chars according to https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
wenzelm
parents:
69549
diff
changeset
|
37 |
if (c.toInt < 32) |
|
57ff523d9008
reject further illegal chars according to https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
wenzelm
parents:
69549
diff
changeset
|
38 |
err_elem("Illegal control character " + c.toInt + " in", s)
|
|
57ff523d9008
reject further illegal chars according to https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
wenzelm
parents:
69549
diff
changeset
|
39 |
if (illegal_char.contains(c)) |
|
57ff523d9008
reject further illegal chars according to https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
wenzelm
parents:
69549
diff
changeset
|
40 |
err_elem("Illegal character " + quote(c.toString) + " in", s)
|
| 69547 | 41 |
} |
| 52106 | 42 |
s |
43 |
} |
|
| 43600 | 44 |
|
45 |
private def root_elem(s: String): Elem = Root(check_elem(s)) |
|
46 |
private def basic_elem(s: String): Elem = Basic(check_elem(s)) |
|
47 |
private def variable_elem(s: String): Elem = Variable(check_elem(s)) |
|
48 |
||
49 |
private def apply_elem(y: Elem, xs: List[Elem]): List[Elem] = |
|
50 |
(y, xs) match {
|
|
51 |
case (Root(_), _) => List(y) |
|
52 |
case (Parent, Root(_) :: _) => xs |
|
53 |
case (Parent, Basic(_) :: rest) => rest |
|
54 |
case _ => y :: xs |
|
55 |
} |
|
56 |
||
57 |
private def norm_elems(elems: List[Elem]): List[Elem] = |
|
| 73361 | 58 |
elems.foldRight(List.empty[Elem])(apply_elem) |
| 43600 | 59 |
|
| 56844 | 60 |
private def implode_elem(elem: Elem, short: Boolean): String = |
| 43600 | 61 |
elem match {
|
62 |
case Root("") => ""
|
|
63 |
case Root(s) => "//" + s |
|
64 |
case Basic(s) => s |
|
| 56844 | 65 |
case Variable("USER_HOME") if short => "~"
|
66 |
case Variable("ISABELLE_HOME") if short => "~~"
|
|
| 43600 | 67 |
case Variable(s) => "$" + s |
68 |
case Parent => ".." |
|
69 |
} |
|
70 |
||
| 72962 | 71 |
private def squash_elem(elem: Elem): String = |
72 |
elem match {
|
|
73 |
case Root("") => "ROOT"
|
|
74 |
case Root(s) => "SERVER_" + s |
|
75 |
case Basic(s) => s |
|
76 |
case Variable(s) => s |
|
77 |
case Parent => "PARENT" |
|
78 |
} |
|
79 |
||
| 43600 | 80 |
|
81 |
/* path constructors */ |
|
82 |
||
| 45244 | 83 |
val current: Path = new Path(Nil) |
84 |
val root: Path = new Path(List(Root("")))
|
|
85 |
def named_root(s: String): Path = new Path(List(root_elem(s))) |
|
| 71601 | 86 |
def make(elems: List[String]): Path = new Path(elems.reverse.map(basic_elem)) |
| 45244 | 87 |
def basic(s: String): Path = new Path(List(basic_elem(s))) |
88 |
def variable(s: String): Path = new Path(List(variable_elem(s))) |
|
89 |
val parent: Path = new Path(List(Parent)) |
|
| 43600 | 90 |
|
| 73522 | 91 |
val USER_HOME: Path = variable("USER_HOME")
|
92 |
val ISABELLE_HOME: Path = variable("ISABELLE_HOME")
|
|
93 |
||
| 43600 | 94 |
|
95 |
/* explode */ |
|
96 |
||
| 75393 | 97 |
def explode(str: String): Path = {
|
| 52106 | 98 |
def explode_elem(s: String): Elem = |
99 |
try {
|
|
100 |
if (s == "..") Parent |
|
101 |
else if (s == "~") Variable("USER_HOME")
|
|
102 |
else if (s == "~~") Variable("ISABELLE_HOME")
|
|
103 |
else if (s.startsWith("$")) variable_elem(s.substring(1))
|
|
104 |
else basic_elem(s) |
|
105 |
} |
|
106 |
catch { case ERROR(msg) => cat_error(msg, "The error(s) above occurred in " + quote(str)) }
|
|
107 |
||
|
43670
7f933761764b
prefer space_explode/split_lines as in Isabelle/ML;
wenzelm
parents:
43669
diff
changeset
|
108 |
val ss = space_explode('/', str)
|
| 43600 | 109 |
val r = ss.takeWhile(_.isEmpty).length |
110 |
val es = ss.dropWhile(_.isEmpty) |
|
111 |
val (roots, raw_elems) = |
|
112 |
if (r == 0) (Nil, es) |
|
113 |
else if (r == 1) (List(Root("")), es)
|
|
114 |
else if (es.isEmpty) (List(Root("")), Nil)
|
|
115 |
else (List(root_elem(es.head)), es.tail) |
|
| 52106 | 116 |
val elems = raw_elems.filterNot(s => s.isEmpty || s == ".").map(explode_elem) |
117 |
||
| 63866 | 118 |
new Path(norm_elems(elems reverse_::: roots)) |
| 43600 | 119 |
} |
| 43669 | 120 |
|
|
55879
ac979f750c1a
clarified path checks: avoid crash of rendering due to spurious errors;
wenzelm
parents:
55555
diff
changeset
|
121 |
def is_wellformed(str: String): Boolean = |
| 48484 | 122 |
try { explode(str); true } catch { case ERROR(_) => false }
|
123 |
||
|
55879
ac979f750c1a
clarified path checks: avoid crash of rendering due to spurious errors;
wenzelm
parents:
55555
diff
changeset
|
124 |
def is_valid(str: String): Boolean = |
|
ac979f750c1a
clarified path checks: avoid crash of rendering due to spurious errors;
wenzelm
parents:
55555
diff
changeset
|
125 |
try { explode(str).expand; true } catch { case ERROR(_) => false }
|
|
ac979f750c1a
clarified path checks: avoid crash of rendering due to spurious errors;
wenzelm
parents:
55555
diff
changeset
|
126 |
|
| 43669 | 127 |
def split(str: String): List[Path] = |
|
43670
7f933761764b
prefer space_explode/split_lines as in Isabelle/ML;
wenzelm
parents:
43669
diff
changeset
|
128 |
space_explode(':', str).filterNot(_.isEmpty).map(explode)
|
| 48457 | 129 |
|
130 |
||
131 |
/* encode */ |
|
132 |
||
133 |
val encode: XML.Encode.T[Path] = (path => XML.Encode.string(path.implode)) |
|
|
69551
adb52af5ba55
exclude file name components that are special on Windows;
wenzelm
parents:
69550
diff
changeset
|
134 |
|
|
adb52af5ba55
exclude file name components that are special on Windows;
wenzelm
parents:
69550
diff
changeset
|
135 |
|
|
adb52af5ba55
exclude file name components that are special on Windows;
wenzelm
parents:
69550
diff
changeset
|
136 |
/* reserved names */ |
|
adb52af5ba55
exclude file name components that are special on Windows;
wenzelm
parents:
69550
diff
changeset
|
137 |
|
|
adb52af5ba55
exclude file name components that are special on Windows;
wenzelm
parents:
69550
diff
changeset
|
138 |
private val reserved_windows: Set[String] = |
|
adb52af5ba55
exclude file name components that are special on Windows;
wenzelm
parents:
69550
diff
changeset
|
139 |
Set("CON", "PRN", "AUX", "NUL",
|
|
adb52af5ba55
exclude file name components that are special on Windows;
wenzelm
parents:
69550
diff
changeset
|
140 |
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", |
|
adb52af5ba55
exclude file name components that are special on Windows;
wenzelm
parents:
69550
diff
changeset
|
141 |
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9") |
|
adb52af5ba55
exclude file name components that are special on Windows;
wenzelm
parents:
69550
diff
changeset
|
142 |
|
|
adb52af5ba55
exclude file name components that are special on Windows;
wenzelm
parents:
69550
diff
changeset
|
143 |
def is_reserved(name: String): Boolean = |
|
adb52af5ba55
exclude file name components that are special on Windows;
wenzelm
parents:
69550
diff
changeset
|
144 |
Long_Name.explode(name).exists(a => reserved_windows.contains(Word.uppercase(a))) |
| 69904 | 145 |
|
146 |
||
147 |
/* case-insensitive names */ |
|
148 |
||
| 75393 | 149 |
def check_case_insensitive(paths: List[Path]): Unit = {
|
| 69904 | 150 |
val table = |
| 73359 | 151 |
paths.foldLeft(Multi_Map.empty[String, String]) { case (tab, path) =>
|
| 69904 | 152 |
val name = path.expand.implode |
153 |
tab.insert(Word.lowercase(name), name) |
|
| 73359 | 154 |
} |
| 69904 | 155 |
val collisions = |
156 |
(for { (_, coll) <- table.iterator_list if coll.length > 1 } yield coll).toList.flatten
|
|
157 |
if (collisions.nonEmpty) {
|
|
158 |
error(("Collision of file names due case-insensitivity:" :: collisions).mkString("\n "))
|
|
159 |
} |
|
160 |
} |
|
| 43600 | 161 |
} |
162 |
||
| 43669 | 163 |
|
| 75393 | 164 |
final class Path private( |
165 |
protected val elems: List[Path.Elem] // reversed elements |
|
166 |
) {
|
|
| 72746 | 167 |
override def hashCode: Int = elems.hashCode |
168 |
override def equals(that: Any): Boolean = |
|
169 |
that match {
|
|
170 |
case other: Path => elems == other.elems |
|
171 |
case _ => false |
|
172 |
} |
|
173 |
||
| 43600 | 174 |
def is_current: Boolean = elems.isEmpty |
| 59319 | 175 |
def is_absolute: Boolean = elems.nonEmpty && elems.last.isInstanceOf[Path.Root] |
| 65559 | 176 |
def is_root: Boolean = elems match { case List(Path.Root(_)) => true case _ => false }
|
| 43600 | 177 |
def is_basic: Boolean = elems match { case List(Path.Basic(_)) => true case _ => false }
|
| 75107 | 178 |
def all_basic: Boolean = elems.forall(_.isInstanceOf[Path.Basic]) |
| 72572 | 179 |
def starts_basic: Boolean = elems.nonEmpty && elems.last.isInstanceOf[Path.Basic] |
| 43600 | 180 |
|
| 73360 | 181 |
def +(other: Path): Path = new Path(other.elems.foldRight(elems)(Path.apply_elem)) |
| 43600 | 182 |
|
183 |
||
| 43604 | 184 |
/* implode */ |
| 43600 | 185 |
|
| 56844 | 186 |
private def gen_implode(short: Boolean): String = |
| 43600 | 187 |
elems match {
|
188 |
case Nil => "." |
|
189 |
case List(Path.Root("")) => "/"
|
|
| 56844 | 190 |
case _ => elems.map(Path.implode_elem(_, short)).reverse.mkString("/")
|
| 43600 | 191 |
} |
| 56844 | 192 |
def implode: String = gen_implode(false) |
193 |
def implode_short: String = gen_implode(true) |
|
| 43600 | 194 |
|
| 43652 | 195 |
override def toString: String = quote(implode) |
| 43600 | 196 |
|
197 |
||
198 |
/* base element */ |
|
199 |
||
200 |
private def split_path: (Path, String) = |
|
201 |
elems match {
|
|
| 45244 | 202 |
case Path.Basic(s) :: xs => (new Path(xs), s) |
| 43604 | 203 |
case _ => error("Cannot split path into dir/base: " + toString)
|
| 43600 | 204 |
} |
205 |
||
206 |
def dir: Path = split_path._1 |
|
| 45244 | 207 |
def base: Path = new Path(List(Path.Basic(split_path._2))) |
| 43600 | 208 |
|
| 74056 | 209 |
def ends_with(a: String): Boolean = |
210 |
elems match {
|
|
211 |
case Path.Basic(b) :: _ => b.endsWith(a) |
|
212 |
case _ => false |
|
213 |
} |
|
214 |
def is_java: Boolean = ends_with(".java")
|
|
215 |
def is_scala: Boolean = ends_with(".scala")
|
|
| 75119 | 216 |
def is_pdf: Boolean = ends_with(".pdf")
|
| 74056 | 217 |
|
| 43600 | 218 |
def ext(e: String): Path = |
219 |
if (e == "") this |
|
220 |
else {
|
|
221 |
val (prfx, s) = split_path |
|
| 43604 | 222 |
prfx + Path.basic(s + "." + e) |
| 43600 | 223 |
} |
| 43604 | 224 |
|
| 72575 | 225 |
def xz: Path = ext("xz")
|
| 73660 | 226 |
def xml: Path = ext("xml")
|
| 72962 | 227 |
def html: Path = ext("html")
|
|
72574
d892f6d66402
build documents in Isabelle/Scala, based on generated tex files as session exports;
wenzelm
parents:
72572
diff
changeset
|
228 |
def tex: Path = ext("tex")
|
|
d892f6d66402
build documents in Isabelle/Scala, based on generated tex files as session exports;
wenzelm
parents:
72572
diff
changeset
|
229 |
def pdf: Path = ext("pdf")
|
| 72776 | 230 |
def thy: Path = ext("thy")
|
| 73628 | 231 |
def tar: Path = ext("tar")
|
232 |
def gz: Path = ext("gz")
|
|
|
73691
2f9877db82a1
reimplemented Mirabelle as Isabelle/ML presentation hook + Isabelle/Scala tool, but sledgehammer is still inactive;
wenzelm
parents:
73660
diff
changeset
|
233 |
def log: Path = ext("log")
|
|
75230
bbbee54b1198
prepare patched version more thoroughly, with explicit patches;
wenzelm
parents:
75220
diff
changeset
|
234 |
def orig: Path = ext("orig")
|
| 75220 | 235 |
def patch: Path = ext("patch")
|
|
75312
e641ac92b489
more formal extension_manifest, with shasum for sources;
wenzelm
parents:
75273
diff
changeset
|
236 |
def shasum: Path = ext("shasum")
|
|
72574
d892f6d66402
build documents in Isabelle/Scala, based on generated tex files as session exports;
wenzelm
parents:
72572
diff
changeset
|
237 |
|
| 75393 | 238 |
def backup: Path = {
|
| 53336 | 239 |
val (prfx, s) = split_path |
240 |
prfx + Path.basic(s + "~") |
|
241 |
} |
|
242 |
||
| 75393 | 243 |
def backup2: Path = {
|
| 58610 | 244 |
val (prfx, s) = split_path |
245 |
prfx + Path.basic(s + "~~") |
|
246 |
} |
|
247 |
||
| 75273 | 248 |
def exe: Path = ext("exe")
|
249 |
def platform_exe: Path = if (Platform.is_windows) exe else this |
|
| 72464 | 250 |
|
|
43697
77ce24aa1770
explicit Document.Node.Header, with master_dir and thy_name;
wenzelm
parents:
43670
diff
changeset
|
251 |
private val Ext = new Regex("(.*)\\.([^.]*)")
|
|
77ce24aa1770
explicit Document.Node.Header, with master_dir and thy_name;
wenzelm
parents:
43670
diff
changeset
|
252 |
|
| 75393 | 253 |
def split_ext: (Path, String) = {
|
|
43697
77ce24aa1770
explicit Document.Node.Header, with master_dir and thy_name;
wenzelm
parents:
43670
diff
changeset
|
254 |
val (prefix, base) = split_path |
|
77ce24aa1770
explicit Document.Node.Header, with master_dir and thy_name;
wenzelm
parents:
43670
diff
changeset
|
255 |
base match {
|
|
77ce24aa1770
explicit Document.Node.Header, with master_dir and thy_name;
wenzelm
parents:
43670
diff
changeset
|
256 |
case Ext(b, e) => (prefix + Path.basic(b), e) |
| 56556 | 257 |
case _ => (prefix + Path.basic(base), "") |
|
43697
77ce24aa1770
explicit Document.Node.Header, with master_dir and thy_name;
wenzelm
parents:
43670
diff
changeset
|
258 |
} |
|
77ce24aa1770
explicit Document.Node.Header, with master_dir and thy_name;
wenzelm
parents:
43670
diff
changeset
|
259 |
} |
|
77ce24aa1770
explicit Document.Node.Header, with master_dir and thy_name;
wenzelm
parents:
43670
diff
changeset
|
260 |
|
| 69367 | 261 |
def drop_ext: Path = split_ext._1 |
262 |
def get_ext: String = split_ext._2 |
|
263 |
||
| 72962 | 264 |
def squash: Path = new Path(elems.map(elem => Path.Basic(Path.squash_elem(elem)))) |
265 |
||
| 43604 | 266 |
|
267 |
/* expand */ |
|
268 |
||
| 75393 | 269 |
def expand_env(env: JMap[String, String]): Path = {
|
| 43604 | 270 |
def eval(elem: Path.Elem): List[Path.Elem] = |
271 |
elem match {
|
|
| 43664 | 272 |
case Path.Variable(s) => |
|
64228
b46969a851a9
expand relatively to given environment, notably remote HOME;
wenzelm
parents:
63866
diff
changeset
|
273 |
val path = Path.explode(Isabelle_System.getenv_strict(s, env)) |
| 48658 | 274 |
if (path.elems.exists(_.isInstanceOf[Path.Variable])) |
|
73715
bf51c23f3f99
clarified signature -- avoid odd warning about scala/bug#6675;
wenzelm
parents:
73712
diff
changeset
|
275 |
error("Illegal path variable nesting: " + Properties.Eq(s, path.toString))
|
| 48658 | 276 |
else path.elems |
| 43604 | 277 |
case x => List(x) |
278 |
} |
|
279 |
||
| 71383 | 280 |
new Path(Path.norm_elems(elems.flatMap(eval))) |
| 43604 | 281 |
} |
| 48373 | 282 |
|
|
64228
b46969a851a9
expand relatively to given environment, notably remote HOME;
wenzelm
parents:
63866
diff
changeset
|
283 |
def expand: Path = expand_env(Isabelle_System.settings()) |
|
b46969a851a9
expand relatively to given environment, notably remote HOME;
wenzelm
parents:
63866
diff
changeset
|
284 |
|
| 69366 | 285 |
def file_name: String = expand.base.implode |
286 |
||
| 48373 | 287 |
|
| 72784 | 288 |
/* implode wrt. given directories */ |
| 48548 | 289 |
|
| 75393 | 290 |
def implode_symbolic: String = {
|
| 72784 | 291 |
val directories = |
292 |
Library.space_explode(':', Isabelle_System.getenv("ISABELLE_DIRECTORIES")).reverse
|
|
293 |
val full_name = expand.implode |
|
294 |
directories.view.flatMap(a => |
|
295 |
try {
|
|
296 |
val b = Path.explode(a).expand.implode |
|
297 |
if (full_name == b) Some(a) |
|
298 |
else {
|
|
299 |
Library.try_unprefix(b + "/", full_name) match {
|
|
300 |
case Some(name) => Some(a + "/" + name) |
|
301 |
case None => None |
|
302 |
} |
|
303 |
} |
|
304 |
} catch { case ERROR(_) => None }).headOption.getOrElse(implode)
|
|
305 |
} |
|
306 |
||
307 |
def position: Position.T = Position.File(implode_symbolic) |
|
| 48548 | 308 |
|
309 |
||
| 66232 | 310 |
/* platform files */ |
| 48373 | 311 |
|
| 60988 | 312 |
def file: JFile = File.platform_file(this) |
| 48548 | 313 |
def is_file: Boolean = file.isFile |
314 |
def is_dir: Boolean = file.isDirectory |
|
| 65833 | 315 |
|
| 73945 | 316 |
def java_path: JPath = file.toPath |
317 |
||
| 66232 | 318 |
def absolute_file: JFile = File.absolute(file) |
319 |
def canonical_file: JFile = File.canonical(file) |
|
| 67181 | 320 |
|
321 |
def absolute: Path = File.path(absolute_file) |
|
322 |
def canonical: Path = File.path(canonical_file) |
|
| 43600 | 323 |
} |