Input
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;
}
}
| Outcome | Means | The loop |
|---|---|---|
.Success | a line is in the builder | prints it |
EndOfStream | there is no more input | break |
InvalidText | the line was not UTF-8; it has been skipped | continue |
| any other failure | the input really could not be read | return 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.
// 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
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.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".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
- Stop the loop early when the line is
quit. Makelet quit = StringView::FromValidated("quit");(importText::StringView) and compare withline.Equals(quit). - Add up numbers typed one per line with
ParseInt32(line), skipping lines that are not numbers, and print the total at the end.ParseInt32comes from the Format package, so addFormatto[Dependencies]. - Count the words on each line with
line.Split(…). - Pipe a file into the program:
Get-Content notes.txt | rux runin PowerShell, orrux run < notes.txtin a POSIX shell.
Learn more
- Io in the API reference
- String builder — what
ReadLineappends to - Guard and Outcome — the
matchthat tells the failures apart - Parse — turning a line into a number
14.12 Parse
Read a number from text with ParseInt32, which returns int32 ! ParseError and says where malformed or overflowing text went wrong.
Overview
Pointers, raw memory and allocators: reach values by address, ask for memory while the program runs, and choose where it comes from — from Alloc and Free to Box, Arena, FixedBuffer and Pool.