diff --git a/.gitignore b/.gitignore index e971f8b..50b8d3a 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ jsrc/*.jar .bloop/ metals.sbt .vscode +*Atfile diff --git a/.scalafmt.conf b/.scalafmt.conf index 1440b62..435287d 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,6 +1,11 @@ version = 3.7.14 +preset = default align.preset = more -maxColumn = 100 +maxColumn = 200 +docstrings.wrapMaxColumn = 100 +docstrings.style = Asterisk +docstrings.oneline = keep + assumeStandardLibraryStripMargin = true align.stripMargin = true runner.dialect = Scala3 @@ -9,3 +14,6 @@ fileOverride { runner.dialect = scala3 } } +optIn.breakChainOnFirstMethodDot = false +rewrite.trailingCommas.style = keep + diff --git a/build.sbt b/build.sbt index 68e937e..17655f5 100644 --- a/build.sbt +++ b/build.sbt @@ -1,21 +1,57 @@ lazy val scala213 = "2.13.12" lazy val scala331 = "3.3.1" lazy val scalaVer = scala331 + lazy val supportedScalaVersions = List(scala213, scala331) //lazy val supportedScalaVersions = List(scalaVer) //ThisBuild / envFileName := "dev.env" // sbt-dotenv plugin gets build environment here -ThisBuild / organization := "org.vastblue" ThisBuild / scalaVersion := scalaVer -ThisBuild / version := "0.8.3-SNAPSHOT" +ThisBuild / version := "0.8.4-SNAPSHOT" -ThisBuild / crossScalaVersions := supportedScalaVersions +ThisBuild / organization := "org.vastblue" +ThisBuild / organizationName := "vastblue.org" +ThisBuild / organizationHomepage := Some(url("https://vastblue.org/")) + +ThisBuild / scmInfo := Some( + ScmInfo( + url("https://github.com/philwalk/pallet"), + "scm:git@github.com:philwalk/pallet.git" + ) +) -lazy val root = (project in file(".")) - .settings( - crossScalaVersions := supportedScalaVersions, - name := "pallet" +ThisBuild / developers.withRank(KeyRanks.Invisible) := List( + Developer( + id = "philwalk", + name = "Phil Walker", + email = "philwalk9@gmail.com", + url = url("https://github.com/philwalk") ) +) + +// Remove all additional repository other than Maven Central from POM +ThisBuild / publishTo := { + // For accounts created after Feb 2021: + val nexus = "https://s01.oss.sonatype.org/" + if (isSnapshot.value) Some("snapshots" at nexus + "content/repositories/snapshots") + else Some("releases" at nexus + "service/local/staging/deploy/maven2") +} + +ThisBuild / publishMavenStyle.withRank(KeyRanks.Invisible) := true + +ThisBuild / crossScalaVersions := supportedScalaVersions + +// For all Sonatype accounts created on or after February 2021 +ThisBuild / sonatypeCredentialHost := "s01.oss.sonatype.org" + +resolvers += Resolver.mavenLocal + +publishTo := sonatypePublishToBundle.value + +lazy val root = (project in file(".")).settings( + crossScalaVersions := supportedScalaVersions, + name := "pallet" +) libraryDependencies ++= Seq( "org.scalacheck" %% "scalacheck" % "1.17.0" % Test, @@ -23,6 +59,8 @@ libraryDependencies ++= Seq( "com.github.sbt" % "junit-interface" % "0.13.3" % Test ) +// If you created a new account on or after February 2021, add sonatypeCredentialHost settings: + /* * build.sbt * SemanticDB is enabled for all sub-projects via ThisBuild scope. diff --git a/jsrc/classpath b/jsrc/classpath new file mode 100644 index 0000000..eafdc2a --- /dev/null +++ b/jsrc/classpath @@ -0,0 +1,70 @@ +#!/usr/bin/env -S scala @./classpathAtfileWindows + +import vastblue.pathextend.* +import vastblue.pathextend.* + +lazy val (psep, cp) = { + val ps = sys.props("path.separator") + val clp = sys.props("java.class.path").split(ps) + (ps, clp) +} +lazy val homedir = sys.props("user.home").replace('\\', '/') +lazy val scriptName = sys.props("script.path").norm.replaceAll(".*/", "") + +def usage(msg: String=""): Nothing = { + if ( msg.nonEmpty ){ + printf("%s\n", msg) + } + printf("%s []\n", scriptName) + val info = Seq( + "-d ; list classpath directories", + "-j ; list classpath jars", + "-v ; verbose", + ) + for (s <- info){ + printf("%s\n", s) + } + sys.exit(0) +} + +// analyze classpath, selectively showing dirs, jars and bad entries +// usage: ${0##*/} [-dirs] [-jars]" +def main(args: Array[String]): Unit = + var verbose = false + var (dirs, jars) = (true, false) + args.indices.foreach { i => + args(i) match { + case "-v" => verbose = true + case "-d" | "-dirs" => dirs = !dirs + case "-j" | "-jars" => jars = !jars + case arg => + usage(s"unrecognized arg [$arg]") + } + } + if (!dirs && !jars && !verbose){ + usage() + } + if (verbose){ + printf("psep[%s]\n", psep) + } + for (e <- cp){ + val fname = if (e.startsWith("~")){ + e.replaceFirst("~",homedir) + } else { + e + } + if (verbose){ + printf("%s\n", fname.norm) + } + val p = java.nio.file.Paths.get(fname) + val isdir = p.toFile.isDirectory + val isfil = p.toFile.isFile + (isfil, isdir) match { + case (true, false) => + if (jars) printf("jar: %s\n", p.norm) + case (false, true) => + if (dirs) printf("dir: %s\n", p.norm) + case _ => + printf("%5s: %5s: %s\n", isdir, isfil, p.norm) + } + } diff --git a/jsrc/platform.sc b/jsrc/platform.sc index 6d4e516..c06a6d9 100644 --- a/jsrc/platform.sc +++ b/jsrc/platform.sc @@ -1,4 +1,4 @@ -#!/usr/bin/env -S scala @classpathAtfileClassesDir +#!/usr/bin/env -S scala @classpathAtfile // hashbang line requires an classpath @file, containing: // -cp target/scala-3.3.0/classes @@ -7,7 +7,11 @@ import vastblue.Platform def main(args: Array[String]): Unit = - Platform.main(args) + if (args.contains("-verbose")){ + Platform.main(args.filter { _ != "-verbose" }) + } + val cygdrivePrefix = Platform.reverseMountMap.get("cygdrive").getOrElse("not-found") + printf("cygdrivePrefix: [%s]\n", cygdrivePrefix) for ((k,v) <- Platform.mountMap){ printf("%-22s: %s\n", k, v) } diff --git a/jsrc/typeMinusAp.sc b/jsrc/typeMinusAp.sc new file mode 100644 index 0000000..447ebf6 --- /dev/null +++ b/jsrc/typeMinusAp.sc @@ -0,0 +1,34 @@ +#!/usr/bin/env -S scala @classpathAtfile + +import vastblue.pathextend.* +import vastblue.Platform.{getStdout, envPath, isWindows} +import scala.util.control.Breaks._ +import java.nio.file.{Files => JFiles, Paths => JPaths} + +def main(args: Array[String]): Unit = + for (arg <- args){ + val list = findAllInPath(arg) + printf("found %d [%s] in PATH:\n", list.size, arg) + for (path <- list) { + printf(" [%s] found at [%s]\n", arg, path.norm) + printf("--version: [%s]\n", getStdout(path.norm, "--version")) + } + } + +def fsep = java.io.File.separator +def exeSuffix: String = if (isWindows) ".exe" else "" + +def findAllInPath(prog: String): Seq[Path] = { + val progname = prog.replace('\\', '/').split("/").last // remove path, if present + var found = List.empty[Path] + for (dir <- envPath) { + // sort .exe suffix ahead of no .exe suffix + for (name <- Seq(s"$dir$fsep$progname$exeSuffix", s"$dir$fsep$progname").distinct) { + val p = JPaths.get(name) + if (p.toFile.isFile) { + found ::= p.normalize + } + } + } + found.reverse +} diff --git a/jsrc/where.sc b/jsrc/where.sc new file mode 100644 index 0000000..f5e0a61 --- /dev/null +++ b/jsrc/where.sc @@ -0,0 +1,6 @@ +#!/usr/bin/env -S scala +def main(args: Array[String]): Unit = { + import scala.sys.process._ + val whereExe = Seq("where.exe", "where").lazyLines_!.take(1).toList.mkString("").replace('\\', '/') + printf("whereExe[%s]\n", whereExe) +} diff --git a/project/plugins.sbt b/project/plugins.sbt index e83d653..9f5153c 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,2 +1,13 @@ -addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.5.11") -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.1") +val SONATYPE_VERSION = sys.env.getOrElse("SONATYPE_VERSION", "3.9.21") +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.5.11") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.1") +addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % SONATYPE_VERSION) +addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.2.1") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") +addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.0.1") +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") + +libraryDependencies += "org.scala-sbt" %% "scripted-plugin" % sbtVersion.value + +//resolvers += Resolver.sonatypeRepo("snapshots") +resolvers ++= Resolver.sonatypeOssRepos("snapshots") diff --git a/src/main/scala-2.13/vastblue/DriveColon.scala b/src/main/scala-2.13/vastblue/DriveColon.scala deleted file mode 100644 index 4aa6ef1..0000000 --- a/src/main/scala-2.13/vastblue/DriveColon.scala +++ /dev/null @@ -1,29 +0,0 @@ -package vastblue - -import vastblue.DriveColon._ - - -object DriveColon { - type DriveColon = String - - // empty string or uppercase "[A-Z]:" - def apply(s: String): DriveColon = { - require(s.length <= 2, s"bad DriveColon String [$s]") - val str: String = s match { - case dl if dl.matches("^[a-zA-Z]:") => dl.toUpperCase - case dl if dl.matches("^[a-zA-Z]") => s"$dl:".toUpperCase - case _ => "" - } - str - } - - implicit class DriveColonExtend(dl: DriveColon) { - def letter: String = dl.substring(0, 1).toLowerCase - def string: String = dl - def posix: String = s"/$letter" - def isEmpty: Boolean = dl.string.isEmpty - def isDrive: Boolean = !dl.isEmpty - } -} - - diff --git a/src/main/scala-2.13/vastblue/DriveRoot.scala b/src/main/scala-2.13/vastblue/DriveRoot.scala new file mode 100644 index 0000000..f2d4b3d --- /dev/null +++ b/src/main/scala-2.13/vastblue/DriveRoot.scala @@ -0,0 +1,40 @@ +package vastblue + +import java.nio.file.{Path, Paths} +import DriveRoot._ +import vastblue.Platform.cygdrive + +//opaque type DriveRoot = String + +// DriveRoot Strings must match "" or "[A-Z]:" +// The `toPath` method resolves path to root of disk, +// rather than the one returned by `Paths.get("C:")`. +object DriveRoot { + type DriveRoot = String + + // empty string or uppercase "[A-Z]:" + def apply(s: String): DriveRoot = { + require(s.length <= 2, s"bad DriveRoot String [$s]") + val str: String = s match { + case dl if dl.matches("^[a-zA-Z]:") => dl.toUpperCase + case dl if dl.matches("^[a-zA-Z]") => s"$dl:".toUpperCase + case _ => "" + } + str + } + + implicit class DriveRootExtend(dl: DriveRoot) { + def letter: String = dl.substring(0, 1).toLowerCase + def string: String = dl + def isEmpty: Boolean = string.isEmpty + def isDrive: Boolean = !isEmpty + def toPath: Path = Paths.get(dl.string + "/") + def workingDir: Path = Paths.get(dl.string).toAbsolutePath + + def posix: String = if (cygdrive.endsWith("/")) { + s"$cygdrive${letter}" + } else { + s"$cygdrive/$letter" // Platform.cygdrive might be "/" + } + } +} diff --git a/src/main/scala-2.13/vastblue/file/EzPath.scala b/src/main/scala-2.13/vastblue/file/EzPath.scala index 007ee01..0888384 100644 --- a/src/main/scala-2.13/vastblue/file/EzPath.scala +++ b/src/main/scala-2.13/vastblue/file/EzPath.scala @@ -41,7 +41,7 @@ class EzPath(val initstring: String, val sl: Slash) { } } object EzPath { - implicit class StExtend(s: String){ + implicit class StExtend(s: String) { def slash: String = s } // val winu = EzPath("c:\\opt", Unx) // valid @@ -80,10 +80,9 @@ object EzPath { def isWindows = !notWindows - def platformPrefix: String = Paths.get(".").toAbsolutePath.getRoot.toString match { case "/" => "" - case s => s.take(2) + case s => s.take(2) } def winlikePathstr(s: String): Boolean = { @@ -108,7 +107,7 @@ object EzPath { override def toString = abs } - implicit class PathExt(p: Path){ + implicit class PathExt(p: Path) { def slash(sl: Slash): String = { if (sl == Win) { p.toString.replace('/', '\\') diff --git a/src/main/scala-2.13/vastblue/pathextend.scala b/src/main/scala-2.13/vastblue/pathextend.scala index f7427fa..8a79a2a 100644 --- a/src/main/scala-2.13/vastblue/pathextend.scala +++ b/src/main/scala-2.13/vastblue/pathextend.scala @@ -9,11 +9,11 @@ import scala.jdk.CollectionConverters._ import vastblue.Platform._ import vastblue.time.FileTime import vastblue.time.FileTime._ -import vastblue.DriveColon._ +import vastblue.DriveRoot._ +// TODO: factor out code common to scala3 and scala2.13 versions object pathextend { def Paths = vastblue.file.Paths - //def Files = vastblue.file.Files type Path = java.nio.file.Path type PrintWriter = java.io.PrintWriter type JFile = java.io.File @@ -44,7 +44,7 @@ object pathextend { implicit class ExtendString(s: String) { def path: Path = vastblue.file.Paths.get(s) def toPath: Path = path - def absPath: Path = s.path.toAbsolutePath.normalize // alias + def absPath: Path = s.path.toAbsolutePath.normalize def toFile: JFile = toPath.toFile def file: JFile = toFile def norm: String = s.replace('\\', '/') @@ -52,45 +52,47 @@ object pathextend { } implicit class ExtendPath(p: Path) { - def toFile: JFile = p.toFile - def length: Long = p.toFile.length - def file: JFile = p.toFile - def realpath: Path = if (p.isSymbolicLink) { + def toFile: JFile = p.toFile + def length: Long = p.toFile.length + def file: JFile = p.toFile + + def realpath: Path = if (p.isSymbolicLink) { try { // p.toRealPath() // good symlinks JFiles.readSymbolicLink(p); } catch { - case fse: java.nio.file.FileSystemException => - p.realpathLs // bad symlinks, or file access permission + case fse: java.nio.file.FileSystemException => + p.realpathLs // bad symlinks, or file access permission } } else { p // not a symlink } def getParentFile: JFile = p.toFile.getParentFile - def parentFile: JFile = getParentFile // alias + def parentFile: JFile = getParentFile def parentPath: Path = parentFile.toPath - def parent: Path = parentPath // alias - def exists: Boolean = JFiles.exists(p) // p.toFile.exists + def parent: Path = parentPath + def exists: Boolean = JFiles.exists(p) def listFiles: Seq[JFile] = p.toFile.listFiles.toList def localpath: String = cygpath2driveletter(p.normalize.toString) def dospath: String = localpath.replace('/', '\\') def isDirectory: Boolean = p.toFile.isDirectory def isFile: Boolean = p.toFile.isFile - def isRegularFile: Boolean = isFile // alias + def isRegularFile: Boolean = isFile def relpath: Path = if (p.norm.startsWith(cwd.norm)) cwd.relativize(p) else p def relativePath: String = relpath.norm def getName: String = p.toFile.getName() - def name: String = p.toFile.getName // alias + def name: String = p.toFile.getName def lcname: String = name.toLowerCase def basename: String = p.toFile.basename def lcbasename: String = basename.toLowerCase def suffix: String = dotsuffix.dropWhile((c: Char) => c == '.') def lcsuffix: String = suffix.toLowerCase def dotsuffix: String = p.toFile.dotsuffix - def noDrive: String = - p.norm.replaceAll("^/?[A-Za-z]:?/", "/") // toss Windows drive letter, if present - def text: String = p.toFile.contentAsString // alias + // toss Windows drive letter, if present + def noDrive: String = p.norm.replaceAll("^/?[A-Za-z]:?/", "/") + + def text: String = p.toFile.contentAsString def extension: Option[String] = p.toFile.extension def pathFields = p.iterator.asScala.toList def reversePath: String = pathFields.reverse.mkString("/") @@ -132,7 +134,7 @@ object pathextend { def lines(encoding: String): Seq[String] = linesCharset(Charset.forName(encoding)) def linesCharset(charset: Charset): Seq[String] = { if (p.norm.startsWith("/proc/")) { - execBinary("cat", p.norm) + execBinary(catExe, p.norm) } else { try { JFiles.readAllLines(p, charset).asScala.toSeq @@ -146,12 +148,13 @@ object pathextend { def linesWithEncoding(encoding: String): Seq[String] = getLinesAnyEncoding(p, encoding) def firstline = p.linesAnyEncoding.take(1).mkString("") def getLinesIgnoreEncodingErrors(): Seq[String] = linesAnyEncoding - def contentAsString: String = p.toFile.contentAsString() + + def contentAsString: String = p.toFile.contentAsString() def contentWithEncoding(encoding: String): String = p.linesWithEncoding(encoding).mkString("\n") def contains(s: String): Boolean = p.toFile.contentAsString().contains(s) def contentAnyEncoding: String = p.toFile.contentAnyEncoding def bytes: Array[Byte] = JFiles.readAllBytes(p) - def byteArray: Array[Byte] = bytes // alias + def byteArray: Array[Byte] = bytes def ageInDays: Double = FileTime.ageInDays(p.toFile) def trimmedLines: Seq[String] = linesCharset(DefaultCharset).map { _.trim } @@ -191,24 +194,24 @@ object pathextend { } } - def abspath: String = norm // alias + def abspath: String = norm // output string should be posix format, either because: // A. non-Windows os // B. C: matching default drive is dropped // C. D: (not matching default drive) is converted to /d - def stdpath: String = { // alias + def stdpath: String = { // drop drive letter, if present val rawString = p.toString val posix = if (notWindows) { rawString // case A } else { val nm = norm - posixDriveLetter(nm) // case C + withPosixDriveLetter(nm) // case C } posix } - def posixpath: String = stdpath // alias + def posixpath: String = stdpath def delete(): Boolean = toFile.delete() def withWriter(charsetName: String = DefaultEncoding, append: Boolean = false)( func: PrintWriter => Any @@ -216,10 +219,6 @@ object pathextend { p.toFile.withWriter(charsetName, append)(func) } - /* - def overwrite(text: String): Unit = p.toFile.overwrite(text) - */ - def dateSuffix: String = { lcbasename match { case DatePattern1(_, yyyymmdd, _) => @@ -230,6 +229,7 @@ object pathextend { "" } } + def renameTo(s: String): Boolean = renameTo(s.path) def renameTo(alt: Path): Boolean = { p.toFile.renameTo(alt) @@ -243,12 +243,12 @@ object pathextend { implicit class ExtendFile(f: JFile) { def path = f.toPath def realfile: JFile = path.realpath.toFile - def name: String = f.getName // alias + def name: String = f.getName def lcname = f.getName.toLowerCase def norm: String = f.path.norm - def abspath: String = norm // alias - def stdpath: String = norm // alias - def posixpath: String = stdpath // alias + def abspath: String = norm + def stdpath: String = norm + def posixpath: String = stdpath def lastModifiedYMD: String = f.path.lastModifiedYMD def basename: String = dropDotSuffix(name) def lcbasename: String = basename.toLowerCase @@ -258,9 +258,9 @@ object pathextend { def extension: Option[String] = f.dotsuffix match { case "" => None; case str => Some(str) } def parentFile: JFile = f.getParentFile def parentPath: Path = parentFile.toPath - def parent: Path = parentPath // alias + def parent: Path = parentPath def isFile: Boolean = f.isFile - def isRegularFile: Boolean = isFile // alias + def isRegularFile: Boolean = isFile def filesTree: Seq[JFile] = { assert(f.isDirectory, s"not a directory [$f]") pathextend.filesTree(f)() @@ -272,9 +272,10 @@ object pathextend { } def contentAsString: String = contentAsString(DefaultCharset) def contentAsString(charset: Charset = DefaultCharset): String = f.lines(charset).mkString("\n") + def contentAnyEncoding: String = f.linesAnyEncoding.mkString("\n") def bytes: Array[Byte] = f.getBytes("UTF-8") // JFiles.readAllBytes(path) - def byteArray: Array[Byte] = bytes // alias + def byteArray: Array[Byte] = bytes def getBytes(encoding: String = "utf-8"): Array[Byte] = contentAsString.getBytes(Charset.forName(encoding)) def lines: Seq[String] = lines(DefaultCharset) @@ -394,12 +395,15 @@ object pathextend { def apply(fpath: String): JFile = new JFile(fpath) } - /** Recursive list of all files below rootfile. Filter for directories to be descended and/or - * files to be retained. - */ def dummyFilter(f: JFile): Boolean = f.canRead() import scala.annotation.tailrec + + /** + * Recursive list of all files below rootfile. + * + * Filter for directories to be descended and/or files to be retained. + */ def filesTree(dir: JFile)(func: JFile => Boolean = dummyFilter): Seq[JFile] = { assert(dir.isDirectory, s"error: not a directory [$dir]") @tailrec @@ -485,6 +489,8 @@ object pathextend { import java.nio.charset.CodingErrorAction var discardWarningAbsorber = Codec(encoding) implicit val codec = Codec(encoding) + + // scalafmt: { optIn.breakChainOnFirstMethodDot = true } discardWarningAbsorber = codec .onMalformedInput(CodingErrorAction.REPLACE) .onUnmappableCharacter(CodingErrorAction.REPLACE) @@ -554,38 +560,53 @@ object pathextend { def cygpath2driveletter(p: Path): String = { cygpath2driveletter(p.stdpath) } - // return a posix version of path string; include drive letter, if not the default drive - // TODO: this assumes that /etc/fstab defines "cygdrive" as "/" - def posixDriveLetter(str: String) = { - val posix = if (str.drop(1).startsWith(":")) { - val letter = str.take(1).toLowerCase - val tail = str.drop(2) - tail match { - case "/" => - s"/$letter" - case s if s.startsWith("/") => - if (letter == workingDrive.letter){ // take(1).toLowerCase) { - tail - } else { - s"/$letter$tail" - } - case _ => - s"/$letter/$tail" - } + + // return posix path string, with cygdrive prefix, if not the default drive + def withPosixDriveLetter(str: String) = { + if (notWindows) { + str } else { - if (workingDrive.isDrive && str.take(3).equalsIgnoreCase(s"/${workingDrive.letter}")) { - str.drop(2) // drop default drive + val posix = if (str.drop(1).startsWith(":")) { + val driveRoot = DriveRoot(str.take(2)) + str.drop(2).string match { + case "/" => + driveRoot.posix + case pathstr if pathstr.startsWith("/") => + if (driveRoot == workingDrive) { + pathstr // implicit drive prefix + } else { + s"${driveRoot.posix}$pathstr" // explicit drive prefix + } + case pathstr => + // Windows drive letter not followed by a slash resolves to + // the "current working directory" for the drive. + val cwd = driveRoot.workingDir.norm + s"$cwd/$pathstr" + } } else { - str + // if str prefix matches workingDrive.posix, remove it + if (!str.startsWith(cygdrive) || !workingDrive.isDrive) { + str // no change + } else { + val prefixLength = cygdrive.length + 3 // "/cygdrive" + "/c/" + if (str.take(prefixLength).equalsIgnoreCase(workingDrive.posix + "/")) { + // drop working drive prefix, for implicit root-relative path + str.drop(prefixLength - 1) // don't drop slash following workingDrive.posix prefix + } else { + str + } + } } + posix } - posix } + def dropDotSuffix(s: String): String = - if (!s.contains(".")) s else s.reverse.dropWhile(_ != '.').drop(1).reverse + if (!s.contains(".")) s else s.replaceFirst("[.][^.\\/]+$", "") + def commonLines(f1: Path, f2: Path): Map[String, List[String]] = { val items = (f1.trimmedLines ++ f2.trimmedLines).groupBy { line => - line.replaceAll("""[^a-zA-Z_0-9]+""", "") // remove whitespace and punctuation + line.replaceAll("""[^a-zA-Z_0-9]+""", "") // remove spaces and punctuation } items.map { case (key, items) => (key, items.toList) } } diff --git a/src/main/scala-3/vastblue/DriveColon.scala b/src/main/scala-3/vastblue/DriveColon.scala deleted file mode 100644 index cfab318..0000000 --- a/src/main/scala-3/vastblue/DriveColon.scala +++ /dev/null @@ -1,26 +0,0 @@ -package vastblue - -opaque type DriveColon = String - -// DriveColon Strings can only match "" or "[A-Z]:" -object DriveColon { - // empty string or uppercase "[A-Z]:" - def apply(s: String): DriveColon = { - require(s.length <= 2, s"bad DriveColon String [$s]") - val str: String = s match { - case dl if dl.matches("^[a-zA-Z]:") => dl.toUpperCase - case dl if dl.matches("^[a-zA-Z]") => s"$dl:".toUpperCase - case _ => "" - } - str - } - - extension (dl: DriveColon){ - def letter: String = dl.substring(0,1).toLowerCase - def string: String = dl - def posix: String = s"/$letter" - def isEmpty: Boolean = string.isEmpty - def isDrive: Boolean = !string.isEmpty - } -} - diff --git a/src/main/scala-3/vastblue/DriveRoot.scala b/src/main/scala-3/vastblue/DriveRoot.scala new file mode 100644 index 0000000..475a9a0 --- /dev/null +++ b/src/main/scala-3/vastblue/DriveRoot.scala @@ -0,0 +1,40 @@ +package vastblue + +import java.nio.file.{Path, Paths} +import DriveRoot._ +import vastblue.Platform.cygdrive + +opaque type DriveRoot = String + +// DriveRoot Strings must match "" or "[A-Z]:" +// The `toPath` method resolves path to root of disk, +// rather than the one returned by `Paths.get("C:")`. +object DriveRoot { + type DriveRoot = String + + // empty string or uppercase "[A-Z]:" + def apply(s: String): DriveRoot = { + require(s.length <= 2, s"bad DriveRoot String [$s]") + val str: String = s match { + case dl if dl.matches("^[a-zA-Z]:") => dl.toUpperCase + case dl if dl.matches("^[a-zA-Z]") => s"$dl:".toUpperCase + case _ => "" + } + str + } + + extension (dl: DriveRoot) { + def letter: String = dl.substring(0, 1).toLowerCase + def string: String = dl + def isEmpty: Boolean = string.isEmpty + def isDrive: Boolean = !isEmpty + def toPath: Path = Paths.get(dl.string + "/") + def workingDir: Path = Paths.get(dl.string).toAbsolutePath + + def posix: String = if (cygdrive.endsWith("/")) { + s"$cygdrive${letter}" + } else { + s"$cygdrive/$letter" // Platform.cygdrive might be "/" + } + } +} diff --git a/src/main/scala-3/vastblue/file/EzPath.scala b/src/main/scala-3/vastblue/file/EzPath.scala index 5c868d6..ac181a1 100644 --- a/src/main/scala-3/vastblue/file/EzPath.scala +++ b/src/main/scala-3/vastblue/file/EzPath.scala @@ -80,14 +80,14 @@ def defaultSlash(s: String): Slash = { if (winlikePathstr(s)) Slash.Win else Slash.Unx } -object PathUnx{ +object PathUnx { def apply(s: String): PathUnx = new PathUnx(s) } class PathUnx(s: String) extends EzPath(s, Slash.Unx) { override def toString = abs } -object PathWin{ +object PathWin { def apply(s: String): PathWin = new PathWin(s) } class PathWin(s: String) extends EzPath(s, Slash.Win) { diff --git a/src/main/scala-3/vastblue/pathextend.scala b/src/main/scala-3/vastblue/pathextend.scala index 19f5cae..07f4115 100644 --- a/src/main/scala-3/vastblue/pathextend.scala +++ b/src/main/scala-3/vastblue/pathextend.scala @@ -9,11 +9,11 @@ import scala.jdk.CollectionConverters.* import vastblue.Platform.* import vastblue.time.FileTime import vastblue.time.FileTime.* -import vastblue.DriveColon.* +import vastblue.DriveRoot.* +// TODO: factor out code common to scala3 and scala2.13 versions object pathextend { def Paths = vastblue.file.Paths - //def Files = vastblue.file.Files type Path = java.nio.file.Path type PrintWriter = java.io.PrintWriter type JFile = java.io.File @@ -44,7 +44,7 @@ object pathextend { extension (s: String) { def path: Path = vastblue.file.Paths.get(s) def toPath: Path = path - def absPath: Path = s.path.toAbsolutePath.normalize // alias + def absPath: Path = s.path.toAbsolutePath.normalize def toFile: JFile = toPath.toFile def file: JFile = toFile def norm: String = s.replace('\\', '/') @@ -52,45 +52,47 @@ object pathextend { } extension (p: Path) { - def toFile: JFile = p.toFile - def length: Long = p.toFile.length - def file: JFile = p.toFile - def realpath: Path = if (p.isSymbolicLink) { + def toFile: JFile = p.toFile + def length: Long = p.toFile.length + def file: JFile = p.toFile + + def realpath: Path = if (p.isSymbolicLink) { try { // p.toRealPath() // good symlinks JFiles.readSymbolicLink(p); } catch { - case fse: java.nio.file.FileSystemException => - p.realpathLs // bad symlinks, or file access permission + case fse: java.nio.file.FileSystemException => + p.realpathLs // bad symlinks, or file access permission } } else { p // not a symlink } def getParentFile: JFile = p.toFile.getParentFile - def parentFile: JFile = getParentFile // alias + def parentFile: JFile = getParentFile def parentPath: Path = parentFile.toPath - def parent: Path = parentPath // alias - def exists: Boolean = JFiles.exists(p) // p.toFile.exists + def parent: Path = parentPath + def exists: Boolean = JFiles.exists(p) def listFiles: Seq[JFile] = p.toFile.listFiles.toList def localpath: String = cygpath2driveletter(p.normalize.toString) def dospath: String = localpath.replace('/', '\\') def isDirectory: Boolean = p.toFile.isDirectory def isFile: Boolean = p.toFile.isFile - def isRegularFile: Boolean = isFile // alias + def isRegularFile: Boolean = isFile def relpath: Path = if (p.norm.startsWith(cwd.norm)) cwd.relativize(p) else p def relativePath: String = relpath.norm def getName: String = p.toFile.getName() - def name: String = p.toFile.getName // alias + def name: String = p.toFile.getName def lcname: String = name.toLowerCase def basename: String = p.toFile.basename def lcbasename: String = basename.toLowerCase def suffix: String = dotsuffix.dropWhile((c: Char) => c == '.') def lcsuffix: String = suffix.toLowerCase def dotsuffix: String = p.toFile.dotsuffix - def noDrive: String = - p.norm.replaceAll("^/?[A-Za-z]:?/", "/") // toss Windows drive letter, if present - def text: String = p.toFile.contentAsString // alias + // toss Windows drive letter, if present + def noDrive: String = p.norm.replaceAll("^/?[A-Za-z]:?/", "/") + + def text: String = p.toFile.contentAsString def extension: Option[String] = p.toFile.extension def pathFields = p.iterator.asScala.toList def reversePath: String = pathFields.reverse.mkString("/") @@ -132,7 +134,7 @@ object pathextend { def lines(encoding: String): Seq[String] = linesCharset(Charset.forName(encoding)) def linesCharset(charset: Charset): Seq[String] = { if (p.norm.startsWith("/proc/")) { - execBinary("cat", p.norm) + execBinary(catExe, p.norm) } else { try { JFiles.readAllLines(p, charset).asScala.toSeq @@ -146,12 +148,13 @@ object pathextend { def linesWithEncoding(encoding: String): Seq[String] = getLinesAnyEncoding(p, encoding) def firstline = p.linesAnyEncoding.take(1).mkString("") def getLinesIgnoreEncodingErrors(): Seq[String] = linesAnyEncoding - def contentAsString: String = p.toFile.contentAsString() + + def contentAsString: String = p.toFile.contentAsString() def contentWithEncoding(encoding: String): String = p.linesWithEncoding(encoding).mkString("\n") def contains(s: String): Boolean = p.toFile.contentAsString().contains(s) def contentAnyEncoding: String = p.toFile.contentAnyEncoding def bytes: Array[Byte] = JFiles.readAllBytes(p) - def byteArray: Array[Byte] = bytes // alias + def byteArray: Array[Byte] = bytes def ageInDays: Double = FileTime.ageInDays(p.toFile) def trimmedLines: Seq[String] = linesCharset(DefaultCharset).map { _.trim } @@ -191,24 +194,24 @@ object pathextend { } } - def abspath: String = norm // alias + def abspath: String = norm // output string should be posix format, either because: // A. non-Windows os // B. C: matching default drive is dropped - // C. D: (not matching default drive) is converted to /d (TODO: or /cygdrive/d) - def stdpath: String = { // alias + // C. D: (not matching default drive) is converted to /d + def stdpath: String = { // drop drive letter, if present val rawString = p.toString val posix = if (notWindows) { rawString // case A } else { val nm = norm - posixDriveLetter(nm) // case C + withPosixDriveLetter(nm) // case C } posix } - def posixpath: String = stdpath // alias + def posixpath: String = stdpath def delete(): Boolean = toFile.delete() def withWriter(charsetName: String = DefaultEncoding, append: Boolean = false)( func: PrintWriter => Any @@ -216,10 +219,6 @@ object pathextend { p.toFile.withWriter(charsetName, append)(func) } - /* - def overwrite(text: String): Unit = p.toFile.overwrite(text) - */ - def dateSuffix: String = { lcbasename match { case DatePattern1(_, yyyymmdd, _) => @@ -230,6 +229,7 @@ object pathextend { "" } } + def renameTo(s: String): Boolean = renameTo(s.path) def renameTo(alt: Path): Boolean = { p.toFile.renameTo(alt) @@ -243,12 +243,12 @@ object pathextend { extension (f: JFile) { def path = f.toPath def realfile: JFile = path.realpath.toFile - def name: String = f.getName // alias + def name: String = f.getName def lcname = f.getName.toLowerCase def norm: String = f.path.norm - def abspath: String = norm // alias - def stdpath: String = norm // alias - def posixpath: String = stdpath // alias + def abspath: String = norm + def stdpath: String = norm + def posixpath: String = stdpath def lastModifiedYMD: String = f.path.lastModifiedYMD def basename: String = dropDotSuffix(name) def lcbasename: String = basename.toLowerCase @@ -258,9 +258,9 @@ object pathextend { def extension: Option[String] = f.dotsuffix match { case "" => None; case str => Some(str) } def parentFile: JFile = f.getParentFile def parentPath: Path = parentFile.toPath - def parent: Path = parentPath // alias + def parent: Path = parentPath def isFile: Boolean = f.isFile - def isRegularFile: Boolean = isFile // alias + def isRegularFile: Boolean = isFile def filesTree: Seq[JFile] = { assert(f.isDirectory, s"not a directory [$f]") pathextend.filesTree(f)() @@ -272,9 +272,10 @@ object pathextend { } def contentAsString: String = contentAsString(DefaultCharset) def contentAsString(charset: Charset = DefaultCharset): String = f.lines(charset).mkString("\n") + def contentAnyEncoding: String = f.linesAnyEncoding.mkString("\n") def bytes: Array[Byte] = f.getBytes("UTF-8") // JFiles.readAllBytes(path) - def byteArray: Array[Byte] = bytes // alias + def byteArray: Array[Byte] = bytes def getBytes(encoding: String = "utf-8"): Array[Byte] = contentAsString.getBytes(Charset.forName(encoding)) def lines: Seq[String] = lines(DefaultCharset) @@ -394,12 +395,15 @@ object pathextend { def apply(fpath: String): JFile = new JFile(fpath) } - /** Recursive list of all files below rootfile. Filter for directories to be descended and/or - * files to be retained. - */ def dummyFilter(f: JFile): Boolean = f.canRead() import scala.annotation.tailrec + + /** + * Recursive list of all files below rootfile. + * + * Filter for directories to be descended and/or files to be retained. + */ def filesTree(dir: JFile)(func: JFile => Boolean = dummyFilter): Seq[JFile] = { assert(dir.isDirectory, s"error: not a directory [$dir]") @tailrec @@ -485,6 +489,8 @@ object pathextend { import java.nio.charset.CodingErrorAction var discardWarningAbsorber = Codec(encoding) implicit val codec = Codec(encoding) + + // scalafmt: { optIn.breakChainOnFirstMethodDot = true } discardWarningAbsorber = codec .onMalformedInput(CodingErrorAction.REPLACE) .onUnmappableCharacter(CodingErrorAction.REPLACE) @@ -554,38 +560,53 @@ object pathextend { def cygpath2driveletter(p: Path): String = { cygpath2driveletter(p.stdpath) } - // return a posix version of path string; include drive letter, if not the default drive - // TODO: this assumes that /etc/fstab defines "cygdrive" as "/" - def posixDriveLetter(str: String) = { - val posix = if (str.drop(1).startsWith(":")) { - val letter = str.take(1).toLowerCase - val tail = str.drop(2) - tail match { - case "/" => - s"/$letter" - case s if s.startsWith("/") => - if (letter == workingDrive.letter){ // take(1).toLowerCase) { - tail - } else { - s"/$letter$tail" - } - case _ => - s"/$letter/$tail" - } + + // return posix path string, with cygdrive prefix, if not the default drive + def withPosixDriveLetter(str: String) = { + if (notWindows) { + str } else { - if (workingDrive.isDrive && str.take(3).equalsIgnoreCase(s"/${workingDrive.letter}")) { - str.drop(2) // drop default drive + val posix = if (str.drop(1).startsWith(":")) { + val driveRoot = DriveRoot(str.take(2)) + str.drop(2).string match { + case "/" => + driveRoot.posix + case pathstr if pathstr.startsWith("/") => + if (driveRoot == workingDrive) { + pathstr // implicit drive prefix + } else { + s"${driveRoot.posix}$pathstr" // explicit drive prefix + } + case pathstr => + // Windows drive letter not followed by a slash resolves to + // the "current working directory" for the drive. + val cwd = driveRoot.workingDir.norm + s"$cwd/$pathstr" + } } else { - str + // if str prefix matches workingDrive.posix, remove it + if (!str.startsWith(cygdrive) || !workingDrive.isDrive) { + str // no change + } else { + val prefixLength = cygdrive.length + 3 // "/cygdrive" + "/c/" + if (str.take(prefixLength).equalsIgnoreCase(workingDrive.posix + "/")) { + // drop working drive prefix, for implicit root-relative path + str.drop(prefixLength - 1) // don't drop slash following workingDrive.posix prefix + } else { + str + } + } } + posix } - posix } + def dropDotSuffix(s: String): String = - if (!s.contains(".")) s else s.reverse.dropWhile(_ != '.').drop(1).reverse + if (!s.contains(".")) s else s.replaceFirst("[.][^.\\/]+$", "") + def commonLines(f1: Path, f2: Path): Map[String, List[String]] = { val items = (f1.trimmedLines ++ f2.trimmedLines).groupBy { line => - line.replaceAll("""[^a-zA-Z_0-9]+""", "") // remove whitespace and punctuation + line.replaceAll("""[^a-zA-Z_0-9]+""", "") // remove spaces and punctuation } items.map { case (key, items) => (key, items.toList) } } diff --git a/src/main/scala/vastblue/Platform.scala b/src/main/scala/vastblue/Platform.scala index 19724db..811c3a5 100644 --- a/src/main/scala/vastblue/Platform.scala +++ b/src/main/scala/vastblue/Platform.scala @@ -10,9 +10,7 @@ import scala.util.control.Breaks._ import java.io.{BufferedReader, FileReader} import scala.util.Using import scala.sys.process._ -import vastblue.DriveColon -import vastblue.DriveColon._ -import scala.sys.process._ +import vastblue.DriveRoot._ /* * Support for writing portable/posix scala scripts. @@ -46,48 +44,24 @@ import scala.sys.process._ */ object Platform { def main(args: Array[String]): Unit = { + printf("SYSTEMDRIVE: %s\n", envOrElse("SYSTEMDRIVE")) for (arg <- args) { val list = findAllInPath(arg) printf("found %d [%s] in PATH:\n", list.size, arg) for (path <- list) { - printf(" [%s] found at [%s]\n", arg, path) - printf("--version: [%s]\n", exec(path.toString, "--version").takeWhile(_ != '(')) + printf(" [%s] found at [%s]\n", arg, path.norm) + printf("--version: [%s]\n", getStdout(path.norm, "--version").take(1).mkString) } } val cwd = ".".path for ((p: Path) <- cwd.paths if p.isDirectory) { printf("%s\n", p.norm) } + for (line <- "/proc/meminfo".path.lines) { printf("%s\n", line) } - val prognames = Seq( - "basename", - "bash", - "cat", - "chgrp", - "chmod", - "chown", - "cksum", - "cp", - "curl", - "date", - "diff", - "env", - "file", - "find", - "git", - "gzip", - "head", - "hostname", - "ln", - "ls", - "md5sum", - "mkdir", - "nohup", - "uname" - ) for (progname <- prognames) { val prog = where(progname) printf("%-12s: %s\n", progname, prog) @@ -126,6 +100,32 @@ object Platform { } } } + lazy val prognames = Seq( + "basename", + "bash", + "cat", + "chgrp", + "chmod", + "chown", + "cksum", + "cp", + "curl", + "date", + "diff", + "env", + "file", + "find", + "git", + "gzip", + "head", + "hostname", + "ln", + "ls", + "md5sum", + "mkdir", + "nohup", + "uname" + ) def notWindows: Boolean = java.io.File.separator == "/" @@ -136,12 +136,13 @@ object Platform { def osName = sys.props("os.name") lazy val osType: String = osName.takeWhile(_ != ' ').toLowerCase match { - case "windows" => "windows" - case "linux" => "linux" + case "windows" => "windows" + case "linux" => "linux" case "mac os x" => "darwin" case other => sys.error(s"osType is [$other]") } + def javaHome = Option(sys.props("java.home")) match { case None => envOrElse("JAVA_HOME", "") case Some(path) => path @@ -168,32 +169,35 @@ object Platform { JPaths.get(path).toFile.isDirectory } -// lazy val programFilesX86: String = System.getenv("ProgramFiles(x86)") match { -// case other: String => other -// case null => "c:/Program Files (x86)" -// } - - lazy val home: Path = JPaths.get(sys.props("user.home")) -// lazy val debug: Boolean = JPaths.get(".debug").toFile.exists - lazy val verbose = Option(System.getenv("VERBY")) != None +// def cygPath: String = localPath("cygpath") +// def localPath(exeName: String): String = where(exeName) // def str2ascii(a:String): String = vastblue.util.StringExtras.str2ascii(a) -// lazy val _debug: Boolean = Option(System.getenv("DEBUG")) != None // def driveLetterColon = JPaths.get(".").toAbsolutePath.normalize.toString.take(2) -// lazy val winshellFlag = { -// isDirectory(realroot) && (isCygwin || isMsys64 || isMingw64 || isGitSdk64) -// } def isWinshell = isCygwin | isMsys64 | isMingw64 | isGitSdk64 | isGitbash + lazy val (shellDrive, shellBaseDir) = driveAndPath(shellRoot) + def driveAndPath(filepath: String) = { + filepath match { + case LetterPath(letter, path) => + (DriveRoot(letter), path) + case _ => + (DriveRoot(""), realrootfull) + } + } + lazy val LetterPath = """([a-zA-Z]):([$/a-zA-Z_0-9]*)""".r + // these must all be lowercase -// def defaultCygroot = "/cygwin64" -// def defaultMsysroot = "/msys64" -// def defaultMingwroot = "/mingw" -// def defaultGitsdkroot = "/git-sdk-64" -// def defaultGitbashroot = "/gitbash" +// def defaultCygroot = "/cygwin64" +// def defaultMsysroot = "/msys64" +// def defaultMingwroot = "/mingw" +// def defaultGitsdkroot = "/git-sdk-64" +// def defaultGitbashroot = "/gitbash" +// def defaultRtoolsroot = "/rtools42" +// def defaultAnacondaroot = "/ProgramData/anaconda3/Library" def uname(arg: String) = { val unamepath: String = where("uname") match { @@ -220,20 +224,22 @@ object Platform { def bashPath: Path = { val pathstr: String = where("bash") + val p = pathstr.path try { p.toRealPath() } catch { - case fse: FileSystemException => - p // no permission to follow link + case fse: FileSystemException => + p // no permission to follow link } } -// def cygPath: String = localPath("cygpath") -// def localPath(exeName: String): String = where(exeName) def execBinary(args: String*): Seq[String] = { Process(Array(args: _*)).lazyLines_! } + def shellExec(cmd: String): Seq[String] = { + execBinary(bashPath.norm, "-c", cmd) + } def spawnCmd(cmd: Seq[String], verbose: Boolean = false): (Int, List[String], List[String]) = { var (out, err) = (List[String](), List[String]()) @@ -245,32 +251,43 @@ object Platform { def toErr(str: String): Unit = { err ::= str - if (verbose) System.err.printf("stderr[%s]\n", str).asInstanceOf[Unit] + if (verbose) System.err.printf("stderr[%s]\n", str) } val exit = cmd ! ProcessLogger((o) => toOut(o), (e) => toErr(e)) (exit, out.reverse, err.reverse) } - def ignoreList = Set( + def exeFilterList = Set( + // intellij provides anemic Path; filter problematic versions of various Windows executables. + "C:/Users/philwalk/AppData/Local/Programs/MiKTeX/miktex/bin/x64/pdftotext.exe", "C:/ProgramData/anaconda3/Library/usr/bin/cygpath.exe", "C:/Windows/System32/bash.exe", - "C:/Windows/System32/find.exe" + "C:/Windows/System32/find.exe", ) + // def execShell(args: String*): Seq[String] = { // val cmd = bashPath.norm :: "-c" :: args.toList // execBinary(cmd: _*) // } + def exec(args: String*): String = { execBinary(args: _*).toList.mkString("") } - def exec3(prog: String, arg: String): String = { - val cmd = Seq(prog, arg) + /* + * capture stdout, discard stderr. + */ + def getStdout(prog: String, arg: String): Seq[String] = { + val cmd = Seq(prog, arg) + val (exit, out, err) = spawnCmd(cmd, verbose) - out.map { _.replace('\\', '/') }.filter { s => !ignoreList.contains(s) }.take(1).mkString("") + + out.map { _.replace('\\', '/') }.filter { s => + !exeFilterList.contains(s) + } } - // find first binaryName in PATH + // find binaryName in PATH def findInPath(binaryName: String): Option[Path] = { findAllInPath(binaryName, findAll = false) match { case Nil => None @@ -280,8 +297,8 @@ object Platform { // find all occurences of binaryName in PATH def findAllInPath(prog: String, findAll: Boolean = true): Seq[Path] = { - // val path = JPaths.get(prog) - val progname = prog.replace('\\', '/').split("/").last // remove path, if present + // isolate program name + val progname = prog.replace('\\', '/').split("/").last var found = List.empty[Path] breakable { for (dir <- envPath) { @@ -300,40 +317,24 @@ object Platform { found.reverse.distinct } - // this is quite slow, you probably should use `where(binaryName)` instead. -// def whichPath(binaryName: String): String = { -// if (isWindows) { -// def exeName = if (binaryName.endsWith(".exe")) { -// binaryName -// } else { -// s"$binaryName.exe" -// } -// def findFirst(binName: String): String = { -// execBinary("where", binName).headOption.getOrElse("") -// } -// findFirst(binaryName) match { -// case "" => findFirst(exeName) -// case pathstr => pathstr -// } -// } else { -// exec("which", binaryName) -// } -// } + lazy val whereExe = Seq("where.exe", "where").lazyLines_!.take(1).toList.mkString("").replace('\\', '/') + // cat is needed to read /proc/ files + lazy val catExe = Seq("where.exe", "cat").lazyLines_!.take(1).toList.mkString("").replace('\\', '/') // get path to binaryName via 'which.exe' or 'where' def where(binaryName: String): String = { if (isWindows) { // prefer binary with .exe extension, ceteris paribus val binName = setSuffix(binaryName) - // exec3 hides stderr complaint: `INFO: Could not find files for the given pattern(s)` - exec3("c:/Windows/System32/where.exe", binName).replace('\\', '/') + // getStdout hides stderr: INFO: Could not find files for the given pattern(s) + getStdout(whereExe, binName).take(1).mkString.replace('\\', '/') } else { exec("which", binaryName) } } - lazy val unamefull = uname("-a") // -s ? + lazy val unamefull = uname("-a") lazy val unameshort = unamefull.toLowerCase.replaceAll("[^a-z0-9].*", "") lazy val isCygwin = unameshort.toLowerCase.startsWith("cygwin") lazy val isMsys64 = unameshort.toLowerCase.startsWith("msys") @@ -341,6 +342,9 @@ object Platform { lazy val isGitSdk64 = unameshort.toLowerCase.startsWith("git-sdk") lazy val isGitbash = unameshort.toLowerCase.startsWith("gitbash") + lazy val home: Path = JPaths.get(sys.props("user.home")) + lazy val verbose = Option(System.getenv("VERBY")).nonEmpty + def listPossibleRootDirs(startDir: String): Seq[JFile] = { JPaths.get(startDir).toAbsolutePath.toFile match { case dir if dir.isDirectory => @@ -350,7 +354,7 @@ object Platform { "msys64", "git-sdk-64", "gitbash", - "MinGW" + "MinGW", ) dir.listFiles.toList.filter { f => f.isDirectory && defaultRootNames.exists { name => @@ -361,12 +365,12 @@ object Platform { Nil } } - + // root from the perspective of shell environment lazy val shellRoot: String = { if (notWindows) "/" else { - val guess = bashPath.norm.replaceFirst("/[^/]*exe$", "") //.replaceFirst("(/usr)?/bin/bash.*", "") + val guess = bashPath.norm.replaceFirst("/[^/]*exe$", "") val guessPath = JPaths.get(guess) // call JPaths.get here to avoid circular reference if (JFiles.isDirectory(guessPath)) { guess @@ -376,6 +380,7 @@ object Platform { } } } + def possibleWinshellRootDirs = { listPossibleRootDirs("/") ++ listPossibleRootDirs("/opt") } @@ -391,30 +396,30 @@ object Platform { case p => p.toString } } -// -// def rootFromBashPath: String = { -// val nb = norm(bashPath) -// val guess = nb.replaceFirst("/bin/bash.*", "") match { -// case str if str.endsWith("/usr") => -// str.substring(0, str.length - 4) -// case str => str -// } -// val guessPath = JPaths.get(guess) -// if (!JFiles.isDirectory(guessPath)) { -// sys.error(s"unable to determine winshell root dir in $osName") -// } -// if (JFiles.isDirectory(guessPath)) { -// guess -// } else { -// sys.error(s"unable to determine winshell root dir in $osName") -// } -// } def realrootfull: String = realroot lazy val mountMap = { fstabEntries.map { (e: FsEntry) => (e.posix -> e.dir) }.toMap } + lazy val ( + cygdrive: String, + posix2localMountMap: Map[String, String], + reverseMountMap: Map[String, String] + ) = { + def emptyMap = Map.empty[String, String] + if (notWindows || shellRoot.isEmpty) { + ("", emptyMap, emptyMap) + } else { + val mmap = mountMap.toList.map { case (k: String, v: String) => (k.toLowerCase -> v) }.toMap + val rmap = mountMap.toList.map { case (k: String, v: String) => (v.toLowerCase -> k) }.toMap + + val cygdrive = rmap.get("cygdrive").getOrElse("") + // to speed up map access, convert keys to lowercase + (cygdrive, mmap, rmap) + // readWinshellMounts + } + } case class FsEntry(dir: String, posix: String, ftype: String) { override def toString = "%-22s, %-18s, %s".format(dir, posix, ftype) @@ -444,66 +449,46 @@ object Platform { entries } - def pwd: Path = JPaths.get(".").toAbsolutePath + def currentWorkingDir(drive: DriveRoot): Path = { + JPaths.get(drive.string).toAbsolutePath + } lazy val cwd = pwd + def pwd: Path = JPaths.get(".").toAbsolutePath + def fsep = java.io.File.separator def psep = sys.props("path.separator") def cwdstr = pwd.toString.replace('\\', '/') -//opaque type DriveColon = String - -//extension (dl: DriveColon){ -// def letter: String = dl.substring(0,1).toLowerCase -// def string: String = dl -// def posix: String = s"/$letter" -// def isEmpty: Boolean = dl.string.isEmpty -// def nonEmpty: Boolean = !dl.isEmpty -//} -//// DriveColon Strings can only match "" or "[A-Z]:" -//object DriveColon { -// // empty string or uppercase "[A-Z]:" -// def apply(s: String): DriveColon = { -// require(s.length <= 2, s"bad DriveColon String [$s]") -// val str: String = s match { -// case dl if dl.matches("^[a-zA-Z]:") => dl.toUpperCase -// case dl if dl.matches("^[a-zA-Z]") => s"$dl:".toUpperCase -// case _ => "" -// } -// str -// } -//} - - lazy val workingDrive: DriveColon = DriveColon(cwdstr.take(2)) + lazy val workingDrive: DriveRoot = DriveRoot(cwdstr.take(2)) +//lazy val workingDrive = if (isWindows) cwdstr.replaceAll(":.*", ":") else "" - lazy val driveLetters: List[DriveColon] = { + lazy val driveLetters: List[DriveRoot] = { val values = mountMap.values.toList val letters = { for { dl <- values.map { _.take(2) } if dl.drop(1) == ":" - } yield DriveColon(dl) + } yield DriveRoot(dl) }.distinct letters } - lazy val cygpathExes = Seq( "c:/msys64/usr/bin/cygpath.exe", - "c:/cygwin64/bin/cygpath.exe" + "c:/cygwin64/bin/cygpath.exe", + "c:/rtools64/usr/bin/cygpath.exe", ) + lazy val cygpathExe: String = { - if (notWindows){ + if (notWindows) { "" } else { val cpexe = where("cygpath.exe") val cp = cpexe match { case "" => - cygpathExes - .find { s => - JPaths.get(s).toFile.isFile - } - .getOrElse(cpexe) + // scalafmt: { optIn.breakChainOnFirstMethodDot = false } + cygpathExes.find { s => JPaths.get(s).toFile.isFile }.getOrElse(cpexe) case f => f } @@ -526,11 +511,12 @@ object Platform { def realrootbare = if (notWindows) { realroot } else { - realroot.replaceFirst(s"^(?i)${workingDrive.string}", "") match { - case "" => - "/" - case str => - str + val noDriveLetter = realroot.replaceFirst(s"^(?i)${workingDrive.string}", "") + noDriveLetter match { + case "" => + "/" + case str => + str } } @@ -588,20 +574,20 @@ object Platform { canExist(path) && JFiles.isDirectory(path) } - def pathDriveletter(ps: String): DriveColon = { + def pathDriveletter(ps: String): DriveRoot = { ps.take(2) match { case str if str.drop(1) == ":" => - DriveColon(str.take(2)) + DriveRoot(str.take(2)) case _ => - DriveColon("") + DriveRoot("") } } - def pathDriveletter(p: Path): DriveColon = { + def pathDriveletter(p: Path): DriveRoot = { pathDriveletter(p.toAbsolutePath.toString) } def canExist(p: Path): Boolean = { - val pathdrive: DriveColon = pathDriveletter(p) + val pathdrive: DriveRoot = pathDriveletter(p) pathdrive.string match { case "" => true @@ -610,17 +596,6 @@ object Platform { } } - lazy val LetterPath = """([a-zA-Z]):([$/a-zA-Z_0-9]*)""".r - - def driveAndPath(filepath: String) = { - filepath match { - case LetterPath(letter, path) => - (DriveColon(letter), path) - case _ => - (DriveColon(""), realrootfull) - } - } - // fileExists() solves the Windows jvm problem that path.toFile.exists // is VEEERRRY slow for files on a non-existent drive (e.g., q:/). def fileExists(p: Path): Boolean = { @@ -707,27 +682,22 @@ object Platform { } localMountMap += "/cygdrive" -> cygdrive - /* - val driveLetters: Array[JFile] = { - if (false) { - java.io.File.listRoots() // veeery slow (potentially) - } else { - // 1000 times faster - val dlfiles = for { - locl <- localMountMap.values.toList - dl = locl.take(2) - if dl.drop(1) == ":" - ff = new JFile(s"$dl/") - } yield ff - dlfiles.distinct.toArray - } - } - */ + // // sometimes very slow + // java.io.File.listRoots() + + // // 1000 times faster + // val dlfiles = for { + // locl <- localMountMap.values.toList + // dl = locl.take(2) + // if dl.drop(1) == ":" + // ff = new JFile(s"$dl/") + // } yield ff + // dlfiles.distinct.toArray for (drive <- driveLetters) { // lowercase posix drive letter, e.g. "C:" ==> "/c" val letter = drive.string.toLowerCase // .take(1).toLowerCase - // winpath preserves uppercase DriveColon (cygpath.exe behavior) + // winpath preserves uppercase DriveRoot (cygpath.exe behavior) val winpath = stdpath(s"$drive/".path.toAbsolutePath) localMountMap += s"/$letter" -> winpath } @@ -736,6 +706,8 @@ object Platform { (localMountMap, cd2r) } + lazy val cygdrivePrefix = reverseMountMap.get("cygdrive").getOrElse("") + def eprint(xs: Any*): Unit = { System.err.print("%s".format(xs: _*)) } diff --git a/src/main/scala/vastblue/file/Paths.scala b/src/main/scala/vastblue/file/Paths.scala index 1b86c26..3323125 100644 --- a/src/main/scala/vastblue/file/Paths.scala +++ b/src/main/scala/vastblue/file/Paths.scala @@ -1,7 +1,6 @@ package vastblue.file -import vastblue.Platform._ // mountMap -import vastblue.pathextend.hook +import vastblue.Platform._ // mountMap import java.io.{File => JFile} import java.nio.file.{Path => JPath} @@ -11,6 +10,7 @@ import scala.util.control.Breaks._ import java.io.{BufferedReader, FileReader} import scala.util.Using import scala.sys.process._ +import vastblue.pathextend.hook /* * Enable access to the synthetic winshell filesystem provided by @@ -85,7 +85,7 @@ object Paths { } else { val _normpath = pathstr.replace('\\', '/') val normpath = _normpath.take(2) match { - case dl if dl.head == '/' => + case dl if dl.startsWith("/") => // apply mount map to paths with leading slash applyPosix2LocalMount(_normpath) // becomes absolute, if mounted case _ => @@ -111,9 +111,7 @@ object Paths { if (literalDrive.nonEmpty) { // no need for cygpath if drive is unambiguous. val fpath = - if ( - fpstr.endsWith(":") && fpstr.take(3).length == 2 && fpstr.equalsIgnoreCase(driveRoot) - ) { + if (fpstr.endsWith(":") && fpstr.take(3).length == 2 && fpstr.equalsIgnoreCase(driveRoot)) { // fpstr is a drive letter expression. // Windows interprets a bare drive letter expression as // the "working directory" each drive had at jvm startup. @@ -127,6 +125,7 @@ object Paths { } } } + lazy val posix2localMountMapKeyset = posix2localMountMap.keySet.toSeq.sortBy { -_.length } /* @@ -154,7 +153,7 @@ object Paths { } } val prefixMatches = lcpath.startsWith(target) - if (prefixMatches){ + if (prefixMatches) { hook += 1 } prefixMatches && exactMatch @@ -166,18 +165,17 @@ object Paths { */ def applyPosix2LocalMount(pathstr: String): String = { require(pathstr.take(2).last != ':', s"bad argument : ${pathstr}") - val segments = pathstr.drop(1).split("/") - require(segments.nonEmpty, s"empty segments for pathstr [$pathstr]") - val firstSeg = segments.head match { - case "/" | "" => "" - case s => s - } val mounted = getMounted(pathstr) val mountTarget = if (mounted.isEmpty) { pathstr } else { - if (mounted == Some(cygdrive)) { + val cyg = s"$cygdrive" + val mountpoint = mounted.getOrElse("") + if (mountpoint == cyg) { + val segments = pathstr.drop(cyg.length).split("/").dropWhile(_.isEmpty) + require(segments.nonEmpty, s"empty segments for pathstr [$pathstr]") + val firstSeg = segments.head if (firstSeg.length == 1 && isAlpha(firstSeg.charAt(0))) { // looks like a cygdrive designator replace '/cygdrive/X' with 'X:/' s"$firstSeg:/${segments.tail.mkString("/")}" @@ -225,6 +223,7 @@ object Paths { } nup } + def canBeDriveLetter(s: String): Boolean = { s.length == 1 && isAlpha(s.charAt(0)) } @@ -238,6 +237,7 @@ object Paths { } JPaths.get(fnamestr) } + def derefTilde(str: String): String = if (str.startsWith("~")) { // val uh = userhome s"${userhome}${str.drop(1)}".replace('\\', '/') @@ -262,7 +262,7 @@ object Paths { } } -// // in Windows 10+, per-directory case-sensitive filesystem is enabled or not. +// in Windows 10+, per-directory case-sensitive filesystem is enabled or not. // def dirIsCaseSensitive(p: Path): Boolean = { // val s = p.toString.replace('\\', '/') // val cmd = Seq("fsutil.exe", "file", "queryCaseSensitiveInfo", s) @@ -311,180 +311,14 @@ object Paths { def userhome: String = sys.props("user.home").replace('\\', '/') lazy val home: Path = Paths.get(userhome) - /* - lazy val programFilesX86: String = Option(System.getenv("ProgramFiles(x86)")) match { - case Some(valu) => valu - case None => "c:/Program Files (x86)" - } - - lazy val winshellFlag = { - isDirectory(shellBaseDir) && (isCygwin || isMsys64 || isGitSdk64) - } - // get first path to prog by searching the PATH - def findInPath(binaryName: String): Option[Path] = { - findAllInPath(binaryName, findAll = false) match { - case Nil => None - case head :: tail => Some(head) - } - } - - // get all occurences of binaryName in PATH - def findAllInPath(prog: String, findAll: Boolean = true): Seq[Path] = { - val progname = prog.replace('\\', '/') match { - case "/" => "/" - case str => str.split("/").last // remove path from program, if present - } - var found = List.empty[Path] - breakable { - for (dir <- envPath) { - // sort .exe suffix ahead of no .exe suffix - for (name <- Seq(s"$dir$fsep$progname$exeSuffix", s"$dir$fsep$progname").distinct) { - val p = Paths.get(name) - if (p.toFile.isFile) { - found ::= p.normalize - if (!findAll) { - break() // quit on first one - } - } - } - } - } - found.reverse.distinct - } - - def spawnCmd(cmd: Seq[String], verbose: Boolean = false): (Int, List[String], List[String]) = { - var (out, err) = (List[String](), List[String]()) - - def toOut(str: String): Unit = { - if (verbose) printf("stdout[%s]\n", str) - out ::= str - } - - def toErr(str: String): Unit = { - err ::= str - if (verbose) System.err.printf("stderr[%s]\n", str).asInstanceOf[Unit] - } - val exit = cmd ! ProcessLogger((o) => toOut(o), (e) => toErr(e)) - (exit, out.reverse, err.reverse) - } - -// def shellExec(cmd: String): Seq[String] = { -// execBinary(bashPath, "-c", cmd) -// } - - def execBinary(args: String*): Seq[String] = { - val (err, stdout, stderr) = spawnCmd(args.toSeq) - if (err != 0) { - hook += 1 - } - stdout - } - - lazy val winshellBashExes = Seq( - "c:/msys64/usr/bin/bash.exe", - "c:/cygwin64/bin/bash.exe" - ) - def bashPath: String = { - val inPath = where(s"bash${exeSuffix}") - if (!isWindows) { inPath } - else { - inPath match { - case "" | "C:/Windows/System32/bash.exe" => - winshellBashExes - .find { (s: String) => - JPaths.get(s).toFile.isFile - } - .getOrElse(inPath) - case s => - s - } - } - } - -// lazy val shellRootPath = Paths.get(shellRoot) - - def exec(args: String*): String = { - execBinary(args: _*).toList.mkString("") - } - // get path to binaryName via 'which.exe' or 'where' - def where(binaryName: String): String = { - if (isWindows) { - // prefer binary with .exe extension, ceteris paribus - val binName = setSuffix(binaryName) - // exec3 hides stderr complaint: `INFO: Could not find files for the given pattern(s)` - exec3("c:/Windows/System32/where.exe", binName).replace('\\', '/') - } else { - exec("which", binaryName) - } - } - - def notWindows: Boolean = java.io.File.separator == "/" - def isWindows: Boolean = !notWindows - def exeSuffix: String = if (isWindows) ".exe" else "" - - def osName = sys.props("os.name") - lazy val osType: String = osName.takeWhile(_ != ' ').toLowerCase match { - case "windows" => "windows" - case "linux" => "linux" - case "mac os x" => "darwin" - case other => - sys.error(s"osType is [$other]") - } - - lazy val unamefull = uname("-a") // -s ? - lazy val unameshort = unamefull.toLowerCase.replaceAll("[^a-z0-9].*", "") - lazy val isCygwin = unameshort.toLowerCase.startsWith("cygwin") - lazy val isMsys64 = unameshort.toLowerCase.startsWith("msys") - lazy val isMingw64 = unameshort.toLowerCase.startsWith("mingw") - lazy val isGitSdk64 = unameshort.toLowerCase.startsWith("git-sdk") - lazy val isGitbash = unameshort.toLowerCase.startsWith("gitbash") - - def listPossibleRootDirs(startDir: String): Seq[JFile] = { - Paths.get(startDir).toAbsolutePath.toFile match { - case dir if dir.isDirectory => - // NOTE: /opt/gitbash is excluded by this approach: - def defaultRootNames = Seq( - "cygwin64", - "msys64", - "git-sdk-64", - "gitbash", - "MinGW" - ) - dir.listFiles.toList.filter { f => - f.isDirectory && defaultRootNames.exists { name => - f.getName.contains(name) - } - } - case path => - Nil - } - } - def possibleWinshellRootDirs = { - listPossibleRootDirs("/") ++ listPossibleRootDirs("/opt") - } - - lazy val envPath: Seq[String] = Option(System.getenv("PATH")) match { - case None => Nil - case Some(str) => str.split(psep).toList.map { canonical(_) }.distinct - } - - def canonical(str: String): String = { - Paths.get(str) match { - case p if p.toFile.exists => p.normalize.toString - case p => p.toString - } - } - */ - def findPath(prog: String, dirs: Seq[String] = envPath): String = { - dirs - .map { dir => - Paths.get(s"$dir/$prog") - } - .find { (p: Path) => - p.toFile.isFile - } match { - case None => "" + // format: off + dirs.map { dir => + Paths.get(s"$dir/$prog") + }.find { (p: Path) => + p.toFile.isFile + } match { + case None => "" case Some(p) => p.normalize.toString.replace('\\', '/') } } @@ -528,11 +362,10 @@ object Paths { pathDriveletter(p.toAbsolutePath.toFile.toString) } - // fileExists() solves the Windows jvm problem that path.toFile.exists - // is VEEERRRY slow for files on a non-existent drive (e.g., q:/). + // path.toFile.exists very slow if drive not found, fileExists() is faster. def fileExists(p: Path): Boolean = { canExist(p) && - p.toFile.exists + p.toFile.exists } def exists(path: String): Boolean = { exists(Paths.get(path)) @@ -541,7 +374,7 @@ object Paths { canExist(p) && { p.toFile match { case f if f.isDirectory => true - case f => f.exists + case f => f.exists } } } @@ -564,23 +397,6 @@ object Paths { def norm(str: String) = str.replace('\\', '/') // Paths.get(str).normalize.toString.replace('\\', '/') - lazy val ( - cygdrive: String, - posix2localMountMap: Map[String, String], - reverseMountMap: Map[String, String] - ) = { - def emptyMap = Map.empty[String, String] - if (notWindows || shellRoot.isEmpty) { - ("", emptyMap, emptyMap) - } else { - val mmap = mountMap.toList.map { case (k: String, v: String) => (k.toLowerCase -> v) }.toMap - val rmap = mountMap.toList.map { case (k: String, v: String) => (v.toLowerCase -> k) }.toMap - val cygdrive = rmap.get("cygdrive").getOrElse("") - // to speed up map access, convert keys to lowercase - (cygdrive, mmap, rmap) - // readWinshellMounts - } - } // lazy val mountMap: Map[String, String] = reverseMountMap.map { (k: String, v: String) => (v -> k)} // lazy val cygdrive = mountMap.get("/cygdrive").getOrElse("/cygdrive") diff --git a/src/main/scala/vastblue/time/FileTime.scala b/src/main/scala/vastblue/time/FileTime.scala index 82a81d8..aac76e5 100644 --- a/src/main/scala/vastblue/time/FileTime.scala +++ b/src/main/scala/vastblue/time/FileTime.scala @@ -66,16 +66,17 @@ object FileTime extends vastblue.time.TimeExtensions { dtf } - /** Get a diff between two dates - * @param date1 - * the oldest date - * @param date2 - * the newest date - * @param timeUnit - * the unit in which you want the diff - * @return - * the diff value, in the provided unit - */ + /** + * Get a diff between two dates + * @param date1 + * the oldest date + * @param date2 + * the newest date + * @param timeUnit + * the unit in which you want the diff + * @return + * the diff value, in the provided unit + */ def diffDays(date1: LocalDateTime, date2: LocalDateTime): Long = { diff(date1, date2, TimeUnit.DAYS) } @@ -180,7 +181,7 @@ object FileTime extends vastblue.time.TimeExtensions { replaceAll(""" (\d):""", " 0$1:") . // make sure all time fields are 2 digits (zero filled) replaceAll("\\s+", " ") - .trim // compress random whitespace to a single space, then trim + .trim // compress random spaces to a single space, then trim val pattern = ( format != "", @@ -402,6 +403,7 @@ object FileTime extends vastblue.time.TimeExtensions { """(\d{4})\D(\d{1,2})\D(\d{1,2})\D(\d{2}):(\d{2}):(\d{2})""".r lazy val validYearPattern = """(1|2)\d{3}""" // only consider years between 1000 and 2999 def parseDateString(_datestr: String): LocalDateTime = { + // scalafmt: { optIn.breakChainOnFirstMethodDot = true } var datestr = _datestr .replaceAll("/", "-") .replaceAll("#", "") diff --git a/src/test/scala/TestUniPath.scala b/src/test/scala/TestUniPath.scala index c39715a..41a7e94 100644 --- a/src/test/scala/TestUniPath.scala +++ b/src/test/scala/TestUniPath.scala @@ -1,8 +1,8 @@ import org.junit.Test -import vastblue.Platform.isWinshell -import vastblue.file.Paths._ -import vastblue.Platform._ +//import org.junit.Assert.* +//import vastblue.file.Paths._ import vastblue.pathextend._ +import vastblue.Platform._ // isWinshell class TestUniPath { def testArgs = Seq.empty[String] diff --git a/src/test/scala/vastblue/file/EzPathTest.scala b/src/test/scala/vastblue/file/EzPathTest.scala index 0d4fede..6754efc 100644 --- a/src/test/scala/vastblue/file/EzPathTest.scala +++ b/src/test/scala/vastblue/file/EzPathTest.scala @@ -10,12 +10,12 @@ import org.scalatest.matchers.should.Matchers class EzPathTest extends AnyFunSpec with Matchers with BeforeAndAfter { describe("EzPath constructors") { it("should correctly create and display EzPath objects") { - val upathstr = "/opt/ue" - val wpathstr = upathstr.replace('/', '\\') - val posixAbsstr = s"$platformPrefix$upathstr" // can insert current working directory prefix - val windowsAbsstr = - posixAbsstr.replace('/', '\\') // windows version of java.io.File.separator - val localAbsstr = if (isWindows) windowsAbsstr else posixAbsstr + val upathstr = "/opt/ue" + val wpathstr = upathstr.replace('/', '\\') + val posixAbsstr = s"$platformPrefix$upathstr" // current working directory prefix + val windowsAbsstr = posixAbsstr.replace('/', '\\') // Windows version + val localAbsstr = if (isWindows) windowsAbsstr else posixAbsstr + printf("notWindows: %s\n", notWindows) printf("isWindows: %s\n", isWindows) diff --git a/src/test/scala/vastblue/file/FileSpec.scala b/src/test/scala/vastblue/file/FileSpec.scala deleted file mode 100644 index da2fb02..0000000 --- a/src/test/scala/vastblue/file/FileSpec.scala +++ /dev/null @@ -1,300 +0,0 @@ -package vastblue.file - -import vastblue.pathextend._ -import org.scalatest._ -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers -import vastblue.Platform.{workingDrive, driveRoot, cwd} - -class FileSpec extends AnyFunSpec with Matchers with BeforeAndAfter { - var verbose = false // manual control - lazy val TMP = { - val gdir = Paths.get("/g") - // val str = gdir.localpath - gdir.isDirectory && gdir.paths.nonEmpty match { - case true => - "/g/tmp" - case false => - "/tmp" - } - } - - /** similar to gnu 'touch '. - */ - def touch(p: Path): Int = { - var exitCode = 0 - try { - p.toFile.createNewFile() - } catch { - case _: Exception => - exitCode = 17 - } - exitCode - } - def touch(file: String): Int = { - touch(file.toPath) - } - lazy val testFile: Path = { - val fnamestr = s"${TMP}/youMayDeleteThisDebrisFile.txt" - isWindows match { - case true => - Paths.get(fnamestr) - case false => - Paths.get(fnamestr) - } - } - lazy val maxLines = 10 - lazy val testFileLines = (0 until maxLines).toList.map { _.toString } - - lazy val testfilename = "~/shellExecFileTest.out" - lazy val testfileb = { - val p = Paths.get(testfilename) - touch(p) - p - } - lazy val here = cwd.normalize.toString.toLowerCase - lazy val uhere = here.replaceAll("[a-zA-Z]:", "").replace('\\', '/') - lazy val hereDrive = here.replaceAll(":.*", ":") match { - case drive if drive >= "a" && drive <= "z" => - drive - case _ => "" - } - lazy val gdrive = "g:/".path - lazy val gdriveTests = - if (gdrive.exists) { // should NOT really be a function of whether driver exists! - List( - ("/g", "g:\\"), - ("/g/", "g:\\") - ) - } else { - List( - ("/g", "g:\\"), - ("/g/", "g:\\") - ) - } - lazy val expectedHomeDir = sys.props("user.home").replaceAll("/", "\\") - lazy val fileDospathPairs = List( - (".", here), - (hereDrive, here), // jvm treats this as cwd, if on c: - ("/q/", "q:\\"), // assumes /etc/fstab mounts /cygdrive to / - ("/q", "q:\\"), // assumes /etc/fstab mounts /cygdrive to / - ("/c/", "c:\\"), - ("~", expectedHomeDir), - ("~/", expectedHomeDir), - ("/g", "g:\\"), - ("/g/", "g:\\"), - ("/c/data/", "c:\\data") - ) ::: gdriveTests - - lazy val nonCanonicalDefaultDrive = { - val dd = driveRoot.take(1).toLowerCase - dd != "c" - } - lazy val username = sys.props("user.name").toLowerCase - lazy val toStringPairs = List( - (".", uhere), - ("/q/", "/q"), - ("/q/file", "/q/file"), // assumes there is no Q: drive - (hereDrive, uhere), // jvm treats bare drive letter as cwd, if default drive - ("/c/", "/c"), - ("~", s"/users/${username}"), - ("~/", s"/users/${username}"), - ("/g", "/g"), - ("/g/", "/g"), - ("/c/data/", "/data") - ) - - before { - // vastblue.fileutils.touch(testfilename) - testFile.withWriter() { (w: PrintWriter) => - testFileLines.foreach { line => - w.print(line + "\n") - } - } - } -// after { -//// if( testFile.exists ) testFile.delete() -//// if( testfileb.exists ) testfileb.delete() -// } - - describe("File") { - describe("#eachline") { - // def parseCsvLine(line:String,columnTypes:String,delimiter:String="") = { - it("should correctly deliver all file lines") { - // val lines = testFile.lines - System.out.printf("testFile[%s]\n", testFile) - for ((line, lnum) <- testFile.lines.toSeq.zipWithIndex) { - val expected = testFileLines(lnum) - if (line != expected) { - println(s"line ${lnum}:\n [$line]\n [$expected]") - } - } - for ((line, lnum) <- testFile.lines.toSeq.zipWithIndex) { - val expected = testFileLines(lnum) - if (line != expected) { - println(s"failure: line ${lnum}:\n [$line]\n [$expected]") - } else { - println(s"success: line ${lnum}:\n [$line]\n [$expected]") - } - assert(line == expected, s"line ${lnum}:\n [$line]\n [$expected]") - } - } - } - - describe("#tilde-in-path-test") { - it("should see file in user home directory if present") { - val ok = testfileb.exists - if (ok) println(s"tilde successfully converted to path '$testfileb'") - assert(ok, s"error: cannot see file '$testfileb'") - } - it("should NOT see file in user home directory if NOT present") { - testfileb.delete() - val ok = !testfileb.exists - if (ok) - println( - s"delete() successfull, and correctly detected by 'exists' method on path '$testfileb'" - ) - assert(ok, s"error: can still see file '$testfileb'") - } - } - // expected values of stdpath and localpath depend - // on whether g:/ exists. - // If so, c:/g is expected to resolve to /g and g:\\ -// lazy val (gu,gw) = os.dirExists("g:/") match { -// case true => ("/g", "g:\\") -// case false => ("/c/g","c:\\g") -// } - if (isWindows) { - printf("gdrive.exists: %s\n", gdrive.exists) - printf("gdrive.isDirectory: %s\n", gdrive.isDirectory) - printf("gdrive.isRegularFileg: %s\n", gdrive.isDirectory) - printf("gdrive.isSymbolicLink: %s\n", gdrive.isSymbolicLink) - describe("# dospath test") { - it("should correctly handle cygwin dospath drive designations, when present") { - var loop = -1 - for ((fname, expected) <- fileDospathPairs) { - loop += 1 - printf("fname[%s], expected[%s]\n", fname, expected) - val file = Paths.get(fname) - printf("%-22s : %s\n", file.stdpath, file.exists) - val a = expected.toLowerCase - // val b = file.toString.toLowerCase - // val c = file.localpath.toLowerCase - val d = file.dospath.toLowerCase - if (a == d) { - println(s"a [$a] == d [$d]") - assert(a == d) - } else { - printf("expected[%s]\n", expected.toLowerCase) - printf("file.localpath[%s]\n", file.localpath.toLowerCase) - printf( - "error: expected[%s] not equal to dospath [%s]\n", - expected.toLowerCase, - file.localpath.toLowerCase - ) - if (file.exists && new JFile(expected).exists) { - assert(a == d) - } else { - println(s"file.exists and expected.exists: [$file] == d [$expected]") - } - } - } - } - } - describe("# toString test") { - it("should correctly handle toString") { - val upairs = toStringPairs.toArray.toSeq - printf("%d pairs\n", upairs.size) - for ((fname, expected) <- upairs) { - if (true || verbose) { - printf("=====================\n") - printf("fname[%s]\n", fname) - printf("expec[%s]\n", expected) - } - val file: Path = Paths.get(fname) - printf("file.norm[%-22s] : %s\n", file.norm, file.exists) - printf("file.stdpath[%-22s] : %s\n", file.stdpath, file.exists) - val exp = expected.toLowerCase - val std = file.stdpath.toLowerCase - val nrm = file.norm.toLowerCase - printf("exp[%s] : std[%s] : nrm[%s]\n", exp, std, nrm) - // val c = file.localpath.toLowerCase - // val d = file.dospath.toLowerCase - if (!std.endsWith(exp)) { - printf("error: toString[%s] doesn't end with expected[%s]\n", nrm, exp) - } - // note: in some cases (on Windows, for semi-absolute paths not on the default drive), the `stdpath` version` - // of the path must include a `cygdrive` version of the drive letter. This test is more subtle in order to - // recognize this case. - if (nonCanonicalDefaultDrive) { - printf("hereDrive[%s]\n", hereDrive) - if (std.endsWith(exp)) { - println(s"std[$std].endsWith(exp[$exp]) for hereDrive[$hereDrive]"); - } - assert(std.endsWith(exp)) - } else { - if (exp == std) { - println(s"std[$std] == exp[$exp]") - } else { - hook += 1 - } - if (driveRoot.nonEmpty) { - assert(exp == std) // || exp.drop(2) == std.drop(2) || std.contains(exp)) - } else { - assert(exp == std) - } - } - - } - } - } - - def getVariants(p: Path): Seq[Path] = { - val pstr = p.toString.toLowerCase - import vastblue.DriveColon._ - def includeStdpath: Seq[String] = if (pstr.startsWith(workingDrive.string)) { - List(p.stdpath) - } else { - Nil - } - - val variants: Seq[String] = List( - p.norm, - p.toString, - p.localpath, - p.dospath - ) ++ includeStdpath // stdpath fails round-trip test when default drive != C: - - val vlist = variants.distinct.map { s => - val p = Paths.get(s) - if (p.toString.take(1).toLowerCase != pstr.take(1)) { - hook += 1 - } - p - } - vlist.distinct - } - describe("# File name consistency") { - it("round trip conversions should be consistent") { - for ( - fname <- - (toStringPairs.toMap.keySet ++ fileDospathPairs.toMap.keySet).toList.distinct.sorted - ) { - val f1: Path = Paths.get(fname) - val variants: Seq[Path] = getVariants(f1) - for (v <- variants) { // not necessarily 4 variants (duplicates removed before map to Path) - // val (k1,k2) = (f1.key,v.key) - if (f1 != v) { - printf("f1[%s]\nv[%s]\n", f1, v) - } - if (f1.equals(v)) { - println(s"f1[$f1] == v[$v]") - } - assert(f1.equals(v), s"f1[$f1] != variant v[$v]") - } - } - } - } - } - } -} diff --git a/src/test/scala/vastblue/file/PathSpec.scala b/src/test/scala/vastblue/file/PathSpec.scala index ba9e696..dade9a9 100644 --- a/src/test/scala/vastblue/file/PathSpec.scala +++ b/src/test/scala/vastblue/file/PathSpec.scala @@ -5,18 +5,26 @@ import vastblue.pathextend._ import vastblue.file.Paths.{canExist, normPath} import org.scalatest.funspec.AnyFunSpec import org.scalatest.matchers.should.Matchers -import vastblue.Platform -import vastblue.Platform.driveRoot +import vastblue.Platform.{driveRoot, cwd, cygdrive} class PathSpec extends AnyFunSpec with Matchers with BeforeAndAfter { - var hook = 0 - lazy val TMP = { - if (canExist("/g".path)) { - val gdir = Paths.get("/g") - // val str = gdir.localpath - gdir.isDirectory && gdir.paths.contains("/tmp") match { + var hook: Int = 0 + var verbose: Boolean = false + + val cygroot: String = cygdrive match { + case str if str.endsWith("/") => str + case str => s"$str/" + } + + lazy val TMP: String = { + val driveLetter = "g" + val driveRoot = s"${cygroot}${driveLetter}" + if (canExist(driveRoot.path)) { + val tmpdir = Paths.get(driveRoot) + // val str = tmpdir.localpath + tmpdir.isDirectory && tmpdir.paths.contains("/tmp") match { case true => - "/g/tmp" + s"${cygroot}g/tmp" case false => "/tmp" } @@ -24,34 +32,12 @@ class PathSpec extends AnyFunSpec with Matchers with BeforeAndAfter { "/tmp" } } - lazy val testFile: Path = { - val fnamestr = s"${TMP}/youMayDeleteThisDebrisFile.txt" - Paths.get(fnamestr) - } - - lazy val maxLines = 10 - lazy val testFileLines = (0 until maxLines).toList.map { _.toString } - lazy val homeDirTestFile = "~/shellExecFileTest.out" - lazy val testfileb: Path = { - Paths.get(homeDirTestFile) - } - lazy val here = Platform.cwd.normalize.toString.toLowerCase - lazy val uhere = here.replaceAll("[a-zA-Z]:", "").replace('\\', '/') - lazy val hereDrive = here.replaceAll(":.*", ":") match { - case drive if drive >= "a" && drive <= "z" => - drive - case _ => "" - } - - /** similar to gnu 'touch '. - */ - def touch(targetFile: Path): Int = { + /** similar to gnu 'touch ' */ + def touch(p: Path): Int = { var exitCode = 0 try { - // line ending is a place holder, if no lines. - // targetFile.withWriter(){ _ => } - targetFile.toFile.createNewFile() + p.toFile.createNewFile() } catch { case _: Exception => exitCode = 17 @@ -62,22 +48,13 @@ class PathSpec extends AnyFunSpec with Matchers with BeforeAndAfter { touch(file.toPath) } before { - // create homeDirTestFile - val tfpath = homeDirTestFile.path - touch(tfpath) - printf("homeDirTestFile: %s\n", homeDirTestFile) - // create testFile - testFile.withWriter() { w => + testFile.withWriter() { (w: PrintWriter) => testFileLines.foreach { line => w.print(line + "\n") } } printf("testFile: %s\n", testFile) } -// after { -//// if( testFile.exists ) testFile.delete() -//// if( testfileb.exists ) testfileb.delete() -// } describe("File") { describe("#eachline") { @@ -94,7 +71,9 @@ class PathSpec extends AnyFunSpec with Matchers with BeforeAndAfter { for ((line, lnum) <- testFile.lines.toSeq.zipWithIndex) { val expected = testFileLines(lnum) if (line != expected) { - System.err.println(s"line ${lnum}:\n [$line]\n [$expected]") + System.err.println(s"failure: line ${lnum}:\n [$line]\n [$expected]") + } else { + println(s"success: line ${lnum}:\n [$line]\n [$expected]") } assert(line == expected, s"line ${lnum}:\n [$line]\n [$expected]") } @@ -104,72 +83,43 @@ class PathSpec extends AnyFunSpec with Matchers with BeforeAndAfter { describe("#tilde-in-path-test") { it("should see file in user home directory if present") { val ok = testfileb.exists + if (ok) println(s"tilde successfully converted to path '$testfileb'") assert(ok, s"error: cannot see file '$testfileb'") } it("should NOT see file in user home directory if NOT present") { val test: Boolean = testfileb.delete() val ok = !testfileb.exists || !test + if (ok) + println( + s"delete() successfull, and correctly detected by 'exists' method on path '$testfileb'" + ) assert(ok, s"error: can still see file '$testfileb'") } } - // expected values of stdpath and localpath depend - // on whether g:/ exists. - // If so, c:/g is expected to resolve to /g and g:\\ -// lazy val (gu,gw) = os.dirExists("g:/") match { -// case true => ("/g", "g:\\") -// case false => ("/c/g","c:\\g") -// } if (isWindows) { - val expectedHomeDir = sys.props("user.home").replace('/', '\\') - val gdrive = Paths.get("g:/") printf("gdrive.exists: %s\n", gdrive.exists) printf("gdrive.isDirectory: %s\n", gdrive.isDirectory) printf("gdrive.isRegularFileg: %s\n", gdrive.isDirectory) printf("gdrive.isSymbolicLink: %s\n", gdrive.isSymbolicLink) - val gdriveTests = - if (gdrive.exists) { // should NOT really be a function of whether driver exists! - List( - ("/g", "g:\\"), - ("/g/", "g:\\") - ) - } else { - List( - ("/g", "g:\\"), - ("/g/", "g:\\") - ) - } - lazy val pathDospathPairs = List( - (".", here), - (hereDrive, here), // jvm treats this as cwd, if on c: - ("/q/", "q:\\"), // assumes /etc/fstab mounts /cygdrive to / - ("/q", "q:\\"), // assumes /etc/fstab mounts /cygdrive to / - ("/c/", "c:\\"), - ("~", expectedHomeDir), - ("~/", expectedHomeDir), - ("/g", "g:\\"), - ("/g/", "g:\\"), - ("/c/data/", "c:\\data") - ) ::: gdriveTests - - describe("# Path dospath test") { + describe("# dospath test") { it("should correctly handle cygwin dospath drive designations, when present") { var loop = -1 for ((fname, expected) <- pathDospathPairs) { loop += 1 + printf("fname[%s], expected[%s]\n", fname, expected) val file = Paths.get(fname) printf("%-22s : %s\n", file.stdpath, file.exists) val a = expected.toLowerCase // val b = file.toString.toLowerCase // val c = file.localpath.toLowerCase - if (fname == "/g") { - hook += 1 - } - // def abs(p: Path) = p.toAbsolutePath.normalize val d = file.dospath.toLowerCase val df = normPath(d) val af = normPath(a) val sameFile = Paths.isSameFile(af, df) - if (!sameFile) { + if (sameFile || a == d) { + println(s"a [$a] == d [$d]") + assert(a == d) + } else { System.err.printf("expected[%s]\n", expected.toLowerCase) System.err.printf("file.localpath[%s]\n", file.localpath.toLowerCase) System.err.printf( @@ -177,98 +127,181 @@ class PathSpec extends AnyFunSpec with Matchers with BeforeAndAfter { expected.toLowerCase, file.localpath.toLowerCase ) - if (file.exists && new JFile(expected).exists) { + val x = file.exists + val y = new JFile(expected).exists + if (x && y) { assert(a == d) + } else { + println(s"[$file].exists: [$x]\n[$expected].exists: [$y]") } } } } } - lazy val nonCanonicalDefaultDrive = driveRoot != "C:" - lazy val username = sys.props("user.name").toLowerCase - lazy val pathToStringPairs = List( - (".", uhere), - ("/q/", "/q"), - ("/q/file", "/q/file"), // assumes there is no Q: drive - (hereDrive, uhere), // jvm treats bare drive letter as cwd, if default drive - ("/c/", "/c"), - ("~", s"/users/${username}"), - ("~/", s"/users/${username}"), - ("/g", "/g"), - ("/g/", "/g"), - ("/c/data/", "/data") - ) - describe("# Path stdpath test") { - it("should correctly handle path toString") { - val upairs = pathToStringPairs.toArray.toSeq + describe("# stdpath test") { + it("should correctly handle toString") { + val upairs = toStringPairs.toArray.toSeq printf("%d pairs\n", upairs.size) var loop = -1 for ((fname, expected) <- upairs) { loop += 1 - if (fname.startsWith("/q")) { - hook += 1 + if (true || verbose) { + printf("=====================\n") + printf("fname[%s]\n", fname) + printf("expec[%s]\n", expected) } val file: Path = Paths.get(fname).toAbsolutePath.normalize() - printf("%-22s : %s\n", file.stdpath, file.exists) + printf("file.norm[%-22s] : %s\n", file.norm, file.exists) + printf("file.stdpath[%-22s] : %s\n", file.stdpath, file.exists) val exp = expected.toLowerCase val std = file.stdpath.toLowerCase + val nrm = file.norm.toLowerCase + printf("exp[%s] : std[%s] : nrm[%s]\n", exp, std, nrm) // val loc = file.localpath.toLowerCase // val dos = file.dospath.toLowerCase + if (!std.endsWith(exp)) { + hook += 1 + } + if (!nrm.endsWith(exp)) { + hook += 1 + } + + // note: in some cases (on Windows, for semi-absolute paths not on the default drive), the posix + // version of the path must include the posix drive letter. This test is subtle in order to + // recognize this case. if (nonCanonicalDefaultDrive) { - if (!std.endsWith(exp)) { - System.err.printf("error: stdpath[%s] not endsWith exp[%s]\n", std, exp) + printf("hereDrive[%s]\n", hereDrive) + if (std.endsWith(expected)) { + println(s"std[$std].endsWith(expected[$expected]) for hereDrive[$hereDrive]"); } + // in this case, there should also be a cygroot prefix (e.g., s"${cygroot}c") assert( std.endsWith(expected) - ) // in this case, there should also be a cygdrive prefix (e.g., "/c") + ) } else { - if (exp != std) { + if (exp == std) { + println(s"std[$std] == exp[$exp]") + } else { System.err.printf("error: expected[%s] not equal to toString [%s]\n", exp, std) } - assert(exp == std) + assert(exp == std) // || exp.drop(2) == std.drop(2) || std.contains(exp)) } - } } } + def getVariants(p: Path): Seq[Path] = { + val pstr = p.toString.toLowerCase + import vastblue.DriveRoot._ val stdpathToo = if (nonCanonicalDefaultDrive) Nil else Seq(p.stdpath) + val variants: Seq[String] = Seq( + p.norm, p.toString, p.localpath, p.dospath - ) ++ stdpathToo + ) ++ stdpathToo // stdpath fails round-trip test when default drive != C: - variants.distinct.map { s => - if (s == "q:/file") { + val vlist = variants.distinct.map { s => + val p = Paths.get(s) + if (p.toString.take(1).toLowerCase != pstr.take(1)) { hook += 1 } - val u = Paths.get(s) - u + p } + vlist.distinct } + describe("# Path consistency") { it("round trip conversions should be consistent") { for ( fname <- - (pathToStringPairs.toMap.keySet ++ pathDospathPairs.toMap.keySet).toList.distinct.sorted + (toStringPairs.toMap.keySet ++ pathDospathPairs.toMap.keySet).toList.distinct.sorted ) { - if (fname == "/q/file") { + if (fname == s"${cygroot}q/file") { hook += 1 } - val f1 = Paths.get(fname) + val f1: Path = Paths.get(fname) val variants: Seq[Path] = getVariants(f1) for (v <- variants) { // not necessarily 4 variants (duplicates removed before map to Path) // val (k1,k2) = (f1.key,v.key) val sameFile = Paths.isSameFile(f1, v) - if (!sameFile) { + if (f1 != v || !sameFile) { System.err.printf("f1[%s]\nv[%s]\n", f1, v) } - assert(sameFile, s"f1[$f1] != variant v[$v]") + if (f1.equals(v)) { + println(s"f1[$f1] == v[$v]") + } + assert(sameFile, s"not sameFile: f1[$f1] != variant v[$v]") + assert(f1.equals(v), s"f1[$f1] != variant v[$v]") } } } } } } + + lazy val testFile: Path = { + val fnamestr = s"${TMP}/youMayDeleteThisDebrisFile.txt" + Paths.get(fnamestr) + } + + lazy val maxLines = 10 + lazy val testFileLines = (0 until maxLines).toList.map { _.toString } + + lazy val homeDirTestFile = "~/shellExecFileTest.out" + lazy val testfileb: Path = { + val p = Paths.get(homeDirTestFile) + touch(p) + p + } + + lazy val here = cwd.normalize.toString.toLowerCase + lazy val uhere = here.replaceAll("[a-zA-Z]:", "").replace('\\', '/') + + lazy val hereDrive = here.replaceAll(":.*", ":") match { + case drive if drive >= "a" && drive <= "z" => + drive + case _ => "" + } + + lazy val expectedHomeDir = sys.props("user.home").replace('/', '\\') + + lazy val gdrive = Paths.get("g:/") + + lazy val gdriveTests = List( + (s"${cygroot}g", "g:\\"), + (s"${cygroot}g/", "g:\\") + ) + + lazy val pathDospathPairs = List( + (".", here), + (hereDrive, here), // jvm treats this as cwd, if on c: + (s"${cygroot}q/", "q:\\"), // assumes /etc/fstab mounts /cygroot to / + (s"${cygroot}q", "q:\\"), // assumes /etc/fstab mounts /cygroot to / + (s"${cygroot}c", "c:\\"), + (s"${cygroot}c/", "c:\\"), + ("~", expectedHomeDir), + ("~/", expectedHomeDir), + (s"${cygroot}g", "g:\\"), + (s"${cygroot}g/", "g:\\"), + (s"${cygroot}c/data/", "c:\\data") + ) ::: gdriveTests + + lazy val nonCanonicalDefaultDrive = driveRoot.toUpperCase != "C:" + + lazy val username = sys.props("user.name").toLowerCase + + lazy val toStringPairs = List( + (".", uhere), + (s"${cygroot}q/", s"${cygroot}q"), + (s"${cygroot}q/file", s"${cygroot}q/file"), // assumes there is no Q: drive + (hereDrive, uhere), // jvm: bare drive == cwd + (s"${cygroot}c/", s"${cygroot}c"), + ("~", s"/users/${username}"), + ("~/", s"/users/${username}"), + (s"${cygroot}g", s"${cygroot}g"), + (s"${cygroot}g/", s"${cygroot}g"), + (s"${cygroot}c/data/", "/data") + ) } diff --git a/src/test/scala/vastblue/file/RootRelativeTest.scala b/src/test/scala/vastblue/file/RootRelativeTest.scala index 6e64362..7d7d294 100644 --- a/src/test/scala/vastblue/file/RootRelativeTest.scala +++ b/src/test/scala/vastblue/file/RootRelativeTest.scala @@ -1,5 +1,6 @@ package vastblue.file +import vastblue.Platform._ import vastblue.pathextend._ import vastblue.file.Paths._ import org.scalatest.BeforeAndAfter @@ -72,7 +73,7 @@ class RootRelativeTest extends AnyFunSpec with Matchers with BeforeAndAfter { def jvmpath: String = { val psep = java.io.File.pathSeparator val entries: List[String] = sys.props("java.library.path").split(psep).map { _.toString }.toList - val path: String = entries.map { _.replace('\\', '/').toLowerCase }.distinct.mkString(";") + val path: String = entries.map { _.replace('\\', '/').toLowerCase }.distinct.mkString(";") path } }