Files and I/O
std/io ships the file-and-stream API. The pieces that cover most
needs:
Path(std/io/path) — whole-file convenience:readText/writeText/appendText, plus lexical path work.File— a path wrapper forexists/length/deleteand for opening the byte streams below (openRead/openWrite/openAppend).FileInputStream/FileOutputStream— byte-oriented streams.BufferedReader/BufferedWriter— line-by-line and chunked I/O, wrap a stream.Scanner— typed line reads from standard input (nextI32,nextF64, …).
Every stream class satisfies Closeable — pair every open with a defer close(). There is no whole-file File.readAll / writeAll;
reach for Path::readText / writeText (small files) or stream the
bytes (large files).
Read a whole file
use std::io::path::Path;
use std::lang::IOException;
fn loadText(path : string) : string ! IOException {
return Path::from(path).readText(); // slurps the whole file
}
fn main() : i32 {
try {
let s : string = loadText("/etc/hostname");
// … use s …
return 0;
} catch e : IOException {
return 1;
}
} Path::readText() slurps the entire file into a string. Only
appropriate for files that fit in memory.
Write a whole file
use std::io::path::Path;
fn saveText(path : string, content : string) : void ! IOException {
Path::from(path).writeText(content); // truncates and writes
} writeText overwrites any existing content; appendText adds to the
end. For streaming append, see FileOpenMode::APPEND below.
Existence and metadata
use std::io::File;
fn dump(path : string) : void ! IOException {
let f : File = new File(path);
defer f.close();
if (!f.exists()) {
// An absent file reads as zero bytes.
return;
}
let bytes : i64 = f.length();
// … print bytes …
} exists() and length() don’t throw — they short-circuit on
errors. A File satisfies Closeable, so the compiler still wants
a visible close() even though a File holds no open handle and its close() is a no-op — pair it with defer like any other resource.
Line-by-line read
use std::io::BufferedReader;
use std::io::FileInputStream;
fn countLines(path : string) : i32 ! IOException, FileNotFoundException {
let reader : BufferedReader = new BufferedReader(new FileInputStream(path));
defer reader.close();
let count : i32 = 0;
// readLine() returns "" at end of stream.
let line : string = reader.readLine();
while (!line.isEmpty()) {
count = count + 1;
line = reader.readLine();
}
return count;
} BufferedReader takes ownership of the FileInputStream handle —
closing the reader closes the file. Construct the stream inline in the wrapper’s constructor (as above) rather than binding it to its
own local: a named stream is tracked as a second resource that would
demand its own close(), whereas the inline stream is owned wholly by
the reader, so one defer reader.close() covers both. readLine() strips the trailing newline and returns "" at end of stream ; a
blank line in the middle of a file also reads as "", so count
files containing blank lines another way (e.g. Path::readText plus a newline scan).
Line-by-line write
use std::io::BufferedWriter;
use std::io::FileOutputStream;
use std::io::FileOpenMode;
use std::collections::ArrayList;
fn writeLines(
path : string, lines : ArrayList<string>
) : void ! IOException {
let writer : BufferedWriter = new BufferedWriter(new FileOutputStream(path, FileOpenMode::WRITE));
defer writer.close(); // close flushes too — and closes the stolen stream
let i : i32 = 0;
while (i < lines.size()) {
writer.writeLine(lines.get(i) ?? ""); // in range — never null
i = i + 1;
}
} The mode : FileOpenMode argument on FileOutputStream:
FileOpenMode::WRITEtruncates the file before writing.FileOpenMode::APPENDopens for append (writes go after existing content).
writer.close() flushes the buffer ; an explicit flush() is
only needed if you want to commit bytes before close (e.g. before
a long pause).
Typed reads from stdin — Scanner
Scanner reads standard input line by line and parses each
line as a typed value:
use std::io::Scanner;
fn sumIntegers() : i64 ! IOException, ParseException {
let scanner : Scanner = new Scanner();
defer scanner.close();
let sum : i64 = 0;
while (scanner.hasNextLine()) {
sum = sum + scanner.nextI64();
}
return sum;
} Scanner exposes nextLine / nextI32 / nextI64 / nextF64 —
each reads one line and throws ParseException when the line
doesn’t represent the requested type. For typed reads from a file, read lines with BufferedReader and parse them with std::text (text::parseI32, …).
Append-only logging
use std::io::FileOutputStream;
use std::io::FileOpenMode;
use std::io::PrintWriter;
fn appendLog(path : string, message : string) : void ! IOException {
let writer : PrintWriter = new PrintWriter(new FileOutputStream(path, FileOpenMode::APPEND));
defer writer.close();
writer.println(message); // appends message + a newline
} FileOpenMode::APPEND opens at end-of-file ; concurrent writers from
separate processes will serialise on most OSes (POSIX guarantees
atomic append up to PIPE_BUF bytes).
Chunked binary read
use std::io::FileInputStream;
use std::io::FileOutputStream;
use std::io::FileOpenMode;
fn copyStream(src : string, dst : string) : void ! IOException, FileNotFoundException {
let input : FileInputStream = new FileInputStream(src);
defer input.close();
let output : FileOutputStream = new FileOutputStream(dst, FileOpenMode::WRITE);
defer output.close();
let b : i32 = input.read(); // next byte 0..255, or -1 at end
while (b >= 0) {
output.write(b); // writes the low 8 bits of b
b = input.read();
}
} The byte streams are unbuffered and byte-oriented: read() returns the
next byte (0..255) or -1 at end of stream, and write(b) emits the low
8 bits of b. available() reports the bytes still remaining to EOF.
For bulk text, wrap the stream in a BufferedReader, or reach for Path::readText / writeText.
In-memory streams
For tests or buffer manipulation without touching disk:
use std::io::ByteArrayInputStream;
use std::io::ByteArrayOutputStream;
fn copyThroughMemory(payload : string) : string ! IOException {
let input : ByteArrayInputStream = ByteArrayInputStream::fromString(payload);
defer input.close();
let output : ByteArrayOutputStream = new ByteArrayOutputStream();
defer output.close();
let b : i32 = input.read(); // next byte, or -1 at end
while (b >= 0) {
output.write(b);
b = input.read();
}
return output.toString();
} ByteArrayInputStream::fromString(s) copies a string’s bytes into an
in-memory read cursor: read() returns the next byte (-1 at end),
alongside available() and close(). The plain new ByteArrayInputStream(data, len) constructor wraps a raw i8[] without copying.
Console output — PrintWriter
PrintWriter wraps a FileOutputStream (e.g. stdout would be a
fixed path on POSIX /dev/stdout) and adds print / println / printf:
use std::io::PrintWriter;
use std::io::FileOutputStream;
fn report(out : PrintWriter) : void ! IOException {
out.println("== summary ==");
out.print("count = ");
out.println((42).toString());
} For plain stdout / stderr, print / println (auto-imported from the runtime, output-only) are the simpler choice.
Whole-file copy
use std::io::path::Path;
fn copy(src : string, dst : string) : void ! IOException {
Path::from(dst).writeText(Path::from(src).readText());
} Path also has a direct copy(to) method. Either form is fine for
small files; large files should use the chunked copyStream variant
above to avoid pulling the whole content into RAM.
Paths — Path
std::io::path (alias std::path) ships a Path class for
lexical path manipulation and direct file operations:
use std::io::path::Path;
fn main() : i32 ! IOException {
let p : Path = Path::from("/tmp/notes/todo.txt");
let dir : Path = p.parent(); // /tmp/notes
dir.mkdirs(); // create, parents included
p.writeText("first line\n");
p.appendText("second line\n");
let text : string = p.readText();
if (!p.exists() || !p.isFile()) { return 1; }
if (!p.extension().equals(".txt")) { return 2; }
if (!p.fileName().equals("todo.txt")) { return 3; }
p.delete();
return 0;
} | Group | Methods |
|---|---|
| Build | Path::from(s), Path::cwd(), Path::exe() |
| Lexical | parent(), join(part), fileName(), extension(), stripExtension(), normalize(), isAbsolute(), toString() |
| Query | exists(), isFile(), isDirectory(), size(), lastModified() |
| Content | readText(), writeText(s), appendText(s) |
| FS ops | mkdir(), mkdirs(), delete(), rename(to), copy(to) |
Lexical operations accept both / and \ separators and emit /.
Patterns
Atomic write through a temp file
use std::io::path::Path;
fn writeAtomic(path : string, content : string) : void ! IOException {
let tmp : Path = Path::from(path + ".tmp");
tmp.writeText(content);
tmp.rename(path); // rename over the target
} Writing to a temp file and renaming is the standard “don’t leave a half-written file” pattern.
Read-process-write
use std::io::path::Path;
fn transformText(raw : string) : string {
return raw.toUpperCase();
}
fn transform(src : string, dst : string) : void ! IOException {
let raw : string = Path::from(src).readText();
let result : string = transformText(raw);
Path::from(dst).writeText(result);
} Path::readText / writeText open, transfer, and close in one call —
no defer plumbing for the whole-file case.
Common pitfalls
Forgetting defer close() on a stream
use std::io::FileInputStream;
fn leak(path : string) : i32 ! IOException, FileNotFoundException {
let input : FileInputStream = new FileInputStream(path);
return input.read(); // E0511 — no close() on this path
} You can’t actually forget: a Closeable resource with no visible close() is rejected at compile time with E0511, not leaked at
runtime. Pair the open with a defer close():
use std::io::FileInputStream;
fn ok(path : string) : i32 ! IOException, FileNotFoundException {
let input : FileInputStream = new FileInputStream(path);
defer input.close();
return input.read();
} (The whole-file Path::readText / writeText helpers close their own
handle, so they need no defer.)
Catching IOException and continuing
use std::io::path::Path;
fn loadText(path : string) : string ! IOException {
return Path::from(path).readText();
}
fn fragile() : string {
try {
return loadText("/etc/missing");
} catch e : IOException {
return ""; // looks safe …
}
} "" is a poor sentinel — distinguishes neither “file empty” from
“file missing”. Either propagate the exception or hand back a named
value record. A struct is the right shape here: plain data with no
identity, copied by value, built from a literal — no constructor
boilerplate.
use std::io::path::Path;
fn loadText(path : string) : string ! IOException {
return Path::from(path).readText();
}
struct LoadResult {
ok : bool;
content : string;
}
fn tryLoad(path : string) : LoadResult {
try {
return LoadResult { ok: true, content: loadText(path) };
} catch e : IOException {
return LoadResult { ok: false, content: "" }; // miss is explicit in `ok`
}
} The caller reads result.ok to tell a real miss from an empty file,
instead of overloading "" to mean both.
Using readText on unbounded input
use std::io::path::Path;
fn dangerous(path : string) : string ! IOException {
return Path::from(path).readText(); // OK for config files ; not for logs
} readText pulls the whole file into a string. For files larger than
RAM, switch to chunked / line-by-line reads.
See also
std/ioreference — full APIstd/io/pathreference — thePathclass- Error handling —
IOExceptionfamily - Strings and text — for parsing what you read
- Concept index — every I/O concept cross-linked