Simple, safe and intuitive Scala I/O
Scala Java HTML
Latest commit 6673c38 Jan 18, 2017 @pathikrit Use javadoc.io in README

README.md

better-files License CircleCI Codacy Gitter

better-files is a dependency-free pragmatic thin Scala wrapper around Java NIO.

Talks

Tutorial Scaladoc

  1. Instantiation
  2. Simple I/O
  3. Streams and Codecs
  4. Java compatibility
  5. Globbing
  6. File system operations
  7. UNIX DSL
  8. File attributes
  9. File comparison
  10. Zip/Unzip
  11. Automatic Resource Management
  12. Scanner
  13. File Monitoring
  14. Reactive File Watcher

sbt UpdateImpact VersionEye

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"
)

Latest version: Maven

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.

Tests codecov


Instantiation

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

Resources in the classpath can be accessed using resource interpolator e.g. resource"production.config"

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}

File Read/Write

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:

file < "hello"     // same as file.overwrite("hello")
file << "world"    // same as file.appendLines("world")
assert(file! == "hello\nworld")

Or even, right-associatively:

"hello" `>:` file
"world" >>: file
val bytes: Array[Byte] = file.loadBytes

Fluent Interface:

 (root/"tmp"/"diary.txt")
  .createIfNotExists()  
  .appendLine()
  .appendLines("My name is", "Inigo Montoya")
  .moveTo(home/"Documents")
  .renameTo("princess_diary.txt")
  .changeExtensionTo(".md")
  .lines

Streams and Codecs

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.lines
val source : scala.io.BufferedSource   = file.newBufferedSource // needs to be closed, unlike the above APIs which auto closes when iterator ends

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 codec too for anything that does a read/write (it assumes scala.io.Codec.default if you don't provide one):

val content: String = file.contentAsString  // default codec
// custom codec:
import scala.io.Codec
file.contentAsString(Codec.ISO8859)
//or
import scala.io.Codec.string2codec
file.write("hello world")(codec = "US-ASCII")

Java interoperability

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 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

Globbing

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)

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)

File system operations

Utilities to ls, cp, rm, mv, ln, md5, diff, 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
Seq(file1, file2) >>: file3      // same as cat file1 file2 >> file3
file.isReadLocked / file.isWriteLocked / file.isLocked
File.newTemporaryDirectory() / File.newTemporaryFile() // create temp dir/file
File.numberOfOpenFileDescriptors        // number of open file descriptors

UNIX DSL

All the above can also be expressed using methods reminiscent of the command line:

import better.files_, Cmds._   // must import Cmds._ 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*)(zipFile)

File attributes

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)

File comparison

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)

Zip APIs

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/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)

Lightweight ARM

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.

Scanner

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 Options 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 HLists 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
))

File Monitoring

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")
  }
}

Akka File Watcher

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")
}