ZetCode

Groovy File I/O

last modified October 15, 2026

Groovy extends java.io.File with a large set of convenience methods, so everyday file handling needs far less ceremony than plain Java. Reading a file, iterating its lines, writing text, and traversing directories are all one-liners, and the withReader and withWriter helpers close streams for you. This tutorial covers reading, writing, copying, and walking the filesystem with practical examples.

Reading a File Line by Line

The eachLine method processes a file one line at a time, which keeps memory flat for large files. A closure receives each line as it is read.

ReadLines.groovy
def file = new File("data.txt")

file.eachLine { line, number ->
    println "$number: $line"
}

The closure receives both the line and its one-based number. Because the stream is opened and closed inside the call, there is no resource cleanup to remember, and only the current line is held in memory.

$ groovy ReadLines.groovy
1: line one
2: line two
3: name,age
4: Alice,30

Reading the Whole File

For small files, text returns the entire content as a single string, while readLines returns a list of lines.

ReadWholeFile.groovy
def file = new File("data.txt")

println file.text

def lines = file.readLines()
println lines.size()
println lines.first()
println lines.last()

The text property yields the full file, and readLines splits it into a list. These shortcuts are ideal for configuration files and small fixtures where loading everything at once is acceptable.

$ groovy ReadWholeFile.groovy
line one
line two
name,age
Alice,30

4
line one
Alice,30

Reading with a Reader and Encoding

When you need a specific character encoding, withReader supplies a BufferedReader that is closed automatically at the end of the block.

ReadEncoding.groovy
def file = new File("data.txt")

file.withReader("UTF-8") { reader ->
    String line
    while ((line = reader.readLine()) != null) {
        println line
    }
}

The explicit UTF-8 encoding avoids the platform default, which may corrupt accented characters. The reader is closed even if an exception is thrown inside the block, guaranteeing no leaked handles.

$ groovy ReadEncoding.groovy
line one
line two
name,age
Alice,30

Writing to a File

The simplest way to write text is the write method, but withWriter is preferred for multiple writes because it manages the stream for you.

WriteFile.groovy
def file = new File("output.txt")

file.write("First line\n")

file.withWriter("UTF-8") { writer ->
    writer.writeLine("Second line")
    writer.writeLine("Third line")
}

The write call replaces the file content, and the withWriter block appends its lines to the freshly created file. The writeLine helper adds the platform line separator automatically.

Appending to a File

To add to an existing file, open it in append mode with withWriterAppend. The new content is placed after the current end of file.

AppendFile.groovy
def log = new File("app.log")

log.withWriterAppend { writer ->
    writer.writeLine("[INFO] application started")
    writer.writeLine("[INFO] user logged in")
}

Each run adds two lines to the log, preserving earlier entries. Append mode is the natural fit for logs, audit trails, and other growing records.

Copying Files

The File class offers explicit copy methods as well as the << operator, which streams one file into another.

CopyFile.groovy
def source = new File("data.txt")
def destination = new File("backup.txt")

source.withInputStream { input ->
    destination.withOutputStream { output ->
        output << input
    }
}

println destination.text

Piping the input stream into the output stream copies the bytes verbatim, including any binary data. Both streams are closed when their blocks exit, so no partial copies are left behind on error.

Listing Directory Contents

The eachFile and listFiles methods enumerate the entries of a directory. A closure receives each File in turn.

ListDir.groovy
def dir = new File(".")

dir.eachFile { file ->
    println "${file.directory ? 'DIR ' : 'FILE'} ${file.name}"
}

Each entry is classified as a directory or file, and its name is printed. The eachFile variants with a filter or a FileType narrow the listing without an explicit condition.

Recursive Traversal

To walk an entire tree, use eachFileRecurse. The closure is invoked for every file and directory at any depth.

RecurseDir.groovy
def root = new File(".")

def entries = []
root.eachFileRecurse { file ->
    entries << file
    if (file.name.endsWith(".groovy")) {
        println file.path
    }
}

println "total entries: ${entries.size()}"

The traversal prints every Groovy source file under the current directory. The closure also collects each entry into a list, so the total is counted in the same pass. Change "." to the directory you want to scan. The method itself returns nothing, so the count has to be collected inside the closure. Recursive traversal is the basis of source scanners, report generators, and build tooling.

Working with Directories

Creating and removing directories is handled by mkdirs and deleteDir, which work recursively where appropriate.

Directories.groovy
def dir = new File("build/tmp")

if (!dir.exists()) {
    dir.mkdirs()
}

println dir.directory

dir.deleteDir()
println dir.exists()

The mkdirs call creates missing parents along the path, and deleteDir removes the directory and everything inside it. These are convenient primitives for cleaning up build workspaces.

Reading Binary Data

For binary files, bytes returns the raw content as a byte array, and withInputStream gives streamed access.

BinaryFile.groovy
def file = new File("image.png")

byte[] data = file.bytes
println "size: ${data.length} bytes"

println data[0..7].collect { String.format("%02X", it) }.join(" ")

The first two bytes of a PNG are the magic number 89 50, which the hex dump confirms. Reading bytes is the right approach for images, archives, and other non-text content.

Processing a CSV File

Combining eachLine with split turns a CSV file into structured records without a dedicated library.

CsvFile.groovy
def csv = new File("people.csv")

csv.eachLine { line, number ->
    if (number == 1) return
    def fields = line.split(",")
    println "name=${fields[0].trim()}, age=${fields[1].trim()}"
}

The header row is skipped by checking the line number, and each remaining line is split into fields. For files with quoted commas you would need a real CSV parser, but this covers simple delimited data.

$ groovy CsvFile.groovy
name=Alice, age=30
name=Bob, age=25

Best Practices for File I/O

Source

Groovy JDK Enhancements

In this tutorial, we read files line by line and in full, handled encodings, wrote and appended text, copied files, listed and traversed directories, created and removed folders, read binary data, and parsed a CSV file. Groovy's extensions to File make routine file handling compact and safe.

Author

My name is Jan Bodnar, and I am a passionate programmer with many years of programming experience. I have been writing programming articles since 2007. So far, I have written over 1400 articles and 8 e-books. I have over eight years of experience in teaching programming.

List all Groovy tutorials.