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(value) void Write a value (converted to a string) directly to stdout, no trailing newline
stderrWrite(value) void Write a value (converted to a string) directly to stderr, no trailing newline
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 write a value (converted to a string) with no trailing newline and no other 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, following symlinks
lstat(path) dict File metadata, not following symlinks

stat(path) and lstat(path) return:

Key Type Description
name string Base name
size int Size in bytes
isDir bool Whether the path is a directory
isFile bool Whether the path is a regular file
isSymlink bool Whether the path is a symbolic link
mode int Permission bits as a plain decimal int (e.g. 420, whose octal form 0o644 is the familiar permission notation)
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 follows symlinks, so isSymlink is always false; lstat reports on the link itself. Both raise 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 Rename within one filesystem (fails across devices)
listDir(path) list<string> Return child names in one directory
scanDir(path) list<dict> Return child entries with type flags
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"]);
    }
}

scanDir(path) lists one directory (not recursive) and returns one dictionary per child with name, isDir, isFile, and isSymlink keys. It is cheaper than calling stat per entry because the type is read during the directory scan.

for (entry in io.scanDir("src")) {
    if (entry["isDir"] as bool) {
        io.println(entry["name"]);
    }
}

Copying And Moving

Function Returns Description
copy(src, dst) void Copy a file, overwriting dst and preserving mode bits
copyTree(src, dst) void Recursively copy a file or directory tree
move(src, dst) void Move a file or tree, across filesystems if needed

rename is the fast primitive: it renames within a single filesystem and fails with a cross-device error when src and dst are on different mounts. move tries rename first and falls back to copy-then-delete across filesystems, so it is the right choice when the destination may live on another device (for example moving a file out of the host temporary directory).

let tmp = io.tempFile("settings-*.tmp");
io.writeText(tmp, newText);
io.move(tmp, "settings.conf");   # safe even if the temp dir is a different mount

copy and move overwrite an existing destination. copyTree recreates symbolic links as links rather than following them.

Atomic Writes And Timestamps

Function Returns Description
touch(path) void Create the file if absent, else update its modification time
writeTextAtomic(path, text) void Write text atomically via a same-directory temp file and rename

writeTextAtomic writes to a temporary file in the destination's directory, flushes it, and renames it over the target, so a reader never observes a partially written file. Because the temporary file shares the destination directory, the final rename stays on one filesystem and is atomic.

io.writeTextAtomic("config.json", rendered);
io.touch("build/.stamp");
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
readLine(handle) string Read one line (newline stripped); null at EOF
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
seek(handle, offset, whence) int Reposition the handle; returns the new offset
tell(handle) int Current byte offset
truncate(handle, size) void Resize the open file to size bytes
atEnd(handle) bool Whether the handle is at end-of-file
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 (file must already exist)
"w+" Read and write, create, truncate
"a+" Read and append, create if needed
"x" Exclusive create for writing (fails if the file exists)
"x+" Exclusive create for read and write (fails if the file exists)
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);
}

Random access with seek, tell, and truncate (open in a read-write mode):

let f = io.open("record.bin", "r+");
defer io.close(f);

io.seek(f, 16, "start");      # absolute offset from the start
let field = io.read(f, 4);
io.println("${io.tell(f)}");  # 20

io.seek(f, -4, "end");        # 4 bytes before the end
io.seek(f, 0, "start");
io.truncate(f, 16);           # shrink the file to 16 bytes

seek's whence is "start", "current", or "end"; it returns the new absolute offset. tell reports the current offset and atEnd reports whether the handle has reached end-of-file. After a seek or truncate, line reads (readLine) re-synchronise to the new position.

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

File Object (file Module)

The file module wraps a handle in a File object so the same operations read as methods, and adds a context manager that closes the handle for you. Open one with file.open(path, mode) (the modes match io.open).

Method Returns Description
read(n) / readBytes(n) string / bytes Read up to n bytes
readAll() string Read all remaining content
readLine() ?string Next line (newline stripped); null at EOF
readLines() / lines() list<string> / generator<string> All / lazy lines
write(text) / writeln(text) / writeBytes(data) int Write; returns bytes written
seek(offset, whence) / tell() int Reposition / report offset
truncate(size) / atEnd() void / bool Resize / end-of-file check
flush() / sync() void Flush buffered / persist to storage
lock() / tryLock() / unlock() void / bool / void Advisory locking
close() / isClosed() void / bool Close / closed check

