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.

1 Comment
Inline Feedbacks
View all comments
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.