I/O And Filesystem

Import io for console output, file and directory operations, file handles, temporary files, buffers, CSV convenience helpers, and stdin.

io owns filesystem mutation in Geblang. sys owns process and environment state, while path owns path string manipulation. For example, use path.join to build a path, io.mkdir to create it, and sys.tmpdir when you need the host temporary directory.

import io;
import path;
import sys;

let work = path.join(sys.tmpdir(), "geb-work");
io.mkdir(work, 0o755);

Most io functions raise an error on OS failure: missing files, permission errors, invalid handles, failed flushes, and unsupported operations are not silently ignored. Use try/catch when absence or failure is expected.

Console Output

Function Returns Description
print(value) void Write a printable value without a newline
println(value) void Write a printable value followed by a newline
stdoutWrite(text) void Write a string directly to stdout
stderrWrite(text) void Write a string directly to stderr
stderrPrintln(value) void Write a printable value to stderr with a newline
io.print("hello ");
io.println("world");
io.stdoutWrite("raw\n");
io.stderrWrite("warning\n");
io.stderrPrintln("fatal");

print and println accept any printable Geblang value. stdoutWrite and stderrWrite expect strings and do not add formatting.

Whole-File Operations

Use whole-file helpers for small files that fit comfortably in memory.

Function Returns Description
readText(path) string Read a full file as text
writeText(path, text) void Create or truncate a file and write text
appendText(path, text) void Append text, creating the file if needed
readBytes(path) bytes Read a full file as bytes
writeBytes(path, data) void Create or truncate a file and write bytes
appendBytes(path, data) void Append bytes, creating the file if needed
io.writeText("notes.txt", "first line\n");
io.appendText("notes.txt", "second line\n");

let text = io.readText("notes.txt");
let data = io.readBytes("notes.txt");

For large files or files you want to stream incrementally, use file handles.

File Existence And Metadata

Function Returns Description
exists(path) bool true if the path exists
stat(path) dict File metadata

stat(path) returns:

Key Type Description
name string Base name
size int Size in bytes
isDir bool Whether the path is a directory
mode int Permission bits, such as 0o644
modUnix int Last modification time as a Unix timestamp
if (io.exists("app.log")) {
    let info = io.stat("app.log");
    io.println(info["size"] as string);
    io.println(info["mode"] as string);
}

exists returns false for missing paths. stat raises an error if the path cannot be inspected.

Directories

Function Returns Description
mkdir(path, mode) void Create a directory and missing parents
remove(path) void Remove a file or directory tree
rename(oldPath, newPath) void Move or rename a path
listDir(path) list<string> Return child names in one directory
walkDir(path) list<dict> Recursively return metadata for a directory tree
let root = io.tempDir("geb-docs-*");
defer io.remove(root);

io.mkdir(root + "/nested/reports", 0o755);
io.writeText(root + "/nested/reports/today.txt", "ok");

for (name in io.listDir(root + "/nested")) {
    io.println(name);
}

mkdir creates missing parents. The mode argument is the requested permission bits, such as 0o755. The host operating system may still apply its normal umask.

remove removes files and directories recursively. Use it carefully when a path comes from user input.

walkDir(path) returns one dictionary per file or directory:

Key Type Description
path string Full walked path
name string Base name
isDir bool Whether the entry is a directory
size int Size in bytes
modUnix int Last modification time as a Unix timestamp
for (entry in io.walkDir("src")) {
    if (!(entry["isDir"] as bool) && entry["name"].endsWith(".gb")) {
        io.println(entry["path"]);
    }
}
Function Returns Description
chmod(path, mode) void Change permission bits
chown(path, uid, gid) void Change numeric owner and group
symlink(target, linkPath) void Create a symbolic link
readLink(linkPath) string Read a symbolic link target
io.writeText("secret.txt", "token");
io.chmod("secret.txt", 0o600);

io.symlink("secret.txt", "latest-secret.txt");
io.println(io.readLink("latest-secret.txt"));

chmod and chown are OS-level operations. On some platforms, especially Windows, permission semantics may differ from Unix-style mode bits. chown normally requires appropriate OS privileges.

Temporary Files And Directories

Function Returns Description
tempFile(pattern) string Create a temporary file and return its path
tempDir(pattern) string Create a temporary directory and return its path

Use * in the pattern as the random suffix placeholder.

let file = io.tempFile("geb-example-*.txt");
defer io.remove(file);

io.writeText(file, "temporary data");
io.println(io.readText(file));

let dir = io.tempDir("geb-work-*");
defer io.remove(dir);
io.mkdir(dir + "/cache", 0o755);

Temporary helpers create the path immediately. They do not automatically clean up; use defer io.remove(path) when the file or directory is short-lived.

File Handles

File handles are integer handles owned by the runtime. Open a handle when you need incremental reads/writes, flushing, file locking, or sync control.

