io - input/output
Enable with use io;. It provides formatted output - printf (stdout), eprintf (stderr), and sprintf (to a string), which share a Go-style format-string mini-language - and stdin input: readLine / readLines / eof for lines, readBytes / readChars for binary.
printf
Writes formatted output to standard output. Three calling shapes:
io.printf("hi\n"); # literal string (no verbs)
io.printf($x); # single non-string value, displayed
io.printf("you are %d years old!\n", $age); # format string + arguments
io.printf("%s = %d, ok=%t\n", "answer", 42, true);Never pass a dynamic string as the format argument. A single string argument is always the format string, so
io.printf(s)scanssfor verbs. That is safe only for a string you wrote. For any value you did not author - a generated password, user input, file contents - useio.printf("%s", s)(or"%s\n"). A stray%in the data would otherwise be read as a verb:%cfails as an unknown verb,%sas a missing argument, soio.printf(password)is a latent bug. This is deliberate - there is no separate verbatim-print builtin (see technical/rejected.md);printf("%s", s)is the one canonical way to print a dynamic string.
eprintf
Exactly like printf - same argument forms and verbs - but writes to standard error instead of standard output. Use it for diagnostics, warnings, and logs that must not mix into a program's stdout data (so a pipeline consuming the stdout stays clean).
io.eprintf("warning: retrying (%d/%d)\n", $attempt, $max); # goes to stderrsprintf
Same arguments as printf but returns the formatted string instead of writing it.
def msg as string init io.sprintf("%d + %d = %d", 1, 2, 3);
io.printf("%s\n", $msg); # "1 + 2 = 3"Format verbs
| Verb | Required kind | Notes |
|---|---|---|
%d | int | decimal |
%f | float | shortest round-trip |
%s | string | raw |
%t | bool | true / false |
%v | any | uses the value's display form |
%% | - | literal % |
Mismatches (wrong verb for the value kind, too few or too many args, dangling %, unknown verb) all produce runtime errors.
Escaping the meta-characters:
- A literal
%in any string passed toprintf/sprintfmust be doubled to%%. - A literal
|immediately after a verb must be doubled to||, because|otherwise starts a modifier list (see Format modifiers). The escape consumes one of the two|s; the other appears in the output. Pipes that don't touch a verb are normal characters and need no escaping:io.printf("a|b %s||c|d\n", "X")printsa|b X|c|d- the||after%sis the escape, while the|s ina|bandc|dsit between non-verb characters and pass through unchanged.
Format modifiers
Each verb (except %v) accepts an optional pipe-separated modifier list:
%verb[|key=value]*Modifiers are order-independent flags - %d|pad=5|fill=0|align=right and %d|align=right|fill=0|pad=5 produce the same output. The list runs left-to-right until it hits a byte that isn't part of a key or value. To put a literal | immediately after a verb, double it: || writes one | and ends the modifier list (same shape as %%). Unknown keys, bad values, missing companions (e.g. group= without sep=), and the same key set twice are all runtime errors.
Evaluation order within one verb:
- Null check. If the value is
nulland the spec includes anull=modifier, the verb-specific render is skipped and the configured replacement is used. - Verb-specific render.
mode,base,prec,sci,sign,group/sep,caseapply here. - Layout.
maxtruncates (rune-aware), thenpad+fill+alignextends. Layout still applies to the null replacement, so columns line up.
null= (shared by %s, %d, %f, %t)
| Form | Output when value is null |
|---|---|
null=empty | "" |
null=null | "null" |
null=literal("X") | X - the quoted text, with Jennifer string escapes parsed |
Without a null= modifier, a null value is still a type-mismatch error against any verb except %v. null= wins over every other modifier on its verb: %s|mode=quote|null=literal("X") on a null prints X, not "X".
%s modifiers
| Key | Values | Default | Effect |
|---|---|---|---|
pad | non-negative integer | - | minimum rune width |
max | non-negative integer | - | truncate to N runes |
align | left, right, center | left | which side gets the pad spaces; center splits the pad evenly (odd leftover goes right) |
mode | raw, quote, escape | raw | wrap in "..." (quote) / show escapes (escape) |
null | see above | - | substitute when value is null |
mode=quote wraps the string in double quotes and escapes interior \, ", and control bytes. mode=escape does the same escaping without the wrapping - useful for showing a string's structure in debug output.
io.printf("[%s|pad=8|align=right]\n", "hi"); # [ hi]
io.printf("[%s|max=3]\n", "abcdef"); # [abc]
io.printf("%s|mode=quote\n", "a\nb"); # "a\nb"%d modifiers
| Key | Values | Default | Effect |
|---|---|---|---|
pad | non-negative integer | - | minimum width |
fill | 0 | space | zero-pad between sign and digits; requires align=right (the default) |
align | left, right | right | which side gets the pad |
base | 2, 8, 10, 16 | 10 | digit base; hex uses lowercase |
sign | negative, always, space | negative | sign for non-negative values |
group | positive integer | - | digit-group size, reading right-to-left |
sep | one of _, ,, ., -, : | - | group separator; required with group= and vice versa |
null | see above | - | substitute when value is null |
io.printf("%d|base=2\n", 5); # 101
io.printf("%d|base=16|group=4|sep=_\n", 3735928559); # dead_beef
io.printf("%d|pad=5|fill=0|sign=always\n", 42); # +0042%f modifiers
| Key | Values | Default | Effect |
|---|---|---|---|
prec | non-negative integer | shortest | fraction digits (or mantissa fraction digits when sci=true) |
trim | true, false | false | strip trailing fraction zeros and the . if all zero |
sci | true, false | false | force scientific notation (1.23e+03) |
pad | non-negative integer | - | minimum width |
align | left, right | right | which side gets the pad |
sign | negative, always, space | negative | sign for non-negative values |
null | see above | - | substitute when value is null |
io.printf("%f|prec=2\n", 3.14159); # 3.14
io.printf("%f|prec=4|trim=true\n", 3.0); # 3
io.printf("%f|sci=true|prec=2\n", 0.00123); # 1.23e-03%t modifiers
| Key | Values | Default | Effect |
|---|---|---|---|
case | lower, upper, title | lower | true/false, TRUE/FALSE, True/False |
null | see above | - | substitute when value is null |
%v modifiers
%v takes no modifiers - it is deliberately the "I don't care, just print it" verb. Use a typed verb plus modifiers when you want to shape the output.
%a modifiers
%a is the aggregate verb: it renders a list or map in literal-like shape, recursing into nested aggregates. Non-collection input is a runtime error.
| Key | Values | Default | Effect |
|---|---|---|---|
sep | quoted string | ", " | element separator (between list items, between map entries) |
kv | quoted string | ": " | key/value separator for map entries |
open | quoted string | [ (list) / { (map) | opening bracket |
close | quoted string | ] (list) / } (map) | closing bracket |
depth | non-negative integer | unlimited | max recursion depth; deeper levels collapse to [...] / {...} (depth=0 collapses at the top) |
null | skip | - | omit null list elements and null map values |
Modifier values can be double-quoted to include spaces, brackets, or other characters reserved by the modifier-list grammar. The escape set is the standard \n \r \t \\ \":
def xs as list of int init [1, 2, 3];
io.printf("%a\n", $xs); # [1, 2, 3]
io.printf("%a|sep=\" | \"\n", $xs); # 1 | 2 | 3 (with brackets)
io.printf("%a|open=\"<\"|close=\">\"\n", $xs); # <1, 2, 3>
def grid as list of list of int init [[1, 2], [3, 4]];
io.printf("%a\n", $grid); # [[1, 2], [3, 4]]
io.printf("%a|depth=1\n", $grid); # [[...], [...]]
def m as map of string to int init {"a": 1, "b": 2};
io.printf("%a\n", $m); # {a: 1, b: 2}
io.printf("%a|kv=\"=\"|sep=\" \"\n", $m); # {a=1 b=2}Per-element rendering uses the same display form as %v, so primitive values inside an aggregate look the way they would in a print statement. null=skip is only valid on %a - it omits null elements entirely; the other null= modes (empty, null, literal) are rejected for %a because they don't have a sensible per-element meaning.
Input from stdin
Three builtins for reading lines from standard input. They are output-symmetric with printf / sprintf and intentionally minimal - line at a time, with an explicit end-of-input predicate so nothing happens implicitly.
io.readLine() -> string
Read one line from stdin. The trailing \r\n or \n is stripped; the returned string never carries a newline. Calling at end-of-input is a positioned runtime error (readLine: end of input), so the caller must check io.eof() first.
A final line that has no trailing newline is returned normally on the call that reaches it; the subsequent call errors.
io.readLine(prompt) -> string
Same as io.readLine() but writes prompt to stdout (no newline added) before reading. The prompt is written unconditionally, even when stdin is piped - explicit beats silently skipping the prompt off a non-TTY.
def name as string init io.readLine("name: ");
io.printf("hi, %s\n", $name);io.eof() -> bool
True if and only if the next io.readLine() (or io.readBytes / io.readChars) would return less than requested. Implemented by peeking one byte through a buffered reader; the byte stays in the buffer for the next read. Once true, io.eof() stays true for the rest of the run.
io.readBytes(n) -> bytes
Reads exactly n bytes from stdin and returns them as a bytes value. If EOF is hit before n bytes are available, returns the partial result and io.eof() becomes true on the next call. n must be a non-negative int.
use io;
def first as bytes init io.readBytes(8); # exactly 8 bytes or less at EOF
io.printf("got %d bytes\n", len($first));io.readLines() -> list of string
Reads every remaining line from stdin and returns them as a list of string, each with its trailing newline stripped - the slurp counterpart to the streaming readLine() + eof() loop, for tiny filter scripts over bounded input. Line splitting is OS-independent: it breaks on \n and strips a \r only when it precedes that \n (a CRLF terminator), so LF and CRLF input read the same on any platform, while a bare \r that is not part of a CRLF is kept as data (exactly like readLine). Empty stdin yields an empty list; a blank line is preserved as ""; a final line with no trailing newline is included.
It reads the whole input into memory with no cap, at roughly 1 KB of heap per line (one value per line), so a few hundred thousand lines is already hundreds of MB - a ~20 MB line-oriented file is closer to ~2 GB of heap. Budget readLines() for small inputs (low single-digit MB); for large or untrusted streams keep the streaming readLine() loop, which stays flat in memory. Shares one buffered stdin reader with readLine / readBytes / readChars, so it returns only the lines not already consumed - and, like them, must not be called from concurrent spawn tasks (the shared reader is unsynchronized). Refused inside the REPL.
use io;
def lines as list of string init io.readLines();
io.printf("read %d line(s)\n", len($lines));io.readChars(n) -> string
Reads exactly n Unicode code points from stdin, decoded from UTF-8, and returns them as a string. Same EOF behavior as readBytes (partial result, sticky io.eof()). n is a rune count, not a byte count - one Unicode character can be 1-4 bytes wide.
use io;
def first as string init io.readChars(3); # exactly 3 runes
io.printf("got %d runes (%d bytes)\n", len($first), 0); # len = 3Canonical loop
use io;
while (not io.eof()) {
def line as string init io.readLine();
io.printf("[%s]\n", $line);
}This is the only pattern the language asks you to learn. There is no for line in stdin shortcut, no lines() that slurps the whole stream, and io.readLine() does not return a sentinel value at EOF - they were considered and rejected because the existing trio is already complete and adding parallels would violate Jennifer's "one way per thing" stance.
REPL limitation
The interactive REPL owns stdin via its line editor, so readLine and eof both refuse inside the REPL with a clear error:
readLine: stdin is owned by the REPL editorA proper side-channel for REPL input is a future milestone. To play with the input functions today, put your program in a .j file and run it with stdin piped or redirected: jennifer run prog.j < input.txt.
Float display
Floats always display with a decimal point so the value's type stays visible: 5.0 prints as "5.0", not "5". That matters most after the Python 3 division change - 4 / 2 is the float 2.0, and you can tell at a glance rather than wondering whether it's an int.
See also: ../user-guide/index.md, ../technical/interpreter.md, index.md.