better-files
is a dependency-free pragmatic thin Scala wrapper around Java NIO.
- Instantiation
- Simple I/O
- Streams
- Encodings
- Java compatibility
- Globbing
- File system operations
- Temporary files
- UNIX DSL
- File attributes
- File comparison
- Zip/Unzip
- Automatic Resource Management
- [Scanner] (#scanner)
- File Monitoring
- Reactive File Watcher
In your build.sbt
, add this:
libraryDependencies += "com.github.pathikrit" %% "better-files" % version
To use the Akka based file monitor, also add this:
libraryDependencies ++= Seq(
"com.github.pathikrit" %% "better-files-akka" % version,
"com.typesafe.akka" %% "akka-actor" % "2.4.16"
)
Although this library is currently only actively developed for Scala 2.12, you can find reasonably recent versions of this library for Scala 2.10 and 2.11 here.
---
The following are all equivalent:
import better.files._
import java.io.{File => JFile}
val f = File("/User/johndoe/Documents") // using constructor
val f1: File = file"/User/johndoe/Documents" // using string interpolator
val f2: File = "/User/johndoe/Documents".toFile // convert a string path to a file
val f3: File = new JFile("/User/johndoe/Documents").toScala // convert a Java file to Scala
val f4: File = root/"User"/"johndoe"/"Documents" // using root helper to start from root
val f5: File = `~` / "Documents" // also equivalent to `home / "Documents"`
val f6: File = "/User"/"johndoe"/"Documents" // using file separator DSL
val f7: File = home/"Documents"/"presentations"/`..` // Use `..` to navigate up to parent
Note: Rename the import if you think the usage of the class File
may confuse your teammates:
import better.files.{File => ScalaFile, _}
import java.io.File
I personally prefer renaming the Java crap instead:
import better.files._
import java.io.{File => JFile}
Dead simple I/O:
val file = root/"tmp"/"test.txt"
file.overwrite("hello")
file.appendLine().append("world")
assert(file.contentAsString == "hello\nworld")
If you are someone who likes symbols, then the above code can also be written as:
import better.files.Dsl.SymbolicOperations
file < "hello" // same as file.overwrite("hello")
file << "world" // same as file.appendLines("world")
assert(file! == "hello\nworld")
Or even, right-associatively:
import better.files.Dsl.SymbolicOperations
"hello" `>:` file
"world" >>: file
val bytes: Array[Byte] = file.loadBytes
(root/"tmp"/"diary.txt")
.createIfNotExists()
.appendLine()
.appendLines("My name is", "Inigo Montoya")
.moveTo(home/"Documents")
.renameTo("princess_diary.txt")
.changeExtensionTo(".md")
.lines
Various ways to slurp a file without loading the contents into memory:
val bytes : Iterator[Byte] = file.bytes
val chars : Iterator[Char] = file.chars
val lines : Iterator[String] = file.lineIterator //file.lines loads all lines in memory
Note: The above APIs can be traversed at most once e.g. file.bytes
is a Iterator[Byte]
which only allows TraversableOnce
.
To traverse it multiple times without creating a new iterator instance, convert it into some other collection e.g. file.bytes.toStream
You can write an Iterator[Byte]
or an Iterator[String]
back to a file:
file.writeBytes(bytes)
file.printLines(lines)
You can supply your own charset too for anything that does a read/write (it assumes java.nio.charset.Charset.defaultCharset()
if you don't provide one):
val content: String = file.contentAsString // default charset
// custom charset:
import java.nio.charset.Charset
file.contentAsString(charset = Charset.forName("US-ASCII"))
//or simply using implicit conversion from Strings
file.write("hello world")(charset = "US-ASCII")
Note: By default, better-files
correctly handles BOMs while decoding.
If you wish to have the incorrect JDK behaviour,
you would need to supply Java's UTF-8 charset e.g.:
file.contentAsString(charset = Charset.forName("UTF-8")) // Default incorrect JDK behaviour for UTF-8 (see: JDK-4508058)
If you also wish to write BOMs while encoding, you would need to supply it as:
file.write("hello world")(charset = UnicodeCharset("UTF-8", writeByteOrderMarkers = true))
You can always access the Java I/O classes:
val file: File = tmp / "hello.txt"
val javaFile : java.io.File = file.toJava
val uri : java.net.URI = file.uri
val url : java.net.URL = file.url
val reader : java.io.BufferedReader = file.newBufferedReader
val outputstream : java.io.OutputStream = file.newOutputStream
val writer : java.io.BufferedWriter = file.newBufferedWriter
val inputstream : java.io.InputStream = file.newInputStream
val path : java.nio.file.Path = file.path
val fs : java.nio.file.FileSystem = file.fileSystem
val channel : java.nio.channel.FileChannel = file.newFileChannel
val ram : java.io.RandomAccessFile = file.newRandomAccess
val fr : java.io.FileReader = file.newFileReader
val fw : java.io.FileWriter = file.newFileWriter(append = true)
val printer : java.io.PrintWriter = file.newPrintWriter
The library also adds some useful implicits to above classes e.g.:
file1.reader > file2.writer // pipes a reader to a writer
System.in > file2.out // pipes an inputstream to an outputstream
src.pipeTo(sink) // if you don't like symbols
val bytes : Iterator[Byte] = inputstream.bytes
val bis : BufferedInputStream = inputstream.buffered
val bos : BufferedOutputStream = outputstream.buffered
val reader : InputStreamReader = inputstream.reader
val writer : OutputStreamWriter = outputstream.writer
val printer : PrintWriter = outputstream.printWriter
val br : BufferedReader = reader.buffered
val bw : BufferedWriter = writer.buffered
val mm : MappedByteBuffer = fileChannel.toMappedByteBuffer
tee
multiple outputstreams:
val s3 = s1 tee s2
s3.printWriter.println(s"Hello world") // gets written to both s1 and s2
No need to port this to Scala:
val dir = "src"/"test"
val matches: Iterator[File] = dir.glob("*.{java,scala}")
// above code is equivalent to:
dir.listRecursively.filter(f => f.extension == Some(".java") || f.extension == Some(".scala"))
You can even use more advanced regex syntax instead of glob syntax:
val matches = dir.glob("^\\w*$")(syntax = File.PathMatcherSyntax.regex)
By default, glob syntax in better-files
is different from
the default JDK glob behaviour since it always includes path. To use the default behaviour:
dir.glob("**/*.txt", includePath = false) // JDK default
//OR
dir.glob("*.txt", includePath = true) // better-files default
You can also extend the File.PathMatcherSyntax
to create your own matchers.
For custom cases:
dir.collectChildren(_.isSymbolicLink) // collect all symlinks in a directory
For simpler cases, you can always use dir.list
or dir.walk(maxDepth: Int)
Utilities to ls
, cp
, rm
, mv
, ln
, md5
, touch
, cat
etc:
file.touch()
file.delete() // unlike the Java API, also works on directories as expected (deletes children recursively)
file.clear() // If directory, deletes all children; if file clears contents
file.renameTo(newName: String)
file.moveTo(destination)
file.copyTo(destination) // unlike the default API, also works on directories (copies recursively)
file.linkTo(destination) // ln file destination
file.symbolicLinkTo(destination) // ln -s file destination
file.{checksum, md5, sha1, sha256, sha512, digest} // also works for directories
file.setOwner(user: String) // chown user file
file.setGroup(group: String) // chgrp group file
Seq(file1, file2) `>:` file3 // same as cat file1 file2 > file3 (must import import better.files.Dsl.SymbolicOperations)
Seq(file1, file2) >>: file3 // same as cat file1 file2 >> file3 (must import import better.files.Dsl.SymbolicOperations)
file.isReadLocked; file.isWriteLocked; file.isLocked
File.numberOfOpenFileDescriptors // number of open file descriptors
Utils to create temporary files:
File.newTemporaryDirectory()
File.newTemporaryFile()
The above APIs allow optional specifications of prefix
, suffix
and parentDir
.
These files are not deleted automatically on exit by the JVM (you have to set deleteOnExit
which adds to shutdownHook
).
A cleaner alternative is to use self-deleting file contexts which deletes the file immediately when done:
File.usingTempFile() {tempFile =>
...
// tempFile is auto deleted at the end of this block - even if an exception happens
}
// or equivalently:
File.newTempFile().applyAndDelete(tempFile => ...)
You can also load resources from your classpath using File.resource
or File.copyResource
.
All the above can also be expressed using methods reminiscent of the command line:
import better.files._
import better.files.Dsl._ // must import Dsl._ to bring in these utils
pwd / cwd // current dir
cp(file1, file2)
mv(file1, file2)
rm(file) /*or*/ del(file)
ls(file) /*or*/ dir(file)
ln(file1, file2) // hard link
ln_s(file1, file2) // soft link
cat(file1)
cat(file1) >>: file
touch(file)
mkdir(file)
mkdirs(file) // mkdir -p
chown(owner, file)
chgrp(owner, file)
chmod_+(permission, files) // add permission
chmod_-(permission, files) // remove permission
md5(file); sha1(file); sha256(file); sha512(file)
unzip(zipFile)(targetDir)
zip(file*)(targetZipFile)
Query various file attributes e.g.:
file.name // simpler than java.io.File#getName
file.extension
file.contentType
file.lastModifiedTime // returns JSR-310 time
file.owner
file.group
file.isDirectory; file.isSymbolicLink; file.isRegularFile
file.isHidden
file.hide(); file.unhide()
file.isOwnerExecutable; file.isGroupReadable // etc. see file.permissions
file.size // for a directory, computes the directory size
file.posixAttributes; file.dosAttributes // see file.attributes
file.isEmpty // true if file has no content (or no children if directory) or does not exist
file.isParentOf; file.isChildOf; file.isSiblingOf; file.siblings
file("dos:system") = true // set custom meta-data for file (similar to Files.setAttribute)
All the above APIs let you specify the LinkOption
either directly:
file.isDirectory(LinkOption.NOFOLLOW_LINKS)
Or using the File.LinkOptions
helper:
file.isDirectory(File.LinkOptions.noFollow)
chmod
:
import java.nio.file.attribute.PosixFilePermission
file.addPermission(PosixFilePermission.OWNER_EXECUTE) // chmod +X file
file.removePermission(PosixFilePermission.OWNER_WRITE) // chmod -w file
assert(file.permissionsAsString == "rw-r--r--")
// The following are all equivalent:
assert(file.permissions contains PosixFilePermission.OWNER_EXECUTE)
assert(file.testPermission(PosixFilePermission.OWNER_EXECUTE))
assert(file.isOwnerExecutable)
Use ==
to check for path-based equality and ===
for content-based equality:
file1 == file2 // equivalent to `file1.isSamePathAs(file2)`
file1 === file2 // equivalent to `file1.isSameContentAs(file2)` (works for regular-files and directories)
file1 != file2 // equivalent to `!file1.isSamePathAs(file2)`
file1 !== file2 // equivalent to `!file1.isSameContentAs(file2)`
There are also various Ordering[File]
instances included, e.g.:
val files = myDir.list.toSeq
files.sorted(File.Order.byName)
files.max(File.Order.bySize)
files.min(File.Order.byDepth)
files.max(File.Order.byModificationTime)
files.sorted(File.Order.byDirectoriesFirst)
You don't have to lookup on StackOverflow "How to zip/unzip in Java/Scala?":
// Unzipping:
val zipFile: File = file"path/to/research.zip"
val research: File = zipFile.unzipTo(destination = home/"Documents"/"research")
// Zipping:
val zipFile: File = directory.zipTo(destination = home/"Desktop"/"toEmail.zip")
// Zipping in:
val zipFile = File("countries.zip").zipIn(file"usa.txt", file"russia.txt")
// Zipping/Unzipping to temporary files/directories:
val someTempZipFile: File = directory.zip()
val someTempDir: File = zipFile.unzip()
assert(directory === someTempDir)
// Gzip handling:
File("countries.gz").newInputStream.gzipped.lines.take(10).foreach(println)
Auto-close Java closeables:
for {
in <- file1.newInputStream.autoClosed
out <- file2.newOutputStream.autoClosed
} in.pipeTo(out)
// The input and output streams are auto-closed once out of scope
better-files
provides convenient managed versions of all the Java closeables e.g. instead of writing:
for {
reader <- file.newBufferedReader.autoClosed
} foo(reader)
You can write:
for {
reader <- file.bufferedReader // returns ManagedResource[BufferedReader]
} foo(reader)
// or simply:
file.bufferedReader.map(foo)
Or use a utility to convert any closeable to an iterator:
val eof = -1
val bytes: Iterator[Byte] = inputStream.autoClosedIterator(_.read())(_ != eof).map(_.toByte)
Note: The autoClosedIterator
only closes the resource when hasNext
i.e. (_ != eof)
returns false.
If you only partially use the iterator e.g. .take(5)
, it may leave the resource open. In those cases, use the managed autoClosed
version instead.
Although java.util.Scanner
has a feature-rich API, it only allows parsing primitives.
It is also notoriously slow since it uses regexes and does un-Scala things like returns nulls and throws exceptions.
better-files
provides a faster, richer, safer, more idiomatic and compossible Scala replacement
that does not use regexes, allows peeking, accessing line numbers, returns Option
s whenever possible and lets the user mixin custom parsers:
val data = t1 << s"""
| Hello World
| 1 true 2 3
""".stripMargin
val scanner: Scanner = data.newScanner()
assert(scanner.next[String] == "Hello")
assert(scanner.lineNumber == 1)
assert(scanner.next[String] == "World")
assert(scanner.next[(Int, Boolean)] == (1, true))
assert(scanner.tillEndOfLine() == " 2 3")
assert(!scanner.hasNext)
If you are simply interested in tokens, you can use file.tokens()
Writing your own custom scanners:
sealed trait Animal
case class Dog(name: String) extends Animal
case class Cat(name: String) extends Animal
implicit val animalParser: Scannable[Animal] = Scannable {scanner =>
val name = scanner.next[String]
if (name == "Garfield") Cat(name) else Dog(name)
}
val scanner = file.newScanner()
println(scanner.next[Animal])
The shapeless-scanner module lets you scan HList
s e.g.:
val in = Scanner("""
12 Bob True
13 Mary False
26 Rick True
""")
import shapeless._
type Row = Int :: String :: Boolean :: HNil
val out = Seq.fill(3)(in.next[Row])
assert(out == Seq(
12 :: "Bob" :: true :: HNil,
13 :: "Mary" :: false :: HNil,
26 :: "Rick" :: true :: HNil
))
Vanilla Java watchers:
import java.nio.file.{StandardWatchEventKinds => EventType}
val service: java.nio.file.WatchService = myDir.newWatchService
myDir.register(service, events = Seq(EventType.ENTRY_CREATE, EventType.ENTRY_DELETE))
The above APIs are cumbersome to use (involves a lot of type-casting and null-checking), are based on a blocking polling-based model, does not easily allow recursive watching of directories and nor does it easily allow watching regular files without writing a lot of Java boilerplate.
better-files
abstracts all the above ugliness behind a simple interface:
val watcher = new ThreadBackedFileMonitor(myDir, recursive = true) {
override def onCreate(file: File) = println(s"$file got created")
override def onModify(file: File) = println(s"$file got modified")
override def onDelete(file: File) = println(s"$file got deleted")
}
watcher.start()
Sometimes, instead of overwriting each of the 3 methods above, it is more convenient to override the dispatcher itself:
import java.nio.file.{Path, StandardWatchEventKinds => EventType, WatchEvent}
val watcher = new ThreadBackedFileMonitor(myDir, recursive = true) {
override def dispatch(eventType: WatchEvent.Kind[Path], file: File) = eventType match {
case EventType.ENTRY_CREATE => println(s"$file got created")
case EventType.ENTRY_MODIFY => println(s"$file got modified")
case EventType.ENTRY_DELETE => println(s"$file got deleted")
}
}
better-files
also provides a powerful yet concise reactive file watcher
based on Akka actors that supports dynamic dispatches:
import akka.actor.{ActorRef, ActorSystem}
import better.files._, FileWatcher._
implicit val system = ActorSystem("mySystem")
val watcher: ActorRef = (home/"Downloads").newWatcher(recursive = true)
// register partial function for an event
watcher ! on(EventType.ENTRY_DELETE) {
case file if file.isDirectory => println(s"$file got deleted")
}
// watch for multiple events
watcher ! when(events = EventType.ENTRY_CREATE, EventType.ENTRY_MODIFY) {
case (EventType.ENTRY_CREATE, file) => println(s"$file got created")
case (EventType.ENTRY_MODIFY, file) => println(s"$file got modified")
}