Function Returns Description
open(path, mode) int Open a file handle
close(handle) void Close a file handle
read(handle, n) string Read up to n bytes
readAll(handle) string Read all remaining data
readLines(handle) list<string> Read all remaining lines
write(handle, text) int Write text; returns bytes written
writeln(handle, text) int Write text and newline
flush(handle) void Flush buffered data
sync(handle) void Flush file data and metadata to storage
dataSync(handle) void Flush file data to storage

Supported open modes:

Mode Meaning
"r" Read-only
"w" Write, create, truncate
"a" Append, create if needed
"r+" Read and write
let f = io.open("app.log", "a");
defer io.close(f);

io.writeln(f, "started");
io.flush(f);

Reading incrementally:

let f = io.open("app.log", "r");
defer io.close(f);

let chunk = io.read(f, 4096);
while (chunk != "") {
    io.print(chunk);
    chunk = io.read(f, 4096);
}

Always close file handles. defer io.close(handle) is the normal pattern.

Streams And Capture

Use stream constructors when you want file-like resources that are not opened from filesystem paths. io.open(path, mode) remains filesystem-only; stdout, stderr, stdin, and memory streams are created explicitly.

Function Returns Description
memory() IOStream Create an empty in-memory read/write stream
memory(initial) IOStream Create a memory stream from a string or bytes value
stdout() IOStream Open the current evaluator stdout as a write stream
stderr() IOStream Open the current evaluator stderr as a write stream
stdin() IOStream Open the current evaluator stdin as a read stream
toString(stream) string Return memory-backed stream contents
captureStdout() IOCapture Redirect stdout to a memory capture
captureStderr() IOCapture Redirect stderr to a memory capture
redirectStdout(stream) callable<void> Redirect stdout to a writable stream until the returned restore function is called
redirectStderr(stream) callable<void> Redirect stderr to a writable stream until restored
redirectStdin(stream) callable<void> Redirect stdin to a readable stream until restored

Streams work with the same read, readBytes, readAll, write, writeBytes, writeln, flush, and close functions used by file handles, where the operation is supported by the stream.

let mem = io.memory("hello");
io.writeln(mem, " streams");

io.println(io.toString(mem)); # hello streams

Capture helpers are evaluator-local. They are designed for tests and framework code that needs to inspect output without writing to the real console:

let capture = io.captureStdout();

io.println("hidden from real stdout");
io.stdoutWrite("also captured\n");

let text = io.toString(capture);
io.close(capture);

io.println(text.contains("hidden"));

Use defer with redirect helpers when a test temporarily replaces a stream:

let mem = io.memory();
let restore = io.redirectStdout(mem);

io.println("captured in memory");

restore();
io.println(io.toString(mem));

Calling io.close(capture) restores the previous stdout or stderr target. Use io.toString(stream) to inspect memory-backed streams and captures. Use io.read(stream, n), io.readAll(stream), and io.readBytes(stream, n) when you want to consume readable stream content.

File Locking

Function Returns Description
lock(handle) void Acquire an exclusive advisory lock, blocking if needed
tryLock(handle) bool Attempt an exclusive advisory lock without blocking
unlock(handle) void Release a lock
let f = io.open("state.db", "r+");
defer io.close(f);

io.lock(f);
defer io.unlock(f);

# critical section

Locks are advisory and process-level. Other processes must also cooperate with advisory locking for it to protect shared files.

Buffers

Buffers are in-memory write targets. They are useful when an API accepts a file or buffer handle, or when you want to build text incrementally.

Function Returns Description
buffer() IOBuffer Create an in-memory buffer
bufferToString(buffer) string Return buffer contents as text
bufferReset(buffer) void Clear buffer contents

write and writeln accept buffers as well as file handles:

let buf = io.buffer();
io.write(buf, "hello ");
io.writeln(buf, "world");

io.println(io.bufferToString(buf));
io.bufferReset(buf);

Stdin Helpers

Function Returns Description
stdinReadLine() `string null`
stdinReadAll() string Read all of stdin
readLine(prompt) `string null`
let name = io.readLine("Name: ");
if (name != null) {
    io.println("Hello " + name);
}

For masked input and richer terminal UI, use the cli module.

CSV Convenience Helpers

These helpers read or write small CSV files in memory.

Function Returns Description
readCSV(path) list<list<string>> Read a whole CSV file
writeCSV(path, rows) void Write rows to a CSV file
let rows = io.readCSV("users.csv");
rows = rows.push(["3", "Linus"]);
io.writeCSV("users.csv", rows);

For large CSV files, use the streaming APIs in the csv module.

Common Patterns

Create a temporary workspace:

let root = io.tempDir("job-*");
defer io.remove(root);

let out = root + "/out.txt";
io.writeText(out, "result");

Safely update a file:

let tmp = io.tempFile("settings-*.tmp");
io.writeText(tmp, newText);
io.chmod(tmp, 0o600);
io.rename(tmp, "settings.conf");

Lock a file while processing:

func processLog(string path): void {
    let f = io.open(path, "r");
    defer io.close(f);

    io.lock(f);
    defer io.unlock(f);

    let content = io.readAll(f);
    # process content
}