对于使用Scala的程序员来说, I/O操作如文件的读写通常使用 scala.io.Source 来实现。但是这个类功能还是欠缺的,而且功能混乱,因此在Scala类库的增强提案( Scala IO fix-up/overhaul )中如何改进它争论相当的大,甚至有些开发者提议将这个库废掉,让社区实现的第三方来完成这方面的工作,或者引导开发者使用 java.nio
来实现I/O操作。
当然,作为一个使用Scala的公司来说,可能会自己实现了辅助的I/O操作的方法, 比如类似FileUtils等名称的一些类。Java程序员可能已经熟悉了使用 java.nio.file.Files 、 Guava 、 Apache common-io 、 jodd FileUtil 等开源I/O库,但是如果使用Scala进行I/O操作时,虽然还是可以使用这些Java I/O库,但是毕竟还是不是那么纯粹,因此,我们可以关注一下Scala实现的I/O库,比如 sbt io 、 Ammonite-Ops 、 better-files 等。
本文为你推荐 better-files 。
为什么推荐 better-files 呢?让我们看看它的功能,就明白它的简单而强大了,就像一把I/O操作的瑞士军刀。
要使用better-files,只需加入下面的依赖:
libraryDependencies += "com.github.pathikrit"%%"better-files"%version
它对Java NIO库进行了包装,不依赖其它的第三方库。
以下介绍摘译自better-files的 官方文档 。
下面的实例都是等价的,才可以采用多种方式得到File对象。可以通过字符串、String interpolator, Java File、隐式转换、定义的常量和操作符"/"等产生File对象。
importbetter.files._ importjava.io.{File => JFile} valf = File("/User/johndoe/Documents")// using constructor valf1: File = file"/User/johndoe/Documents"// using string interpolator valf2: File ="/User/johndoe/Documents".toFile// convert a string path to a file valf3: File =newJFile("/User/johndoe/Documents").toScala// convert a Java file to Scala valf4: File = root/"User"/"johndoe"/"Documents"// using root helper to start from root valf5: File = `~` /"Documents"// also equivalent to `home / "Documents"` valf6: File ="/User"/"johndoe"/"Documents"// using file separator DSL valf7: File = home/"Documents"/"presentations"/`..`// Use `..` to navigate up to parent
可以一行搞定:
valfile = root/"tmp"/"test.txt" file.overwrite("hello") file.appendLine().append("world") assert(file.contentAsString == "hello/nworld")
类似C++/Shell风格的读写,和上面的功能一样:
file < "hello"// same as file.overwrite("hello") file << "world"// same as file.appendLines("world") assert(file! == "hello/nworld")
或者这样:
"hello"`>:` file "world">>: file valbytes: 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
产生迭代器:
valbytes : Iterator[Byte] = file.bytes valchars : Iterator[Char] = file.chars vallines : Iterator[String] = file.lines valsource : scala.io.BufferedSource = file.newBufferedSource// needs to be closed, unlike the above APIs which auto closes when iterator ends
编解码:
valcontent: String = file.contentAsString// default codec // custom codec: importscala.io.Codec file.contentAsString(Codec.ISO8859) //or importscala.io.Codec.string2codec file.write("hello world")(codec ="US-ASCII")
转换成Java对象:
valfile: File = tmp /"hello.txt" valjavaFile : java.io.File = file.toJava valuri : java.net.uri = file.uri valreader : java.io.BufferedReader = file.newBufferedReader valoutputstream : java.io.OutputStream = file.newOutputStream valwriter : java.io.BufferedWriter = file.newBufferedWriter valinputstream : java.io.InputStream = file.newInputStream valpath : java.nio.file.Path = file.path valfs : java.nio.file.FileSystem = file.fileSystem valchannel : java.nio.channel.FileChannel = file.newFileChannel valram : java.io.RandomAccessFile = file.newRandomAccess valfr : java.io.FileReader = file.newFileReader valfw : java.io.FileWriter = file.newFileWriter(append =true) valprinter : java.io.PrintWriter = file.newPrintWriter
以及
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 valbytes : Iterator[Byte] = inputstream.bytes valbis : BufferedInputStream = inputstream.buffered valbos : BufferedOutputStream = outputstream.buffered valreader : InputStreamReader = inputstream.reader valwriter : OutputStreamWriter = outputstream.writer valprinter : PrintWriter = outputstream.printWriter valbr : BufferedReader = reader.buffered valbw : BufferedWriter = writer.buffered valmm : MappedByteBuffer = fileChannel.toMappedByteBuffer
/** * @returntrueiffileisa directorywithnochildrenora filewithnocontents */ def isEmpty(file: File): Boolean = file match { caseFile.Type.SymbolicLink(to)=>isEmpty(to)//thismust be firstcasestatementifyou want to handle symlinks specially;elsewill follow link caseFile.Type.Directory(files)=>files.isEmpty caseFile.Type.RegularFile(content)=>content.isEmpty case_=>file.notExists//a file maynotbe oneofthe above e.g. UNIX pipes, sockets, devices etc } //oras extractorsonLHS: val File.Type.Directory(researchDocs) = home/"Downloads"/"research"
valdir ="src"/"test" valmatches: Iterator[File] = dir.glob("**/*.{java,scala}") // above code is equivalent to: dir.listRecursively.filter(f => f.extension == Some(".java") || f.extension == Some(".scala"))
甚至使用正则表达式:
valmatches = dir.glob("^//w*$")(syntax = File.PathMatcherSyntax.regex)
文件系统操作也非常的便利:
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命令风格的操作:
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.name // simpler than java.io.File#getName file.extension file.contentType file.lastModifiedTime // returns JSR-310time file.owner /file.group file.isDirectory /file.isSymbolicLink /file.isRegularFile file.isHidden file.hide() /file.unhide() file.isOwnerExecutable /file.isGroupReadable // etc. seefile.permissions file.size //foradirectory, computes the directory size file.posixAttributes /file.dosAttributes // seefile.attributes file.isEmpty // trueiffilehasnocontent (ornochildrenifdirectory)ordoes not exist file.isParentOf /file.isChildOf /file.isSiblingOf /file.siblings
chmod
:
importjava.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)
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)`
排序:
valfiles = 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)
// Unzipping: valzipFile: File = file"path/to/research.zip" valresearch: File = zipFile.unzipTo(destination = home/"Documents"/"research") // Zipping: valzipFile: File = directory.zipTo(destination = home/"Desktop"/"toEmail.zip") // Zipping/Unzipping to temporary files/directories: valsomeTempZipFile: File = directory.zip() valsomeTempDir: 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)
better-files
提供了更加便利的管理,因此下面的代码
for{ reader <- file.newBufferedReader.autoClosed } foo(reader)
可以写成:
for{ reader <- file.bufferedReader // returns ManagedResource[BufferedReader] } foo(reader) // or simply: file.bufferedReader.map(foo)
valdata = t1 << s""" | Hello World | 1 true 2 3 """.stripMargin valscanner: 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)
你甚至可以写定制的Scanner。
普通的Java文件监控:
importjava.nio.file.{StandardWatchEventKinds => EventType} valservice: java.nio.file.WatchService = myDir.newWatchService myDir.register(service, events = Seq(EventType.ENTRY_CREATE, EventType.ENTRY_DELETE))
better-files
抽象了一个简单的接口:
valwatcher =newThreadBackedFileMonitor(myDir, recursive =true) { overridedefonCreate(file: File) = println(s"$file got created") overridedefonModify(file: File) = println(s"$file got modified") overridedefonDelete(file: File) = println(s"$file got deleted") } watcher.start()
或者用下面的写法:
importjava.nio.file.{Path, StandardWatchEventKinds => EventType, WatchEvent} valwatcher =newThreadBackedFileMonitor(myDir, recursive =true) { overridedefdispatch(eventType: WatchEvent.Kind[Path], file: File) = eventTypematch{ caseEventType.ENTRY_CREATE => println(s"$file got created") caseEventType.ENTRY_MODIFY => println(s"$file got modified") caseEventType.ENTRY_DELETE => println(s"$file got deleted") } }
importakka.actor.{ActorRef, ActorSystem} importbetter.files._, FileWatcher._ implicit valsystem = ActorSystem("mySystem") valwatcher: ActorRef = (home/"Downloads").newWatcher(recursive =true) // register partial function for an event watcher ! on(EventType.ENTRY_DELETE) { casefileiffile.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") }