1. Introduction
In most programming languages, file operations need a lot of boilerplate code. From opening and closing the resources to creating nested directories, we need to write many lines of descriptive code.
Especially in smaller projects and utilities, a simple file library will make a lot of difference. Ammonite-Ops is such a library in Scala that makes the file operations easy, and it’s part of the Ammonite project.
Ammonite-Ops can be used independently as ammonite scripts or added like any other library in big projects as a library dependency. In this tutorial, we’ll look at Ammonite-Ops and how we can use it to quickly perform everyday file operations.
2. Advantages
Some of the advantages of using Ammonite-Ops are:
- No boilerplate code required
- Simple and concise methods
- Similar to the shell file commands
3. Dependency
We can add the library as an sbt dependency:
libraryDependencies += "com.lihaoyi" %% "ammonite-ops" % "2.4.0"
Now, we can start using the library. All the everyday operations are accessible with the import statement:
import ammonite.ops._
4. File Operations
Let’s have a look at the standard file operations.
4.1. Creating Path Reference
Ammonite-Ops operates on Path. This is an alternative to the java.nio.Path from the os-lib library, on top of which Ammonite-Ops is built. We can create the Path reference using the pwd method:
val workingDir: os.Path = pwd / "base"
4.1. Listing Directory Contents
To get the contents of a particular directory, we can use the ls! method:
val list = ls! workingDir
This will return all the files and directories under the base directory. To get only files, we can apply a filter with the isFile function:
val onlyFiles = list.filter(_.isFile)
We can get a list of all files and directories recursively using the ls.rec method:
val recursiveList = ls.rec! workingDir
There are many other methods like isDir, isSymLink, owner, permissions, and so on, that can be used to get more details about a file.
4.2. Create Directory
We can use the method mkdir! to create a directory:
mkdir! pwd/"base"
We can also create nested directories, without needing to check if they already exist or not. It will create the directory if doesn’t exist, and skip directory creation if it already exists:
mkdir! pwd / "sub1" / "sub2" / "sub3"
4.3. Copy Files and Directories
There are multiple ways in which we can copy files and directories. Let’s look at a few different scenarios.
The cp method copies the content of one directory to another path:
cp pwd/"dir1" pwd/"dir2"
Note that the directory dir2 should not exist. If the directory already exists, then cp will throw an exception.
Similarly, the cp.into method copies the content of a directory into another directory. If the target directory already exists, then it will throw an exception:
cp.into(wd / "dir1", wd/"dir2")
The cp.over method is similar to cp.into the method, but it will overwrite the contents if already exists:
cp.over(wd / "dir1", wd/"dir4")
4.4. Delete File and Directory
We can easily delete a file or a directory using the rm! method:
rm! wd / "dir1"
If we delete a directory, all its contents will also be deleted.
4.5. Write to a File
It’s easy to write contents to a file using the write method. There’s no need to open or close resources, unlike most other file libraries. To write a simple file, we can invoke the write method with the path to the file and the content:
write(wd / "text" / "simple.txt", "This is a simple text file written using ammonite ops.")
This will create a file simple.txt in the path wd/text. If the target directory doesn’t exist, then it will throw an exception. However, we can pass the parameter createFolders as true in the write method to automatically create the required directories:
write(wd / "text" / "simple.txt", "This is a simple text file written using ammonite ops.", createFolders = true)
If we want to overwrite an existing file, we can use the write.over method:
write.over(wd / "text" / "simple.txt", "Overwrite contents", createFolders = true)
This will create a new file and write the contents to that file if it doesn’t exist.
Instead of overwriting, we can also use the append method to add the contents to an existing file:
write.append(filePath, "Append new contents", createFolders = true)
4.6. Reading File Contents
We can use the read method to get the contents of a file:
val filePath = wd / "s1" / "sub"/ "simple.txt"
val content: String = read(filePath)
By default, the read method reads the file content as String. We can use the read.bytes method to read the contents as Array[Byte]:
val byteContent: Array[Byte] = read.bytes(filePath)
We can also read the contents of a file line by line and get the result as a Seq using the read.lines method:
val lines: Seq[String] = read.lines(filePath)
4.7. Move or Rename Files and Directories
We can use the mv method to rename or move a file or directory:
mv(wd / "file.txt", wd / "myFile.txt")
We can also move a file from one directory to another existing directory using the mv.into method:
mv.into(wd / "dir" / "file.txt", wd / "dir" / "subDir")
5. Spawn a Sub-Process
We can also spawn sub-processes from Scala code using Ammonite-Ops. This is extremely useful in writing utility scripts. However, this depends on the operating system in which the code runs.
We need to add an import statement to bring the required methods into scope:
import ammonite.ops.ImplicitWd._
There are two different methods we can use to execute OS-level commands from the code: % and %%.
The % method executes the commands and writes the output to stdout or stderr. The %% methods return CommandResult, which contains the result of stderr or stdout, depending on the status of the command:
%("ls") //writes the ls output to stdout
val items: CommandResult = %%("ls")
items.out.lines // returns List of files
6. Conclusion
In this article, we discussed how we can use the Ammonite-Ops library to perform file operations easily. We can use it either in a standalone script or in a fully-fledged Scala project.
As always, the sample code used in this tutorial is available over on GitHub.