Provides support for expressive idioms typical of scripting languages, for writing portable code that runs everywhere.
Leverages vastblue.unifile.Paths.get()
to support both posix
and Windows
filesystem paths.
-
Supported Scala Versions
scala 3.x
-
Tested Target environments
Linux
Darwin/OSX
Windows
Cygwin64
Msys64
Mingw64
Git-bash
WSL Linux
To use pallet
in an SBT
project, add this dependency to build.sbt
"org.vastblue" % "pallet" %% "0.10.19" // scala 3
"org.vastblue" % "pallet_3" % "0.10.19" // scala 2.13.x
Simplicity and Universal Portability:
-
Use
scala
instead ofbash
orpython
for portable general purpose scripting -
Publish universal scala scripts, rather than multiple OS-customized versions
-
Script as though you're running in Linux, even on Windows or Mac.
-
standard OS and platform variables, based on
uname
information -
directly read csv file rows from
java.nio.file.Path
objects Extends the range of scala scripting: -
reference Windows filesystem paths via posix abstractions
-
predefined environment information:
- osType: String
- osName: String
- scalaHome: String
- javaHome: String
- userhome: String
- username: String
- hostname: String
- uname: String
- shellRoot: String
- isLinux: Boolean
- isWinshell: Boolean
- isDarwin: Boolean
- isWsl: Boolean
- isCygwin: Boolean
- isMsys: Boolean
- isMingw: Boolean
- isGitSdk: Boolean
- isGitbash: Boolean
- isWindows: Boolean
- verbose: Boolean
-
extension methods on
java.nio.file.Path
andjava.io.File
- name: String
- basename: String // drop extension
- parent: Path
- lines: Iterator[String]
- md5: String
- sha256: String
- cksum: Long
- lastModified: Long
- newerThan(other: Path): Boolean
- olderThan(other: Path): Boolean
- lastModifiedMillisAgo: Long
- lastModSecondsAgo: Double
- lastModMinutesAgo: Double
- lastModHoursAgo: Double
- lastModDaysAgo: Double
- withFileWriter(p: Path)(func: PrintWriter => Any)
- append DATA to script file
- many others
-
iterate directory subfiles:
- files: Iterator[JFile]
- paths: Iterator[Path]
- walkTree(file: JFile, depth: Int = 1, maxdepth: Int = -1): Iterable[JFile]
- walkTreeFiltered(file: JFile, depth: Int = 1, maxdepth: Int = -1)(filt: JFile => Boolean): Iterable[JFile]
- walkTreeFast(p: Path, tossDirs: Set[String], maxdepth: Int = -1)(filt: Path => Boolean)
-
read files in the
/proc
tree in Windows, e.g.:/proc/meminfo
/proc/$PID/cmdline
In Windows, requires installing a posix shell:
In Darwin/OSX
, requires homebrew
or similar.
Best with a recent version of coreutils:
(e.g., ubuntu
: 8.32-4.1ubuntu1, osx
: stable 9.4)
- Concise, expressive and readable scripting idioms
- correct portable handling of command line args
vastblue.file.Paths
is ajava.nio.file.Paths
drop-in replacement that:- correctly handles mounted
posix
paths - returns ordinary
java.nio.file.Path
objects
- correctly handles mounted
If you work in diverse environments, you generally must customize scripts for each environment:
- in
Linux
,Darwin/Osx
, shell or python scripts - in
Windows
, batch files, powershell scripts, or other Windows-specific tools
Hard to make scala scripts portable across Linux
, Osx
, Windows
, because
the jvm doesn't support filesystem abstractions of cygwin64
, msys64
, etc.
Most platforms other than Windows
are unix-like, but:
- differing conventions and incompatibilities:
- Linux / OSX symantics of
/usr/bin/env
, etc.
- Linux / OSX symantics of
This library provides the missing piece.
When using scala-cli
, the classpath is fully managed for you.
For java version 9 or above, you can also define the classpath in a java options file.
When specifying an options file for Darwin/Osx
, the @atFile
path must be absolute.
There are various ways to write scala3 script hash-bang
lines:
#!/usr/bin/env -S scala-cli shebang
#!/usr/bin/env -S scala
#!/usr/bin/env -S scala @/opt/scalaAtfile
For scala versions 3.5+, the first two variations are roughly (exactly?) equivalent.
For versions 3.4.3 and earlier, the 3rd form defines the classpath
in an options file.
Each scala-cli
script specifies required dependencies internally, and the classpath is managed for you.
Some differences to be aware of between scala-cli
scripts and legacy scala
scripts:
- a
scala-cli
script declares dependencies within the script via special comments - if a
main()
method is defined,scala-cli
requires it to be explicitly called at package level.
If you work in a Windows
posix shell, you are aware that java.nio.file.Paths.get()
expects file path String to be legal Windows
paths.
The following command line should print true
to the Console:
scala -e 'println(java.nio.file.Paths.get("C:/Windows").toFile.isDirectory)'
and the following command line will print false
:
scala -e 'println(java.nio.file.Paths.get("/etc/fstab").toFile.isFile)'
#!/usr/bin/env -S scala-cli shebang
//> using dep "org.vastblue::pallet::0.10.19"
import vastblue.pallet.*
val p = Paths.get("/etc/fstab")
printf("env: %-10s| posixroot: %-12s| %-22s| %d lines\n",
uname("-o"), posixroot, p.posx, p.lines.size)
Linux Mint # env: GNU/Linux | posixroot: / | /etc/fstab | 21 lines
Darwin # env: Darwin | posixroot: / | /etc/fstab | 0 lines
WSL Ubuntu # env: GNU/Linux | posixroot: / | /etc/fstab | 6 lines
Cygwin64 # env: Cygwin | posixroot: C:/cygwin64 | C:/cygwin64/etc/fstab | 24 lines
Msys64 # env: Msys | posixroot: C:/msys64/ | C:/msys64/etc/fstab | 22 lines
Note that on Darwin, there is no /etc/fstab
file, so the Path#lines
extension returns Nil
.
#!/usr/bin/env -S scala-cli shebang
//> using dep "org.vastblue::pallet::0.10.19"
import vastblue.pallet.*
// list child directories of "."
val cwd: Path = Paths.get(".")
for ( p: Path <- cwd.paths.filter { _.isDirectory }) {
printf("%s\n", p.posx)
}
#!/usr/bin/env -S scala-cli shebang
//> using dep "org.vastblue::pallet::0.10.19"
import vastblue.pallet.*
// display native path of command-line provided filenames
if args.isEmpty then
printf("usage: %s <path1> [<path2> ...]\n", scriptPath)
else
val dirs = for
fname <- args
p = Paths.get(fname)
if p.isFile
yield p.posx
printf("%s\n", dirs.toList.mkString("\n"))
Windows
: install one of the following:Linux
: required packages:sudo apt install coreutils
Darwin/OSX
:brew install coreutils
Things that maximize the odds of your script running on another system:
- use
scala 3
- use
posix
file paths by default - in
Windows
- represent paths internally with forward slashes
- drive letter not needed for paths on the current working drive (often C:)
- to access disks other than the working drive, mount them via
/etc/fstab
vastblue.Paths.get()
can parse bothposix
andWindows
filesystem paths
- don't assume path strings use
java.nio.File.separator
orsys.props("line.separator")
- use them to format output, as appropriate, never to parse path strings
- split strings with
"(\r)?\n"
rather thanline.separator
split("\n")
can leave carriage-return debris- `split(File.separator) fails or leaves debris if input string came from another OS
- create
java.nio.file.Path
objects in either of two ways:- `vastblue.file.Paths.get("/etc/fstab")
"/etc/fstab".path // guaranteed to use
vastblue.file.Paths.get()`
Examples below illustrate some of the capabilities.
#!/usr/bin/env -S scala-cli shebang
//> using dep "org.vastblue::pallet::0.10.19"
import vastblue.pallet.*
printf("uname / osType / osName:\n%s\n", s"platform info: ${unameLong} / ${osType} / ${osName}")
if (isLinux) {
// uname is "Linux"
printf("hello Linux\n")
} else if (isDarwin) {
// uname is "Darwin*"
printf("hello Mac\n")
} else if (isWinshell) {
// isWinshell: Boolean = isMsys | isCygwin | isMingw | isGitSdk | isGitbash
printf("hello %s\n", unameShort)
} else if (envOrEmpty("MSYSTEM").nonEmpty) {
printf("hello %s\n", envOrEmpty("MSYSTEM"))
} else {
assert(isWindows, s"unknown environment: ${unameLong} / ${osType} / ${osName}")
printf("hello Windows\n")
}
#!/usr/bin/env -S scala-cli shebang
//> using dep "org.vastblue::pallet::0.10.19"
import vastblue.pallet.*
import vastblue.pallet.*
import vastblue.file.ProcfsPaths.cmdlines
var verbose = false
for (arg <- args) {
arg match {
case "-v" =>
verbose = true
}
}
if (isLinux || isWinshell) {
printf("script name: %s\n\n", scriptName)
// find /proc/[0-9]+/cmdline files
for ((procfile, cmdline) <- cmdlines) {
if (verbose || cmdline.contains(scriptName)) {
printf("%s\n", procfile)
printf("%s\n\n", cmdline)
}
}
} else {
printf("procfs filesystem not supported in os [%s]\n", osType)
}
$ jsrc/procCmdline.sc
output when run from a Windows Msys64
bash session:
script name: jsrc/procCmdline.sc
/proc/32314/cmdline
'C:\opt\jdk\bin\java.exe' '-Dscala.home=C:/opt/scala' '-classpath' 'C:/opt/scala/lib/scala-library-2.13.10.jar;C:/opt/scala/lib/scala3-library_3-3.4.3.jar;C:/opt/scala/lib/scala-asm-9.5.0-scala-1.jar;C:/opt/scala/lib/compiler-interface-1.3.5.jar;C:/opt/scala/lib/scala3-interfaces-3.4.3.jar;C:/opt/scala/lib/scala3-compiler_3-3.4.3.jar;C:/opt/scala/lib/tasty-core_3-3.4.3.jar;C:/opt/scala/lib/scala3-staging_3-3.4.3.jar;C:/opt/scala/lib/scala3-tasty-inspector_3-3.4.3.jar;C:/opt/scala/lib/jline-reader-3.19.0.jar;C:/opt/scala/lib/jline-terminal-3.19.0.jar;C:/opt/scala/lib/jline-terminal-jna-3.19.0.jar;C:/opt/scala/lib/jna-5.3.1.jar;;' 'dotty.tools.MainGenericRunner' '-classpath' 'C:/opt/scala/lib/scala-library-2.13.10.jar;C:/opt/scala/lib/scala3-library_3-3.4.3.jar;C:/opt/scala/lib/scala-asm-9.5.0-scala-1.jar;C:/opt/scala/lib/compiler-interface-1.3.5.jar;C:/opt/scala/lib/scala3-interfaces-3.4.3.jar;C:/opt/scala/lib/scala3-compiler_3-3.4.3.jar;C:/opt/scala/lib/tasty-core_3-3.4.3.jar;C:/opt/scala/lib/scala3-staging_3-3.4.3.jar;C:/opt/scala/lib/scala3-tasty-inspector_3-3.4.3.jar;C:/opt/scala/lib/jline-reader-3.19.0.jar;C:/opt/scala/lib/jline-terminal-3.19.0.jar;C:/opt/scala/lib/jline-terminal-jna-3.19.0.jar;C:/opt/scala/lib/jna-5.3.1.jar;;' '-deprecation' '-cp' 'target/scala-3.4.3/classes' './procCmdline.sc'
/proc/32274/cmdline
'bash' '/c/opt/scala/bin/scala' '-deprecation' '-cp' 'target/scala-3.4.3/classes' './procCmdline.sc'
Example #2: write and read .csv
files:
#!/usr/bin/env -S scala-cli shebang
//> using dep "org.vastblue::pallet::0.10.19"
import vastblue.pallet.*
val testFiles = Seq("tabTest.csv", "commaTest.csv")
for (filename <- testFiles){
val testFile: Path = filename.toPath
if (!testFile.exists) {
// create tab-delimited and comma-delimited test files
val delim: String = if filename.startsWith("tab") then "\t" else ","
testFile.withWriter() { w =>
w.printf(s"1st${delim}2nd${delim}3rd\n")
w.printf(s"A${delim}B${delim}C\n")
}
}
assert(testFile.isFile)
printf("\n# filename: %s\n", testFile.posx)
// display file text lines
for ((line: String, i: Int) <- testFile.lines.zipWithIndex){
printf("%d: %s\n", i, line)
}
// display file csv rows
for (row: Seq[String] <- testFile.csvRows){
printf("%s\n", row.mkString("|"))
}
}
$ time jsrc/csvWriteRead.sc
Output:
# filename: C:/Users/username/workspace/pallet/tabTest.csv
0: 1st 2nd 3rd
1: A B C
1st|2nd|3rd
A|B|C
# filename: C:/Users/username/workspace/pallet/commaTest.csv
0: 1st,2nd,3rd
1: A,B,C
1st|2nd|3rd
A|B|C
real 0m4.269s
user 0m0.135s
sys 0m0.411s