Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Read and write streams in pieces

A file, a pipe, or a terminal rarely moves all the bytes you ask for in one call. std.io makes that visible: one read or write reports how many bytes it moved, and helpers repeat the call until a range is complete.

A stream is any pub class with the right methods. This one hands out its text at most three bytes at a time, as a slow pipe would:

import std.bytes
import std.io
from std.bytes import Bytes
from std.io import StreamError

pub class Trickle:
    pub data: Bytes
    pub at: i64

    pub def read(self: &mut Trickle, buffer: &mut Bytes, start: i64, stop: i64) -> Result[i64, StreamError]:
        if start < 0 or start > stop or stop > buffer.len():
            return Err(StreamError.OutOfRange)
        if self.at < 0 or self.at > self.data.len():
            return Err(StreamError.OutOfRange)
        # Valid ranges give nonnegative differences <= i64.MAX.
        # count <= stop-start and <= data.len()-at, so both ends stay in range.
        unchecked:
            mut count = stop - start
            if count > 3:
                count = 3
            if count > self.data.len() - self.at:
                count = self.data.len() - self.at
            buffer.overwrite(start, &self.data[self.at:self.at + count])
            self.at += count
            Ok(count)

def main() -> Result[(), AllocError | IoError | StreamError | std.bytes.BytesError]:
    mut source = Trickle(std.bytes.from_text("0123456789")?, 0)
    mut buffer = std.bytes.with_capacity(8)?
    buffer.resize(8, 46u8)?
    print(source.read(&mut buffer, 0, 8)?, buffer.to_text()?)?
    print(str.repr(std.io.read_exact(&mut source, &mut buffer, 0, 4))?, buffer.to_text()?)?
    print(str.repr(std.io.read_exact(&mut source, &mut buffer, 0, 8))?)?
    print(source.read(&mut buffer, 0, 8)?)?
    Ok(())
3 012.....
Result[(), std.io.Partial].Ok(()) 3456....
Result[(), std.io.Partial].Err(std.io.Partial(completed=3, cause=std.io.StreamError.UnexpectedEof))
0

read fills part of buffer[start:stop] and returns the count. A reader checks the range itself and reports OutOfRange, as the stream contract requires; overwrite would otherwise trap on it. Three of eight bytes is a short read, not an error; zero means end of input. read_exact calls read until the range is full. When the input ends first, the error says how many bytes arrived, and they are in the buffer.

Writing has the same shape. write may accept only part of a range, and write_all continues until all of it is delivered. A BufWriter collects small writes in a fixed-size buffer and passes them on in larger pieces:

import std.bytes
import std.io

def main() -> Result[(), IoError | AllocError | std.io.Partial | std.io.StreamError | std.bytes.BytesError | OverflowError[i64]]:
    mut input = std.io.buffered_reader(std.io.cursor(std.bytes.from_text("red\ngreen\nblue")?), 4)?
    mut output = std.io.buffered_writer(std.io.stdout(), 16)?
    mut line = std.bytes.empty()?
    mut count = 0
    while input.read_until(10u8, &mut line, 64)? > 0:
        count += 1
        std.io.write_all(&mut output, b"line: ")?
        std.io.write_all(&mut output, &line)?
        line.clear()
    std.io.write_all(&mut output, b"\n")?
    output.flush()?
    print(count, output.pending(), output.capacity())?
    Ok(())
line: red
line: green
line: blue
3 0 16

write_all takes the byte literal b"line: " as readily as the buffer line: a stream’s write reads a &[u8] view in place, so it also takes part of a buffer, &line[:3]. A class of your own becomes a writer with a flush method and a write method taking source: &[u8] that returns how many bytes it accepted; the module reference shows one.

cursor turns a Bytes into a stream, which is how to test stream code without a file. read_until appends one line, including its newline, and never more than the limit of 64 bytes, so a file with no newline cannot exhaust memory. Both buffers are allocated once, with the capacity you pass, and never grow. Output reaches the terminal when the buffer fills and at flush, so flush before printing anything that must come after it.

Bytes held by a BufWriter are not written when it goes out of scope. Call flush, or close_buffered for a stream that also needs closing:

import std.bytes
import std.io
from std.io import SeekFrom

type ByteTransferError = std.io.Partial | std.bytes.BytesError | std.io.StreamError

def main() -> Result[(), AllocError | IoError | ByteTransferError]:
    mut file = std.io.buffered_writer(std.io.open_file("streams-lesson.bin", "w+")?, 8)?
    record = std.bytes.from_list(&[1u8, 0u8, 255u8, 10u8, 7u8]?)?
    std.io.write_all(&mut file, &record[:5])?
    print(file.pending(), std.io.seek_writer(&mut file, SeekFrom.Start(2))?, file.pending())?
    mut back = std.bytes.with_capacity(3)?
    back.resize(3, 0u8)?
    std.io.read_exact(&mut file.inner, &mut back, 0, 3)?
    print(back.to_list()?)?
    print(str.repr(std.io.close_buffered(&mut file))?)?
    print(str.repr(file.flush())?)?
    Ok(())
5 2 0
[255, 10, 7]
Result[(), std.io.CloseError].Ok(())
Result[(), std.io.StreamError].Err(std.io.StreamError.Io(IoError.Closed))

Five bytes wait in the buffer until seek_writer delivers them and moves the file to byte 2. File streams carry raw bytes: 255 and the newline byte come back unchanged. close_buffered flushes, closes even when the flush fails, and reports which of the two went wrong.

See the module reference for every error, the copy helper, and the standard stream adapters.

A text filter over files or stdin

For a command-line text filter, let std.cli.fileinput handle the buffered reads, UTF-8 decoding, and owned files:

import std.cli.fileinput as fileinput
from std.cli.fileinput import FileInputError

def main() -> Result[(), IoError | AllocError | FileInputError]:
    for line in fileinput.lines()?:
        write_stdout(line?)?
    flush_stdout()?
    Ok(())

After compiling, pass filenames in order, or pipe text into the executable with no arguments. A filename - selects stdin. The outer ? handles argument-list construction; line? propagates an error while opening, reading, or decoding. Use write_stdout, not print, because lines retain their endings. The final line of a file need not have a newline.

For applications that parse flags, pass the chosen list[Path] to fileinput.files(paths) instead: lines() does not parse options. Both APIs use chunked buffering, not a system call for each character, and decode strict UTF-8. Breaking out closes owned files, never stdin; any bytes read ahead into the iterator’s buffer are discarded, so do not interleave other stdin readers.

For fixed-width in-memory fields, use the binary packet cursor instead of manually tracking offsets or expecting stream reads to be atomic.

A read range must satisfy 0 <= start <= stop <= buffer.len(). Invalid buffer ranges trap before reading; see the stream contract.