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 / 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 stderr
sprintf
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.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 = 3
Canonical 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 editor
A 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.