Skip to content

Latest commit

 

History

History
498 lines (445 loc) · 20.9 KB

README.md

File metadata and controls

498 lines (445 loc) · 20.9 KB

better-files License CircleCI Codacy Gitter

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

Tutorial Scaladoc

  1. Instantiation
  2. Simple I/O
  3. Streams and Codecs
  4. Java compatibility
  5. Pattern matching
  6. Globbing
  7. File system operations
  8. UNIX DSL
  9. File attributes
  10. File comparison
  11. Zip/Unzip
  12. Automatic Resource Management
  13. [Scanner] (#scanner)
  14. File Monitoring
  15. 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.3.15"
)

Latest version: Maven

Although this library is compatible with both Scala 2.10 and 2.11, it needs minimum JDK 8.

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

Pattern matching

Instead of if-else, more idiomatic powerful Scala pattern matching:

/**
 * @return true if file is a directory with no children or a file with no contents
 */
def isEmpty(file: File): Boolean = file match {
  case File.Type.SymbolicLink(to) => isEmpty(to)  // this must be first case statement if you want to handle symlinks specially; else will follow link
  case File.Type.Directory(files) => files.isEmpty
  case File.Type.RegularFile(content) => content.isEmpty
  case _ => file.notExists    // a file may not be one of the above e.g. UNIX pipes, sockets, devices etc
}
// or as extractors on LHS:
val File.Type.Directory(researchDocs) = home/"Downloads"/"research"

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

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

All the above APIs let's you specify the LinkOption either directly:

file.isDirectory(LinkOption.NOFOLLOW_LINKS)

Or using the File.Links helper:

file.isDirectory(File.Links.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(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] 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 let's 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")
}