Working with Groovy file operations is surprisingly concise. See 12 practical examples covering read, write, delete, append, directory traversal, and more. Tested on Groovy 5.0.6.
“A program that doesn’t interact with files is like a carpenter who never picks up wood. Files are where real work happens.”
Last Updated: July 2026 | Tested on: Groovy 5.0.6, Java 17+ | Difficulty: Beginner to Intermediate | Reading Time: 20 minutes
Java file I/O means wrapping everything in try-catch-finally, managing streams, and remembering to close resources. It works, but it’s verbose. Groovy file operations take a completely different approach – reading a file is a single property access, writing is one assignment, and iterating over lines is a closure. The GDK adds dozens of methods directly to java.io.File.
In this tutorial, we’ll explore every major file operation Groovy offers – reading, writing, appending, deleting, directory traversal, temporary files, and file metadata. Every example is tested on Groovy 5.0.6 and ready to use in your projects. If you’re just getting started with Groovy’s dynamic features, our def keyword guide covers the fundamentals you’ll need.
The diagram below shows a decision tree for picking the right read method: answer a few quick questions about binary vs text, file size, random access, and custom reader logic, and it lands you on file.bytes, file.text, readLines, eachLine, or withReader:
Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.
Table of Contents
Why Groovy for File Operations?
Before we jump into examples, let’s understand what makes Groovy’s file handling special. According to the official Groovy GDK documentation, Groovy adds methods directly to java.io.File that dramatically simplify I/O tasks.
Key Advantages:
- One-liner reads:
new File('data.txt').textreads the entire file - Automatic resource management:
withReader(),withWriter()close resources automatically - Closure-based iteration:
eachLine(),eachFile()make loops readable - No boilerplate: No try-catch-finally blocks needed for basic operations
- Full Java compatibility: All Java file APIs still work alongside Groovy enhancements
Here is a quick comparison to understand why Groovy is a better choice for file scripting:
Java vs Groovy File Read
// Java way - verbose and boilerplate-heavy
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("data.txt"));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) reader.close();
}
// Groovy way - clean and concise
new File('data.txt').eachLine { println it }
That’s the difference. One line vs fifteen lines. Same result. Let’s dig deeper.
Reading Files in Groovy
Groovy gives you multiple ways to read files, each suited to different scenarios. Here’s a quick reference before we get into the examples:
| Method | Returns | Best For |
|---|---|---|
file.text | Entire file as String | Small files, quick reads |
file.readLines() | List of Strings | Line-by-line processing |
file.eachLine { } | void (iterates) | Large files, memory efficient |
file.withReader { } | Closure result | Custom BufferedReader logic |
file.bytes | byte[] | Binary files |
Writing Files in Groovy
Writing is just as easy. Groovy provides properties and methods for both overwriting and appending:
| Method | Behavior | Best For |
|---|---|---|
file.text = "..." | Overwrites entire file | Quick writes |
file.append("...") | Adds to end of file | Logging, data accumulation |
file.withWriter { } | BufferedWriter with auto-close | Multi-line writes |
file.newWriter() | Returns a BufferedWriter | Manual control |
file.bytes = [...] | Writes raw bytes | Binary data |
12 Practical Groovy File Examples
Example 1: Read Entire File with File.text
What we’re doing: Reading the complete content of a file using Groovy’s text property – the simplest way to groovy read file content.
Example 1: File.text Property
// First, create a sample file to work with
def file = new File('sample.txt')
file.text = 'Line 1: Hello Groovy\nLine 2: File operations are easy\nLine 3: This is amazing'
// Now read the entire file
def content = new File('sample.txt').text
println content
println "---"
println "Type: ${content.getClass().name}"
println "Length: ${content.length()} characters"
Output
Line 1: Hello Groovy Line 2: File operations are easy Line 3: This is amazing --- Type: java.lang.String Length: 77 characters
What happened here: The text property reads the entire file into a single String. This is Groovy’s GDK magic – Java’s File class doesn’t have a text property, but Groovy adds it. It handles opening the file, reading all content, and closing the resource. Perfect for small to medium files.
Example 2: Read File Line by Line with eachLine()
What we’re doing: Iterating through each line of a file using a closure – memory-efficient for large files.
Example 2: eachLine()
def file = new File('sample.txt')
file.text = 'Apple\nBanana\nCherry\nDate\nElderberry'
// eachLine with just the line
println "=== Lines ==="
file.eachLine { line ->
println "Fruit: ${line}"
}
// eachLine with line number (1-based)
println "\n=== With Line Numbers ==="
file.eachLine { line, lineNo ->
println "${lineNo}: ${line}"
}
Output
=== Lines === Fruit: Apple Fruit: Banana Fruit: Cherry Fruit: Date Fruit: Elderberry === With Line Numbers === 1: Apple 2: Banana 3: Cherry 4: Date 5: Elderberry
What happened here: The eachLine() method reads one line at a time, passing it to your closure. Unlike text which loads everything into memory, eachLine() is streamed – making it suitable for large files. The two-parameter version gives you the line number starting at 1.
Example 3: Read Lines into a List with readLines()
What we’re doing: Getting all lines as a List for random access or collection operations.
Example 3: readLines()
def file = new File('sample.txt')
file.text = 'Red\nGreen\nBlue\nYellow\nPurple'
def lines = file.readLines()
println "Total lines: ${lines.size()}"
println "First line: ${lines[0]}"
println "Last line: ${lines[-1]}"
println "Type: ${lines.getClass().name}"
// Use collection methods
println "\nSorted: ${lines.sort()}"
println "Filtered (5+ chars): ${lines.findAll { it.length() >= 5 }}"
Output
Total lines: 5 First line: Red Last line: Purple Type: java.util.ArrayList Sorted: [Blue, Green, Purple, Red, Yellow] Filtered (5+ chars): [Green, Purple, Yellow]
What happened here: The readLines() method returns a List<String>, which means you can use all of Groovy’s powerful collection methods – sort(), findAll(), collect(), and more. Note how Groovy’s negative indexing (lines[-1]) gives you the last element directly. One gotcha: sort() sorts the list in place, so the findAll() on the next line runs on the already sorted list. That is why Purple comes before Yellow in the filtered result.
Example 4: Read with BufferedReader using withReader()
What we’re doing: Using a BufferedReader with automatic resource management for custom read logic.
Example 4: withReader()
def file = new File('sample.txt')
file.text = 'server=prod-01\nport=8080\ntimeout=30\ndebug=false\nmax_connections=100'
// withReader automatically closes the reader when done
def config = [:]
file.withReader { reader ->
String line
while ((line = reader.readLine()) != null) {
def parts = line.split('=')
if (parts.length == 2) {
config[parts[0].trim()] = parts[1].trim()
}
}
}
println "Parsed config:"
config.each { key, value ->
println " ${key} = ${value}"
}
println "\nServer: ${config.server}"
println "Port: ${config.port}"
Output
Parsed config: server = prod-01 port = 8080 timeout = 30 debug = false max_connections = 100 Server: prod-01 Port: 8080
What happened here: The withReader() method creates a BufferedReader and passes it to your closure. When the closure finishes – whether normally or with an exception – the reader is automatically closed. No try-finally needed. This is Groovy’s equivalent of Java’s try-with-resources, but cleaner.
Example 5: Write to a File
What we’re doing: Writing content to a file using multiple approaches – the text property, withWriter(), and newWriter().
Example 5: Writing Files
// Method 1: Using text property (simplest)
def file1 = new File('output1.txt')
file1.text = 'Hello from Groovy!\nThis overwrites everything.'
println "Method 1 - text property:"
println file1.text
// Method 2: Using withWriter (auto-closes)
def file2 = new File('output2.txt')
file2.withWriter('UTF-8') { writer ->
writer.writeLine 'First line'
writer.writeLine 'Second line'
writer.writeLine 'Third line'
}
println "\nMethod 2 - withWriter:"
println file2.text
// Method 3: Using newWriter (manual control)
def file3 = new File('output3.txt')
def writer = file3.newWriter()
writer.write('Manual writer control\n')
writer.write('Remember to close!\n')
writer.close()
println "Method 3 - newWriter:"
println file3.text
// Cleanup
[file1, file2, file3].each { it.delete() }
Output
Method 1 - text property: Hello from Groovy! This overwrites everything. Method 2 - withWriter: First line Second line Third line Method 3 - newWriter: Manual writer control Remember to close!
What happened here: Three different ways to write files. The text property is the simplest – just assign a string. The withWriter() method is preferred for multi-line writes because it handles resource cleanup. The newWriter() method gives you manual control but you must remember to close it yourself.
Pro Tip: Always prefer
withWriter()overnewWriter()in production code. It guarantees the stream is closed even if an exception occurs.
Example 6: Append to a File
What we’re doing: Adding content to the end of an existing file without overwriting.
Example 6: Appending
def logFile = new File('app.log')
// Start fresh
logFile.text = ''
// Append log entries
logFile.append("[2026-03-08 10:00:00] INFO - Application started\n")
logFile.append("[2026-03-08 10:00:01] INFO - Loading configuration\n")
logFile.append("[2026-03-08 10:00:02] WARN - Config file missing, using defaults\n")
logFile.append("[2026-03-08 10:00:05] INFO - Server listening on port 8080\n")
logFile.append("[2026-03-08 10:01:30] ERROR - Connection timeout to database\n")
println "Log file contents:"
println logFile.text
println "Log file size: ${logFile.length()} bytes"
// Append with encoding
logFile.append("[2026-03-08 10:02:00] INFO - Retrying connection...\n", 'UTF-8')
println "Lines after append: ${logFile.readLines().size()}"
logFile.delete()
Output
Log file contents: [2026-03-08 10:00:00] INFO - Application started [2026-03-08 10:00:01] INFO - Loading configuration [2026-03-08 10:00:02] WARN - Config file missing, using defaults [2026-03-08 10:00:05] INFO - Server listening on port 8080 [2026-03-08 10:01:30] ERROR - Connection timeout to database Log file size: 289 bytes Lines after append: 6
What happened here: The append() method adds content to the end of a file. Unlike the text property which overwrites everything, append() preserves existing content. You can also specify encoding as a second parameter. This is ideal for logging, audit trails, or any data accumulation scenario.
Example 7: Delete Files and Check Existence
What we’re doing: Checking if files exist before operations, and deleting files safely.
Example 7: Delete and Exists
// Create some test files
def file1 = new File('temp1.txt')
def file2 = new File('temp2.txt')
def file3 = new File('nonexistent.txt')
file1.text = 'Temporary file 1'
file2.text = 'Temporary file 2'
// Check existence
println "temp1.txt exists: ${file1.exists()}"
println "temp2.txt exists: ${file2.exists()}"
println "nonexistent.txt exists: ${file3.exists()}"
// Safe delete pattern
[file1, file2, file3].each { f ->
if (f.exists()) {
def deleted = f.delete()
println "Deleted ${f.name}: ${deleted}"
} else {
println "Skipped ${f.name}: file not found"
}
}
// Verify deletion
println "\nAfter deletion:"
println "temp1.txt exists: ${file1.exists()}"
println "temp2.txt exists: ${file2.exists()}"
Output
temp1.txt exists: true temp2.txt exists: true nonexistent.txt exists: false Deleted temp1.txt: true Deleted temp2.txt: true Skipped nonexistent.txt: file not found After deletion: temp1.txt exists: false temp2.txt exists: false
What happened here: The exists() method checks whether a file is present on disk. The delete() method returns true if the file was successfully deleted, false otherwise. Always check exists() before deleting to avoid silent failures or unexpected behavior.
Example 8: Create Directories with mkdir() and mkdirs()
What we’re doing: Creating single directories and nested directory structures.
Example 8: Directory Creation
// Create a single directory
def dir1 = new File('testdir')
println "Created 'testdir': ${dir1.mkdir()}"
println "Is directory: ${dir1.isDirectory()}"
// Create nested directories (mkdirs creates parent dirs too)
def dir2 = new File('parent/child/grandchild')
println "Created nested dirs: ${dir2.mkdirs()}"
println "Nested dir exists: ${dir2.exists()}"
// Create a file inside the directory
def file = new File('testdir/hello.txt')
file.text = 'Hello from inside a directory!'
println "File in dir: ${file.text}"
// Check if path is file or directory
println "\ntestdir is file: ${dir1.isFile()}"
println "testdir is directory: ${dir1.isDirectory()}"
println "hello.txt is file: ${file.isFile()}"
// Cleanup
file.delete()
dir1.delete()
new File('parent/child/grandchild').delete()
new File('parent/child').delete()
new File('parent').delete()
Output
Created 'testdir': true Is directory: true Created nested dirs: true Nested dir exists: true File in dir: Hello from inside a directory! testdir is file: false testdir is directory: true hello.txt is file: true
What happened here: There’s an important difference between mkdir() and mkdirs(). The mkdir() method creates only the final directory and fails if parent directories don’t exist. The mkdirs() method creates the entire path, including any missing parent directories – similar to mkdir -p in Unix.
Example 9: List Files in a Directory
What we’re doing: Listing files in a directory using listFiles(), eachFile(), and eachDir().
Example 9: List Files
// Setup test directory structure
def baseDir = new File('project')
baseDir.mkdirs()
new File('project/src').mkdirs()
new File('project/lib').mkdirs()
new File('project/README.md').text = '# Project'
new File('project/build.gradle').text = 'apply plugin: java'
new File('project/app.groovy').text = 'println "hello"'
new File('project/src/Main.groovy').text = 'class Main {}'
// listFiles() - returns array of File objects
println "=== listFiles() ==="
baseDir.listFiles().each { println "${it.name} (${it.isDirectory() ? 'DIR' : 'FILE'})" }
// eachFile - only direct children
println "\n=== eachFile ==="
baseDir.eachFile { file ->
println file.name
}
// eachDir - only directories
println "\n=== eachDir ==="
baseDir.eachDir { dir ->
println "Directory: ${dir.name}"
}
// eachFileMatch - filter by pattern
println "\n=== Groovy files only ==="
baseDir.eachFileMatch(~/.*\.groovy/) { file ->
println file.name
}
// Cleanup
new File('project/src/Main.groovy').delete()
new File('project/src').delete()
new File('project/lib').delete()
['README.md', 'build.gradle', 'app.groovy'].each { new File("project/$it").delete() }
baseDir.delete()
Output
=== listFiles() === src (DIR) lib (DIR) README.md (FILE) build.gradle (FILE) app.groovy (FILE) === eachFile === src lib README.md build.gradle app.groovy === eachDir === Directory: src Directory: lib === Groovy files only === app.groovy
What happened here: Groovy adds several traversal methods to File. The eachFile() method iterates over direct children, eachDir() iterates over subdirectories only, and eachFileMatch() filters using a regex pattern or other Groovy matching strategies. Notice how eachFileMatch(~/.*\.groovy/) uses a slashy regex to find only .groovy files.
Example 10: Recursive Directory Traversal with eachFileRecurse()
What we’re doing: Walking through an entire directory tree, including all subdirectories.
Example 10: Recursive Traversal
import groovy.io.FileType
// Setup a nested directory structure
def root = new File('webapp')
new File('webapp/src/main/groovy').mkdirs()
new File('webapp/src/test/groovy').mkdirs()
new File('webapp/resources').mkdirs()
new File('webapp/src/main/groovy/App.groovy').text = 'class App {}'
new File('webapp/src/main/groovy/Utils.groovy').text = 'class Utils {}'
new File('webapp/src/test/groovy/AppTest.groovy').text = 'class AppTest {}'
new File('webapp/resources/config.yml').text = 'server: 8080'
new File('webapp/build.gradle').text = 'apply plugin: groovy'
// Recurse through ALL files (not directories)
println "=== All Files (Recursive) ==="
root.eachFileRecurse(FileType.FILES) { file ->
println file.path.replace('\\', '/')
}
// Recurse through directories only
println "\n=== All Directories ==="
root.eachFileRecurse(FileType.DIRECTORIES) { dir ->
println dir.path.replace('\\', '/')
}
// eachDirRecurse - shortcut for directories
println "\n=== Using eachDirRecurse ==="
root.eachDirRecurse { dir ->
println " ${dir.name}"
}
// Cleanup
root.deleteDir()
Output
=== All Files (Recursive) === webapp/src/main/groovy/App.groovy webapp/src/main/groovy/Utils.groovy webapp/src/test/groovy/AppTest.groovy webapp/resources/config.yml webapp/build.gradle === All Directories === webapp/src webapp/src/main webapp/src/main/groovy webapp/src/test webapp/src/test/groovy webapp/resources === Using eachDirRecurse === src main groovy test groovy resources
What happened here: The eachFileRecurse() method walks through the entire directory tree. You pass FileType.FILES to get only files, FileType.DIRECTORIES for only directories, or FileType.ANY for both. Also notice deleteDir() – a GDK method that recursively deletes a directory and all its contents. Much easier than manually deleting files bottom-up.
Example 11: Temporary Files
What we’re doing: Creating and using temporary files that automatically get a unique name.
Example 11: Temporary Files
// Create a temp file with prefix and suffix
def tempFile = File.createTempFile('groovy_', '.tmp')
println "Temp file: ${tempFile.absolutePath}"
println "Temp file name: ${tempFile.name}"
// Write data to temp file
tempFile.text = 'Temporary data for processing'
println "Content: ${tempFile.text}"
// Create temp file in a specific directory
def customDir = new File('temp_work')
customDir.mkdirs()
def tempFile2 = File.createTempFile('data_', '.csv', customDir)
tempFile2.text = 'id,name,score\n1,Alice,95\n2,Bob,87'
println "\nCustom temp file: ${tempFile2.name}"
println "Content:\n${tempFile2.text}"
// Mark for deletion when JVM exits
tempFile.deleteOnExit()
tempFile2.deleteOnExit()
println "\nTemp dir: ${System.getProperty('java.io.tmpdir')}"
// Cleanup
tempFile.delete()
tempFile2.delete()
customDir.delete()
Output
Temp file: C:\Users\Rahul\AppData\Local\Temp\groovy_1527072799010814747.tmp Temp file name: groovy_1527072799010814747.tmp Content: Temporary data for processing Custom temp file: data_3381504219464494037.csv Content: id,name,score 1,Alice,95 2,Bob,87 Temp dir: C:\Users\Rahul\AppData\Local\Temp\
What happened here: The File.createTempFile() method generates a file with a unique name using the given prefix and suffix. The three-argument version lets you specify the directory. The deleteOnExit() method registers the file for automatic cleanup when the JVM shuts down – useful for test data or intermediate processing files. The long number in the name is random and changes on every run, and the temp directory depends on your OS. This run was on Windows; on Linux you will typically see /tmp instead.
Example 12: File Properties – Size, Last Modified, Permissions
What we’re doing: Inspecting file metadata including size, modification time, and permissions.
Example 12: File Properties
def file = new File('metadata_test.txt')
file.text = 'This is a test file with some content for metadata inspection.'
println "=== File Properties ==="
println "Name: ${file.name}"
println "Absolute path: ${file.absolutePath}"
println "Parent: ${file.parent}"
println "Size: ${file.length()} bytes"
println "Last modified: ${new Date(file.lastModified())}"
println "Is file: ${file.isFile()}"
println "Is directory: ${file.isDirectory()}"
println "Is hidden: ${file.isHidden()}"
println "Can read: ${file.canRead()}"
println "Can write: ${file.canWrite()}"
println "Can execute: ${file.canExecute()}"
// Get file extension (Groovy doesn't have a built-in, but easy to do)
def extension = file.name.tokenize('.').last()
println "Extension: ${extension}"
// Human-readable size
def sizeKB = file.length() / 1024.0
println "Size (KB): ${String.format('%.2f', sizeKB)}"
// Modify last modified time
file.setLastModified(0)
println "\nAfter setLastModified(0): ${new Date(file.lastModified())}"
file.delete()
Output
=== File Properties === Name: metadata_test.txt Absolute path: C:\tmp\groovy-demo\metadata_test.txt Parent: null Size: 62 bytes Last modified: Thu Jul 02 22:08:37 SGT 2026 Is file: true Is directory: false Is hidden: false Can read: true Can write: true Can execute: true Extension: txt Size (KB): 0.06 After setLastModified(0): Thu Jan 01 07:30:00 SGT 1970
What happened here: Java’s File class provides many metadata methods, all of which work directly in Groovy. The length() method returns file size in bytes, lastModified() returns a timestamp in milliseconds, and the permission methods check OS-level file permissions. Notice how we got the file extension by tokenizing the name on dots and taking the last part. Two things surprise people here. First, parent prints null because we built the File from a bare relative name; use file.absoluteFile.parent when you need the actual directory.
Second, canExecute() returned true because this run was on Windows, which treats regular files as executable; on Linux a freshly created file normally reports false. The path and timestamps will of course reflect your own machine.
Directory Operations
Beyond listing files, Groovy provides built-in methods for working with directories. Here are the key operations:
Directory Operations Summary
import groovy.io.FileType
def dir = new File('demo_project')
dir.mkdirs()
// Create some files
new File('demo_project/app.groovy').text = 'println "app"'
new File('demo_project/utils.groovy').text = 'class Utils {}'
new File('demo_project/config.xml').text = '<config/>'
new File('demo_project/data.json').text = '{}'
new File('demo_project/sub').mkdirs()
new File('demo_project/sub/helper.groovy').text = 'class Helper {}'
// Collect all Groovy files recursively
def groovyFiles = []
dir.eachFileRecurse(FileType.FILES) { f ->
if (f.name.endsWith('.groovy')) {
groovyFiles << f.name
}
}
println "Groovy files: ${groovyFiles}"
// Get total size of directory
long totalSize = 0
dir.eachFileRecurse(FileType.FILES) { f ->
totalSize += f.length()
}
println "Total size: ${totalSize} bytes"
// Count files by extension
def extCount = [:]
dir.eachFileRecurse(FileType.FILES) { f ->
def ext = f.name.tokenize('.').last()
extCount[ext] = (extCount[ext] ?: 0) + 1
}
println "Files by type: ${extCount}"
// deleteDir() removes directory and all contents
dir.deleteDir()
println "Directory deleted: ${!dir.exists()}"
Output
Groovy files: [app.groovy, helper.groovy, utils.groovy] Total size: 53 bytes Files by type: [groovy:3, xml:1, json:1] Directory deleted: true
What happened here: Three passes with eachFileRecurse() collect names, total bytes, and counts by extension, then deleteDir() wipes the whole tree in one call. Keep in mind that eachFileRecurse() visits entries in whatever order the filesystem lists them, so the order inside groovyFiles can differ between machines. The totals stay the same.
File Properties and Metadata
Groovy gives you access to all file metadata through simple method calls. Here’s a full reference:
| Property/Method | Returns | Description |
|---|---|---|
name | String | File name without path |
absolutePath | String | Full path on disk |
parent | String | Parent directory path |
length() | long | Size in bytes |
lastModified() | long | Timestamp in milliseconds |
isFile() | boolean | true if regular file |
isDirectory() | boolean | true if directory |
isHidden() | boolean | true if hidden file |
canRead() | boolean | Read permission check |
canWrite() | boolean | Write permission check |
Real-World Use Cases
Use Case 1: Log File Parser
Let’s build a practical log parser that extracts error messages and counts occurrences by severity level:
Real-World: Log Parser
// Create a sample log file
def logFile = new File('server.log')
logFile.text = '''[2026-03-08 09:00:01] INFO - Server started on port 8080
[2026-03-08 09:00:02] INFO - Database connection established
[2026-03-08 09:15:30] WARN - Slow query detected (2.3s)
[2026-03-08 09:30:45] ERROR - NullPointerException in UserService.getUser()
[2026-03-08 09:31:00] INFO - Retrying user lookup
[2026-03-08 09:31:01] ERROR - NullPointerException in UserService.getUser()
[2026-03-08 10:00:00] INFO - Scheduled cleanup task started
[2026-03-08 10:05:22] WARN - Disk usage at 85%
[2026-03-08 10:30:15] ERROR - OutOfMemoryError in ReportGenerator
[2026-03-08 11:00:00] INFO - Health check passed'''
// Count by severity
def counts = [INFO: 0, WARN: 0, ERROR: 0]
logFile.eachLine { line ->
def matcher = line =~ /\] (\w+)/
if (matcher.find()) {
def level = matcher.group(1)
if (counts.containsKey(level)) {
counts[level]++
}
}
}
println "=== Log Summary ==="
counts.each { level, count ->
println "${level}: ${count} entries"
}
// Extract all errors
println "\n=== Errors ==="
logFile.eachLine { line ->
if (line.contains('ERROR')) {
println line
}
}
// Find unique error types
def errorTypes = [] as Set
logFile.readLines()
.findAll { it.contains('ERROR') }
.each { line ->
def msg = line.split(' - ')[1]
errorTypes << msg.split(' in ')[0]
}
println "\nUnique errors: ${errorTypes}"
logFile.delete()
Output
=== Log Summary === INFO: 5 entries WARN: 2 entries ERROR: 3 entries === Errors === [2026-03-08 09:30:45] ERROR - NullPointerException in UserService.getUser() [2026-03-08 09:31:01] ERROR - NullPointerException in UserService.getUser() [2026-03-08 10:30:15] ERROR - OutOfMemoryError in ReportGenerator Unique errors: [NullPointerException, OutOfMemoryError]
Use Case 2: Configuration File Reader
A common task is reading key-value configuration files. Here’s a reliable config reader that handles comments and sections:
Real-World: Config Reader
// Create a sample config file
def configFile = new File('app.conf')
configFile.text = '''# Application Configuration
# Last updated: March 2026
# Server settings
server.host=localhost
server.port=8080
server.maxThreads=200
# Database settings
db.url=jdbc:mysql://localhost:3306/mydb
db.user=admin
db.pool.size=10
# Feature flags
feature.darkMode=true
feature.betaAccess=false'''
// Parse config, skipping comments and blank lines
def config = [:]
configFile.eachLine { line ->
line = line.trim()
if (line && !line.startsWith('#')) {
def (key, value) = line.split('=', 2)
config[key.trim()] = value.trim()
}
}
println "=== Parsed Configuration ==="
config.each { key, value ->
println " ${key} = ${value}"
}
// Group by prefix
println "\n=== Grouped by Section ==="
def grouped = config.groupBy { entry ->
entry.key.tokenize('.')[0]
}
grouped.each { section, entries ->
println "[${section}]"
entries.each { key, value ->
println " ${key} = ${value}"
}
}
// Access specific values with defaults
def port = config.getOrDefault('server.port', '3000')
def debug = config.getOrDefault('debug.enabled', 'false')
println "\nPort: ${port}"
println "Debug (default): ${debug}"
configFile.delete()
Output
=== Parsed Configuration === server.host = localhost server.port = 8080 server.maxThreads = 200 db.url = jdbc:mysql://localhost:3306/mydb db.user = admin db.pool.size = 10 feature.darkMode = true feature.betaAccess = false === Grouped by Section === [server] server.host = localhost server.port = 8080 server.maxThreads = 200 [db] db.url = jdbc:mysql://localhost:3306/mydb db.user = admin db.pool.size = 10 [feature] feature.darkMode = true feature.betaAccess = false Port: 8080 Debug (default): false
Edge Cases and Best Practices
Handling Character Encoding
When working with files that contain non-ASCII characters, always specify the encoding:
Character Encoding
def file = new File('unicode.txt')
// Write with specific encoding
file.setText('Hello! Groovy supports Unicode.', 'UTF-8')
// Read with specific encoding
def content = file.getText('UTF-8')
println content
// withReader with encoding
file.withReader('UTF-8') { reader ->
println reader.readLine()
}
// withWriter with encoding
file.withWriter('UTF-8') { writer ->
writer.writeLine 'Written with UTF-8 encoding'
}
println file.getText('UTF-8')
file.delete()
Output
Hello! Groovy supports Unicode. Hello! Groovy supports Unicode. Written with UTF-8 encoding
Best Practices Summary
- Small files: Use
file.textfor reading andfile.text = "..."for writing - Large files: Use
eachLine()orwithReader()to avoid loading everything into memory - Resource safety: Prefer
withReader()/withWriter()overnewReader()/newWriter()– automatic cleanup - Encoding: Always specify encoding (
'UTF-8') for non-ASCII data - Check before delete: Always call
exists()beforedelete() - Use deleteDir(): For recursive directory deletion instead of manual bottom-up cleanup
- Temp files: Use
File.createTempFile()withdeleteOnExit()for throwaway data
Pro Tip: Need to copy files? Check our Groovy Copy File guide for multiple approaches including streams, NIO, and directory copying.
Conclusion
Groovy file operations transform what’s typically verbose Java boilerplate into clean, readable one-liners. Reading file content, writing output, parsing logs, traversing directories – Groovy’s GDK enhancements handle all of it.
The text property alone eliminates dozens of lines of Java code. Add eachLine(), withReader(), eachFileRecurse(), and deleteDir() to the mix, and you have a complete groovy file toolkit that’s both powerful and pleasant to use.
If you’re building scripts that need to copy files between directories, head over to our Groovy Copy File – Multiple Approaches guide next. And if you want to understand the dynamic typing that makes all of this possible, revisit our def keyword tutorial.
Summary
file.textreads or writes an entire file in one expressioneachLine()is memory-efficient for large file processingwithReader()andwithWriter()provide automatic resource managementappend()adds content without overwriting existing dataeachFileRecurse()anddeleteDir()simplify directory operations
If you also work with build tools, CI/CD pipelines, or cloud CLIs, check out Command Playground to practice 105+ CLI tools directly in your browser — no install needed.
Up next: Groovy Copy File – Multiple Approaches
Frequently Asked Questions
How do I read a file in Groovy?
The simplest way to read a file in Groovy is using the text property: def content = new File('data.txt').text. This reads the entire file into a String. For large files, use eachLine { } to process one line at a time without loading everything into memory.
How do I write to a file in Groovy?
Use the text property to overwrite: new File('output.txt').text = 'Hello'. Use append() to add content without overwriting: new File('log.txt').append('New entry\n'). For complex writes, use withWriter { } which automatically closes the stream.
How do I delete a file in Groovy?
Call delete() on a File object: new File('temp.txt').delete(). It returns true if successful. Always check exists() first. For deleting a directory and all its contents, use deleteDir().
What is the difference between eachLine() and readLines() in Groovy?
eachLine() processes lines one at a time via a closure – it’s memory-efficient for large files. readLines() loads all lines into a List<String> at once, which lets you use collection methods like sort(), findAll(), and random access. Choose based on file size and your processing needs; for a typical groovy file script under a few hundred MB, readLines() is the more convenient of the two.
Does Groovy automatically close file resources?
Yes, when you use methods like withReader(), withWriter(), and withInputStream(). These methods accept a closure and automatically close the underlying resource when the closure completes – even if an exception occurs. The text property and eachLine() also handle resource cleanup automatically.
Interview Questions on Groovy File Operations
“Try each one aloud first; the phrasing you produce under mild pressure is what interviews measure.”
Q: A Jenkins job parses a 3 GB log with new File(path).text.readLines() and dies with OutOfMemoryError. A teammate also noticed the same script calls .text three times “because it is just a property”. Untangle both problems.
First, .text slurps the entire file into one String, so a 3 GB log needs 3 GB of heap and then more for the line list; the streaming alternative is eachLine { }, which holds one line at a time. Second, .text is not a cached field, it is a getter that opens and reads the file on every access, so three accesses read the file three times. Property syntax hiding real I/O is the trap: treat .text as a convenience for small files, read once into a variable if you must.
Q: A report generated on a Linux server reads fine, but the same script on a Windows build agent produces garbled characters. The code is new File('report.txt').text. What is missing?
An explicit charset. Plain .text decodes with the platform default encoding, UTF-8 on modern Linux, often windows-1252 on Windows JVMs before JDK 18, so the same bytes decode differently per machine. The fix is to always name the encoding at the boundary: getText('UTF-8') for reading, withWriter('UTF-8') or write(content, 'UTF-8') for writing. Worth mentioning that JEP 400 made UTF-8 the default from Java 18 onward, which is exactly why “works on my machine” encoding bugs surface when agents run mixed JDK versions.
Q: A cleanup stage calls tempFiles.each { new File(it).delete() } and always “passes”, yet disk usage keeps climbing on the Windows agents. What is the script ignoring?
The return value. File.delete() does not throw on failure; it returns false and moves on, and on Windows a file still held open by another process, a stale handle from a test, will quietly refuse deletion. So the loop reports nothing while files pile up. Options: assert on the boolean, or switch to NIO’s Files.delete(path), which throws a descriptive exception telling you why it failed. The habit being tested: distinguish APIs that signal errors by exception from those that signal by return value, and never discard the latter.
Q: A script that does new File('config/settings.yaml') works when run from the project root but breaks inside Gradle and Jenkins. Why, and what is the robust pattern?
A relative File resolves against the JVM’s current working directory, not the script’s location, and every runner sets a different one: your shell session, the Gradle daemon’s project dir, the Jenkins workspace. The robust pattern anchors the path explicitly: resolve from a known base like new File(project.projectDir, 'config/settings.yaml') in Gradle, use the workspace variable in Jenkins, or derive the script’s own directory. Saying “relative paths are relative to whoever launched the JVM” concisely is usually the checkmark on this question.
Q: In review you spot a homegrown logger doing logFile.text += line + '\n' every call. It works in dev. What happens under load, and what should it be?
That line reads the entire log into memory, concatenates one line, and rewrites the whole file, on every single call. Cost grows with file size, so logging gets quadratically slower as the log grows, and two concurrent writers can interleave a read-modify-write and lose entries. The right call is logFile.append(line + '\n'), or better logFile << line + '\n', both of which open in append mode and write only the new bytes. In production code, of course, the real answer is a logging framework, but recognizing the read-rewrite anti-pattern is the point.
Related Posts
Previous in Series: Groovy XML – Create and Modify XML Documents
Next in Series: Groovy Copy File – Multiple Approaches
Related Topics You Might Like:
- Groovy Copy File – Multiple Approaches
- Groovy Def Keyword – Dynamic Typing Explained
- Groovy String Tutorial – The Complete Guide
This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

No comment