13. File and Network IO
This chapter covers how a program talks to the world outside it. Files and directories, paths, standard IO, environment variables, child processes, and TCP / UDP sockets all live in rok::std.
IO can fail at any time, so almost every function is throws (IoError). The try / catch from chapter 10 is used throughout.
Reading and writing files
The simplest way is fs::write and fs::read_to_str. The functions open and close the file for you. The example creates a directory under env::temp_dir(), works in it, and removes it at the end.
use rok::std::fs;
use rok::std::env;
use rok::std::io::IoError;
fn main() throws (IoError) {
let dir = env::temp_dir() / f"rok_learn13_{env::pid()}";
try fs::create_dir_all(dir / "notes" / "2026");
try fs::write(dir / "hello.txt", "first line\nsecond line\n");
let text = try fs::read_to_str(dir / "hello.txt");
print(text);
println(f"{text.lines().count()} lines");
try fs::read_to_str(dir / "nope.txt") catch e {
println(f"error kind: {e.kind():?}");
""
};
try fs::remove_dir_all(dir);
println(f"exists after cleanup: {fs::exists(dir)}");
}
first line
second line
2 lines
error kind: NotFound
exists after cleanup: false
An IoError carries a kind() (NotFound, PermissionDenied, TimedOut, ...) and a human-readable message. To branch on the kind of error, compare kind().
Path arguments accept either a str or a Path (the AsPath trait), so you can pass "a.txt" and dir / "a.txt" alike.
File and OpenOptions
To write incrementally or read line by line, open a File directly. With a using block, the file is closed when the block exits. To choose how the file is opened, such as for appending, use OpenOptions.
use rok::std::fs;
use rok::std::fs::{File, OpenOptions};
use rok::std::env;
use rok::std::io::{IoError, Reader};
fn main() throws (IoError) {
let log = env::temp_dir() / f"rok_learn13_log_{env::pid()}.txt";
using f = try File::create(log) {
try f.write_str("start\n");
}
using f = try File::open_with(log, OpenOptions::new().append(true)) {
try f.write_str("more\n");
}
using f = try File::open(log) {
for line in f.lines() {
println(f"> {try line.get()}");
}
}
try fs::remove_file(log);
}
> start
> more
File::create creates or truncates a file, and File::open opens it read-only. lines() is a method of the Reader trait, so it needs use rok::std::io::Reader. Each line may be a read error, so it arrives as a Result; unwrap it with try line.get().
Directories and the Dir capability
fs::walk traverses a directory depth-first, in name order. fs::read_dir lists only one level.
Dir is a little special: a Dir handle is a capability. Dir::root() is the default handle that works with ordinary paths, and a Dir opened from it with open_dir can only reach inside that directory. Attempts to escape with an absolute path or .. are refused. This is a safety net when handling untrusted paths (user input, names inside an archive).
use rok::std::fs;
use rok::std::fs::Dir;
use rok::std::env;
use rok::std::io::IoError;
fn main() throws (IoError) {
let base = env::temp_dir() / f"rok_learn13_dir_{env::pid()}";
try fs::create_dir_all(base / "notes" / "2026");
try fs::write(base / "notes" / "a.md", "# a");
try fs::write(base / "notes" / "2026" / "b.md", "# b");
for e in fs::walk(base) {
let e = try e.get();
let rel = try e.path().strip_prefix(base) catch _ { e.path() };
if rel.as_str() != "" { println(f"{rel} dir={e.is_dir()}"); }
}
let d = try Dir::root().open_dir(base / "notes");
println(f"inside: {d.exists("a.md")}");
try d.open("../../escape.txt") catch e { println(f"refused: {e.kind():?}"); };
try d.open("/etc/hosts") catch e { println(f"refused: {e.kind():?}"); };
try d.close();
try fs::remove_dir_all(base);
}
notes dir=true
notes/2026 dir=true
notes/2026/b.md dir=false
notes/a.md dir=false
inside: true
refused: PermissionDenied
refused: PermissionDenied
create_dir, remove_file, remove_dir, rename, copy, and metadata are also available both as free functions and as Dir methods.
Paths: Path
rok::std::path::Path is a UTF-8 path. Like Python's pathlib, you join paths with the / operator. A path may have no parent or extension, so those methods return T | none.
use rok::std::path::Path;
let p = Path::from("/home/rok/src/main.rok");
println(f"parent: {p.parent() ?? Path::new()}");
println(f"file name: {p.file_name() ?? "-"}");
println(f"stem: {p.file_stem() ?? "-"}");
println(f"extension: {p.extension() ?? "-"}");
println(f"absolute: {p.is_absolute()}");
let q = Path::from("docs") / "guide" / "intro.md";
println(f"joined: {q}");
println(f"swapped: {q.with_extension("html")}");
println(f"normal: {Path::from("a/./b/../c").normalize()}");
for c in q.components() { print(f"[{c.as_str()}]"); }
println("");
parent: /home/rok/src
file name: main.rok
stem: main
extension: rok
absolute: true
joined: docs/guide/intro.md
swapped: docs/guide/intro.html
normal: a/c
[docs][guide][intro.md]
normalize only cleans up the string; canonicalize asks the file system and resolves symbolic links too.
Standard IO and the environment
println is convenient, but to write without a newline or to write to standard error, use io::stdout() / io::stderr(). io::stdin().read_line() returns the line including its trailing \n, and returns "" at end of input.
use rok::std::io;
use rok::std::io::{IoError, Reader, Writer};
fn main() throws (IoError) {
let out = io::stdout();
try out.write_str("Your name? ");
try out.flush();
let line = try io::stdin().read_line();
if line == "" {
println("(no input)");
} else {
println(f"Hello, {line.trim()}!");
}
try io::stderr().write_str("bye\n");
}
Command-line arguments and environment variables live in rok::std::env. Element 0 of env::args() is the executable path. env::var throws VarError if the variable is not set.
use rok::std::env;
let args = env::args();
println(f"{args.len()} arg(s)");
env::set_var("ROK_GREETING", "annyeong");
println(try env::var("ROK_GREETING") catch _ { "unset" });
env::remove_var("ROK_GREETING");
println(try env::var("ROK_GREETING") catch e { f"error: {e}" });
let mine = env::vars().filter(|kv| kv.0.starts_with("ROK_GREETING")).count();
println(f"matching vars: {mine}");
println(f"temp is absolute: {env::temp_dir().is_absolute()}");
1 arg(s)
annyeong
error: environment variable not found
matching vars: 0
temp is absolute: true
Child processes
process::Command runs another program. Set arguments, environment, and working directory with builder methods, then run it with one of output() (waits for exit and collects stdout / stderr), status() (exit status only), or spawn() (returns a Child without waiting).
use rok::std::process::Command;
use rok::std::io::IoError;
fn main() throws (IoError) {
let out = try Command::new("echo").args(["hello", "from", "echo"]).output();
print(try str::from_utf8(out.stdout) catch _ { "" });
println(f"success: {out.status.success()}");
let st = try Command::new("sh").args(["-c", "exit 3"]).status();
println(f"code: {st.code() ?? -1}");
try Command::new("no-such-program").status() catch e {
println(f"spawn failed: {e.kind():?}");
};
}
hello from echo
success: true
code: 3
spawn failed: NotFound
out.stdout is bytes (List<u8>), so convert it to text with str::from_utf8. For a process killed by a signal, code() is none.
TCP and UDP
The rok::std::net API is blocking by default. The example below runs a server and a client in one program. Binding to port 0 lets the OS pick a free port, and local_addr() reports the actual address. The server accepts one connection on a thread and echoes the received text back in upper case.
use rok::std::net;
use rok::std::net::{TcpStream, Shutdown};
use rok::std::io::{IoError, Reader, Writer};
use rok::std::thread;
fn main() throws (IoError) {
let listener = try net::tcp_listen("127.0.0.1:0");
let addr = try listener.local_addr();
let server = thread::spawn(|| {
let (conn, _) = try! listener.accept();
let msg = try! conn.read_to_str();
try! conn.write_str(msg.to_uppercase());
conn.close();
msg.len()
});
let client = try TcpStream::connect(addr);
try client.write_str("hello, socket");
// Half-close: the server's read_to_str sees the end.
try client.shutdown(Shutdown::Write);
println(f"reply: {try client.read_to_str()}");
client.close();
println(f"server read {try! server.join()} bytes");
listener.close();
}
reply: HELLO, SOCKET
server read 13 bytes
UDP exchanges datagrams without a connection. recv_from returns the number of bytes received and the sender's address.
use rok::std::net;
use rok::std::io::{IoError, byte_buf};
fn main() throws (IoError) {
let a = try net::udp_bind("127.0.0.1:0");
let b = try net::udp_bind("127.0.0.1:0");
try b.send_to("ping", try a.local_addr());
let buf = byte_buf(64);
let (n, from) = try a.recv_from(buf);
println(f"got {n} bytes: {try str::from_utf8(buf[0:n]) catch _ { "?" }}");
println(f"from b: {from == (try b.local_addr())}");
}
got 4 bytes: ping
from b: true
Async IO: *_async
Inside a task (see chapter 12), instead of a blocking call you await the version of the same name with an _async suffix. For sockets, only the task pauses while waiting, and the thread runs other tasks. Regular files have no OS support for async waiting, so the call runs on a separate thread while the task awaits the result. Errors are thrown exactly as in the synchronous version.
use rok::std::fs;
use rok::std::net;
use rok::std::net::TcpStream;
use rok::std::env;
let path = env::temp_dir() / f"rok_learn13_async_{env::pid()}.txt";
try! await fs::write_async(path, "written in a task");
println(try! await fs::read_to_str_async(path));
try! await fs::remove_file_async(path);
let l = try! net::tcp_listen("127.0.0.1:0");
let addr = try! l.local_addr();
let server = task::spawn(async {
let (conn, _) = try! await l.accept_async();
let got = try! await conn.read_to_end_async();
conn.close();
got.len()
});
let c = try! await TcpStream::connect_async(addr);
try! await c.write_str_async("twelve bytes");
c.close();
println(f"server got {await server} bytes");
written in a task
server got 12 bytes
With thousands of connections, tasks are much lighter than a thread per connection. For a tool with only a few connections, the blocking API is simpler.
Summary
- Whole files:
fs::read_to_str/fs::write. Incrementally:File+using, withOpenOptionsto choose the open mode. Diris a capability. Anything outside an openedDir(absolute paths,..) is refused withPermissionDenied.- Build paths with
Pathand the/operator; parts that may be missing come back asT | none. - Use
io::stdin/stdout/stderr,env::args/var/temp_dir, andprocess::Commandto deal with the outside environment. - TCP / UDP:
net::tcp_listen/TcpStream::connect/net::udp_bind. With port0, the OS picks one. - Inside a task,
awaitthe*_asyncversions.