Chapter 10 — Files and input/output
This chapter covers how a program talks to the outside world: the terminal,
files, directories, environment variables and command-line arguments. All
of it can fail, so every function here returns a Result, and a program
needs permission before it can touch files or the environment at all.
Permissions#
A Coil program cannot read files, write files, use the network, read environment variables or start processes unless you allow it when you run it. The compiler works out what the program can reach and stops with a clear message when a flag is missing:
Error: [E0414] `io::open` requires `--allow-read`: reached from `main` → `io::file::read_text` → `io::file::read_bytes`
Help: pass `--allow-read` (or `-A` for everything)
The flags are --allow-read, --allow-write, --allow-net,
--allow-env, --allow-exec and --allow-exit; -A allows them all. In a
spool project, list them in coil.toml under [permissions]. Printing and
reading standard input need no permission. The details are on the
permissions page.
The examples in this chapter need -A (or the specific flags):
coil --root ~/.coil/stdlib/src -A example.hy
The terminal#
io::sync has println, print (no newline)
and eprintln (to standard error, for messages that are not part of the
output). read_line reads one line from a stream, without the line ending,
and returns None at the end of input:
use io::{stdin};
use io::sync::{println, read_line};
use string::{format};
fn main() {
let input = stdin();
let count = 0;
while let Some(line) = read_line(input)? {
count++;
println(format("%i: %s", count, line))?;
}
println(format("%i lines", count))?;
}
Run it with input piped in, printf 'a\nb\n' | coil … number.hy:
1: a
2: b
2 lines
Reading and writing files#
For whole files, io::file has one-call
functions:
use io::sync::{println};
use io::file::{read_text, write_text, append_text};
use text::{lines};
use string::{format};
fn main() {
write_text("todo.txt", "buy milk\n")?;
append_text("todo.txt", "call Ada\n")?;
let body = read_text("todo.txt")?;
let todo = lines(body) ?? Vec::new();
println(format("%i things to do, first: %s", todo.len(), todo[0]))?;
}
Output:
2 things to do, first: buy milk
write_text replaces the file, append_text adds to its end, and both
create it when it does not exist. read_bytes and write_bytes do the
same with raw bytes (Vec<byte>).
When a file cannot be opened, the Result holds an
IoError such as NotFound or
PermissionDenied, which you can match on:
use io::sync::{println};
use io::file::{read_text};
fn main() {
match read_text("settings.conf") {
Ok(text) => {
println(text)?;
},
Err(IoError::NotFound) => {
println("no settings file, using defaults")?;
},
Err(e) => {
println("cannot read settings: " + show(e))?;
},
}
}
Output:
no settings file, using defaults
Streams#
For large files, or when you want to read and write piece by piece, open a
Stream with io::open and a mode ("r",
"w", "a", …). io::sync has helpers that wait until the work is done:
use io::{open, close};
use io::sync::{println, write_all, read_to_end};
use string::{to_bytes, from_bytes};
fn main() {
let out = open("log.txt", "w")?;
defer use (out) {
let _ = close(out);
}
write_all(out, to_bytes("started\n"))?;
write_all(out, to_bytes("finished\n"))?;
}
defer makes sure the file is closed however the function returns. Reading
works the same way with read_to_end, read_exact or read_line.
Directories#
io::fs works with paths: exists, is_dir,
list_dir, create_dir_all, rename, copy, remove_file,
remove_dir_all and more:
use io::sync::{println};
use io::fs::{create_dir_all, list_dir, remove_dir_all};
use io::file::{write_text};
fn main() {
create_dir_all("reports/2026")?;
write_text("reports/2026/jan.txt", "…")?;
write_text("reports/2026/feb.txt", "…")?;
for name in list_dir("reports/2026")? {
println(name)?;
}
remove_dir_all("reports")?;
}
Output:
feb.txt
jan.txt
The stdlib’s Path class wraps a path string
with methods such as join, dirname, basename and extension.
Arguments and environment#
env gives you the command-line arguments,
environment variables, the working directory and the exit status:
use io::sync::{println, eprintln};
use env::{args, var, exit};
fn main() {
let argv = args() ?? Vec::new();
if argv.len() < 2 {
eprintln("usage: greet NAME")?;
exit(2);
}
let greeting = var("GREETING") ?? "Hello";
println(greeting + ", " + argv[1])?;
}
args() starts with the program’s own name. Pass arguments to a spool
project with spool run -- Ada, or build an executable with
coil package and run that. exit needs --allow-exit and var needs
--allow-env.
Networking#
TCP and UDP sockets are in io::net. A server
usually handles many connections at once, which is what tasks are for:
chapter 13 builds a small concurrent server. For HTTP, TLS and other
protocols, see the packages.
Exercises#
- Write a program that counts the lines, words and bytes of a file, like
wc. - Read numbers from standard input, one per line, and print their sum.
Skip lines that are not numbers (
conv::parse_int) and report how many were skipped on standard error. - Copy every
.txtfile in a directory into abackup/subdirectory.
Next: Chapter 11 — Testing.