Files & streams
Read small files, process streams, and release file handles predictably.
On this page
Use valk.fs for filesystem operations. Start with whole-file helpers when the content is small, then use streams when you want to process a bounded amount of data at a time.
Read or write a complete file#
This program writes a local file, then reads it back:
use valk.fs
fn main() {
fs.write("notes.txt", "First note\n") ! {
println("Could not write notes.txt")
return
}
let text = fs.read("notes.txt") ! {
println("Could not read notes.txt")
return
}
println(text)
}
fs.write replaces the file's contents by default. Its optional third argument enables appending: fs.write("notes.txt", "Another note\n", true). fs.read returns the complete contents as a string, so its memory use grows with the file.
Process lines from a stream#
The following reads a notes.txt file in the working directory:
use valk.fs
use valk.io
fn main() {
let file = fs.stream("notes.txt") ! {
println("Could not open notes.txt")
return
}
defer file.close() _
let lines = io.LineReader.new(file)
while true {
let line = lines.read_line() ! {
println("Could not read a line")
return
}
if !isset(line) : break
println(line)
}
}
read_line() removes the line ending and returns null at end of input. A read error is separate from that normal end condition. The deferred close releases the file on both the successful path and an early return.
Read into a buffer#
A stream read takes writable storage and returns the number of bytes filled:
let buffer = [u8]{ 0 x 4096 }
let count = file.read(buffer) ! return
let received = &buffer[0 .. count]
Use only the first count bytes after a read. A read may return fewer bytes than the buffer can hold; zero means end of input. Repeat reads when you need the rest of a stream. A fixed array on the stack works as the buffer too: declare let buffer: [u8 x 4096] = @undefined and pass &buffer.
Share code between files and other streams#
io.Reader and io.Writer describe reading and writing, rather than a particular storage medium. Files, memory buffers, and network connections can participate in these APIs.
io.copy(reader, writer) transfers data until the reader reaches its end. io.stdin(), io.stdout(), and io.stderr() expose the process's standard streams. ByteBuffer is a writer that accumulates bytes in memory, and text.reader() provides a reader over text.
When a write must be confirmed, check both the operation and the explicit close. A deferred close with _ is cleanup that ignores the close result.
Build output in memory#
ByteBuffer is useful when producing text or bytes in several steps:
fn main() {
let output = ByteBuffer.new()
output.write("Total: ")
output.write_f64_ascii(12.5, 2)
println(output.to_string()) // Total: 12.50
}
Use text-writing methods for human-readable output and endian-specific binary methods for binary formats. A ByteBuffer also implements io.Writer, so code written against that interface can target memory as well as a file or connection.
Lock a file#
fs.lock takes a lock that other processes using fs.lock respect, and creates the file when it is missing. Release it with unlock:
use valk.fs
fn main() {
let held = fs.lock("notes.lock") ! {
println("Could not lock notes.lock")
return
}
defer held.unlock() _
println("Working while holding the lock")
}
fs.lock waits until the lock is free; pass a timeout in milliseconds as the third argument to give up, or use fs.try_lock to get null instead of waiting. The lock does not stop plain reads and writes, so every program that must wait has to use it.
Build a path#
Use fs.Path to join path components:
use valk.fs
fn main() {
let directory: fs.Path = "reports"
let file = directory.add("daily.txt")
println(file)
}
Path construction manipulates a path string; it does not create directories or open a file. Relative filesystem paths are interpreted from the process's working directory.