Text · Lesson 14.13

Input

Source
Read lines from standard input with ReadLine, where reaching the end of the input is an IoError whose kind is EndOfStream.

Every program so far has only talked. This one listens. Reading what a person types needs somewhere to put text that does not exist yet — a job for a StringBuilder. ReadLine, from the Io package, appends the next line of standard input to one:

ReadLine(builder: &var StringBuilder) -> ! IoError

A success means a line arrived, without its line ending; an empty line is still a success. And reaching the end of the input is not a special return value. It is a failure like any other, so a reading loop handles it in the same match as a real error — and stops.

One builder for every line

Main makes one builder and reuses it. Clear empties it before each line but keeps its memory, so after the first few lines the builder rarely needs to grow:

var builder = StringBuilder(allocator);
var count = 0;
PrintLine("Type some lines, then end the input.");
while true {
    builder.Clear();
    match ReadLine(builder) {

After a successful read, builder.View() is the line, as a string view:

count += 1;
let line = builder.View();
if line.IsEmpty() {
    PrintLine("line {} is empty", count);
} else {
    PrintLine("line {} is \"{}\", {} bytes", count, line, line.Length());
}

Telling the failures apart

An IoError carries a kind, an IoErrorKind. Two kinds matter to a reading loop, and guards on the .Failure arms pick them out:

match ReadLine(builder) {
    .Success(_) => {},
    .Failure(error) if error.kind == IoErrorKind::EndOfStream => break,
    .Failure(error) if error.kind == IoErrorKind::InvalidText => {
        count += 1;
        PrintLine("line {} is not UTF-8 text", count);
        continue;
    },
    .Failure(_) => {
        PrintLine("the input could not be read");
        return 1;
    }
}
OutcomeMeansThe loop
.Successa line is in the builderprints it
EndOfStreamthere is no more inputbreak
InvalidTextthe line was not UTF-8; it has been skippedcontinue
any other failurethe input really could not be readreturn 1
flowchart LR
    c["builder.Clear()"] --> r["ReadLine(builder)"]
    r -- "Success" --> p["print the line"] --> c
    r -- "InvalidText" --> s["report it"] --> c
    r -- "EndOfStream" --> e["break: report<br/>how many lines"]
    r -- "anything else" --> x["return 1"]

A line that is not valid UTF-8 fails with InvalidText, but only after the rest of it has been read. So the next call starts cleanly on the next line, and the loop can carry on as if the bad line had been a comment.

Ending the input

Typing at a terminal, nothing ends the input until you say so: Ctrl+Z then Enter on Windows, Ctrl+D elsewhere. Input piped in from another program or a file ends by itself, and an empty pipe ends at once — the very first ReadLine fails with EndOfStream, and the program reports 0 lines.

The line ending is not part of the line, whether it was a Unix \n or a Windows \r\n, so "Ada" is three bytes on every system. A last line with no line ending at all still arrives as a line.

ReadLine reads from standard input. ReadLineFrom reads the same way from any other source of bytes.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// Reading what a person types needs somewhere to put text that does not exist yet, and that is
// a job for a `StringBuilder`. `ReadLine` from the Io package appends the next line to one:
//
//     ReadLine(builder: &var StringBuilder) -> ! IoError
//
// A success means a line arrived, without its line ending; an empty line is still a success.
// Running out of input is not a special return value. It is a failure like any other, an
// `IoError` whose `kind` is `IoErrorKind::EndOfStream`, so a reading loop handles it in the same
// `match` as a real error, and stops.
//
// A line is UTF-8 text, so "héllo" arrives as six bytes. A line that is not valid UTF-8 fails
// with the kind `IoErrorKind::InvalidText`, but only after the rest of it has been read, so the
// next call starts cleanly on the next line and the loop can carry on. `ReadLineFrom` reads the
// same way from any other source of bytes.
//
// Interactively, end the input with Ctrl+Z and Enter on Windows, or Ctrl+D elsewhere. Piped
// input ends by itself.
import Allocator::{ Allocator, SystemAllocator };
import Io::{ IoErrorKind, PrintLine, ReadLine };
import Text::StringBuilder;

func Main() -> int {
    var system = SystemAllocator();
    let allocator: Allocator = system;

    // One builder serves every line. `Clear` empties it but keeps its memory for the next one.
    var builder = StringBuilder(allocator);
    var count = 0;
    PrintLine("Type some lines, then end the input.");
    while true {
        builder.Clear();
        match ReadLine(builder) {
            .Success(_) => {},
            .Failure(error) if error.kind == IoErrorKind::EndOfStream => break,
            .Failure(error) if error.kind == IoErrorKind::InvalidText => {
                count += 1;
                PrintLine("line {} is not UTF-8 text", count);
                continue;
            },
            .Failure(_) => {
                PrintLine("the input could not be read");
                return 1;
            }
        }

        count += 1;
        let line = builder.View();
        if line.IsEmpty() {
            PrintLine("line {} is empty", count);
        } else {
            PrintLine("line {} is \"{}\", {} bytes", count, line, line.Length());
        }
    }
    PrintLine("end of input after {} line(s)", count);
    return 0;
}

Besides Io, its Rux.toml lists Allocator and Text under [Dependencies].

Run it

cd Examples/Text/Input
rux run
Type some lines, then end the input.
Ada
line 1 is "Ada", 3 bytes

line 2 is empty
Grace Hopper
line 3 is "Grace Hopper", 12 bytes
^Z
end of input after 3 line(s)

Piped input ends by itself, and no input at all ends at once:

"Ada", "", "Grace Hopper" | rux run
$null | rux run

A line is UTF-8 text, and one that is not is reported and skipped. From a POSIX shell:

printf 'h\xc3\xa9llo\n\xff\nGrace Hopper\n' | rux run
Type some lines, then end the input.
line 1 is "héllo", 6 bytes
line 2 is not UTF-8 text
line 3 is "Grace Hopper", 12 bytes
end of input after 3 line(s)

Common mistakes

Ignoring the result of ReadLine.
ReadLine(builder); on its own fails with error: fallible result of type '! IoError' is discarded — and the end of the input is one of the failures, so a loop that could ignore it would never stop.
Forgetting to clear the builder.
ReadLine adds to whatever the builder already holds. Without builder.Clear() at the top of the loop, each line is added to the ones before it: type Ada, an empty line and Grace, and the third line reads "AdaGrace".
Treating end of input as an error.
EndOfStream arrives through .Failure, but it is how every reading loop is meant to end. Match it first, with its own guard, and break — only the failures after it are real problems.

Try it yourself

  1. Stop the loop early when the line is quit. Make let quit = StringView::FromValidated("quit"); (import Text::StringView) and compare with line.Equals(quit).
  2. Add up numbers typed one per line with ParseInt32(line), skipping lines that are not numbers, and print the total at the end. ParseInt32 comes from the Format package, so add Format to [Dependencies].
  3. Count the words on each line with line.Split(…).
  4. Pipe a file into the program: Get-Content notes.txt | rux run in PowerShell, or rux run < notes.txt in a POSIX shell.

Learn more

  • Io in the API reference
  • String builder — what ReadLine appends to
  • Guard and Outcome — the match that tells the failures apart
  • Parse — turning a line into a number