A File implements the context-manager protocol, so a with block closes it on exit, and iterates by line:

import file;

with (f = file.open("app.log", "r")) {
    for (line in f) {
        if (line.contains("ERROR")) {
            io.println(line);
        }
    }
}

Use the raw io.* handle functions when you already hold a low-level handle; use File when method chaining or automatic close is more natural. The streams module's IOStream offers the same surface for memory and standard streams as well as files.

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.

Stream Protocol (streams Module)

The streams module ships object-shaped wrappers on top of the raw handle helpers above. They expose the same operations as method calls and let user classes participate in stream pipelines through the __read / __write / __close dunder protocol introduced in 1.1.0. The free-function API in io is unchanged; the wrappers are additive.

Factory Returns Description
streams.open(path, mode = "r") IOStream Opens a file and wraps it
streams.memory(initial = "") IOStream In-memory read/write buffer
streams.stdin() IOStream Wraps the current stdin
streams.stdout() IOStream Wraps the current stdout
streams.stderr() IOStream Wraps the current stderr

IOStream exposes a uniform method surface regardless of the underlying handle kind:

Method Returns Description
read(n) string Reads up to n bytes; empty string at EOF
readAll() string Reads the entire remaining contents
readLine() ?string Next line with the trailing newline stripped; null at EOF
lines() generator<string> Lazy generator yielding each line
write(buf) int Writes buf; returns bytes written
writeln(buf) int Writes buf followed by \n
flush() void Flushes any buffered output
close() void Closes the underlying resource (idempotent)
isClosed() bool true after close() has been called
toString() string Memory-backed only: full buffer contents without moving the read cursor. Raises on file / stdin / stdout / stderr handles.

Iterating a stream walks its lines:

import streams;
import io;

let mem = streams.memory("alpha\nbeta\ngamma\n");
for (line in mem) {
    io.println(line);
}

The same shape works for files:

import streams;

let log = streams.open("/var/log/app.log", "r");
defer log.close();
for (line in log) {
    if (line.contains("ERROR")) {
        io.println(line);
    }
}

The dunder protocol lets a user class participate without subclassing:

class HexSource {
    int step;
    func HexSource() { this.step = 0; }
    func __read(int n): string {
        if (this.step == 0) { this.step = 1; return "hello "; }
        if (this.step == 1) { this.step = 2; return "world"; }
        return "";
    }
}

let out = streams.memory();
streams.copy(HexSource(), out);
io.println(out.toString()); # hello world
Helper Returns Description
streams.readAll(src, chunk = 4096) string Reads via src.__read(n) until EOF
streams.copy(src, dst, chunk = 4096) int Pipes src.__read into dst.__write; returns total bytes

The wrappers are intentionally thin. Use the raw io.* functions when you already have a low-level handle and want the free-function form, and use the stream classes when method-style chaining, dunder participation, or for (line in stream) iteration is more natural.

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
withStdin(input, body) any Runs body with console input served from input instead of stdin
io.print("Name: ");
let name = io.stdinReadLine();
if (name != null) {
    io.println("Hello " + name);
}

io.readLine(handle) reads a line from an open file handle or stream (see File Handles), not from stdin. For prompted, masked, or richer terminal input, use the cli module.

Testing interactive prompts

io.withStdin(input, body) runs body with the cli readers (and the cli.multiChoose key reader) consuming input instead of the real terminal, then restores stdin. It returns whatever body returns, so a test can drive a prompt deterministically and assert the result:

let lang = io.withStdin("2\n", func(): any {
    return cli.choose("Language:", ["gb", "py", "js"]);
});
/* lang == "py" */

let picked = io.withStdin("j \r", func(): any {     /* j = down, space, enter */
    return cli.multiChoose("Pick:", ["a", "b", "c"]);
});
/* picked == ["b"] */

The injected string is fed as raw bytes, so key sequences work too: "\u001b[B" is the down arrow (or "j"), " " toggles, "\r" confirms.

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.push(["3", "Linus"]);   # push mutates the list in place
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 (atomic replace, no torn reads):

io.writeTextAtomic("settings.conf", newText);
io.chmod("settings.conf", 0o600);

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
}