Standard Library: I/O
Shape provides a unified I/O module for files, network sockets, processes, and paths.
Importing
Section titled “Importing”use std::core::ioAll I/O functions live under the io namespace. Handles returned by io::open,
io::tcp_connect, and similar functions implement the Drop trait, so they are
automatically cleaned up when they go out of scope.
File Operations
Section titled “File Operations”Reading Files
Section titled “Reading Files”Open a file with io::open and read its contents:
let f = io::open("data.csv")let content = io::read_to_string(f)io::close(f)The second argument controls the mode. Omitting it or passing "r" opens the
file for reading. To read a fixed number of bytes instead of the entire file,
use io::read:
let f = io::open("binary.dat", "r")let header = io::read(f, 64) // first 64 bytes (string)io::close(f)For raw byte arrays, use io::read_bytes:
let f = io::open("image.png", "r")let bytes = io::read_bytes(f, 1024) // Array<int>In practice, you do not need to close handles manually. The handle implements
Drop and is closed automatically when it goes out of scope:
fn read_data() -> string { let f = io::open("data.csv") io::read_to_string(f) // f is closed automatically when the function returns, even on error}Writing Files
Section titled “Writing Files”Open a file for writing with "w" (truncate) or "a" (append):
let f = io::open("output.csv", "w")io::write(f, "name,value\n")io::write(f, "alpha,142.58\n")io::write(f, "beta,378.20\n")// f is closed automatically when it goes out of scopeWrites are buffered. Call io::flush to force buffered data to disk before the
block exits:
let f = io::open("log.txt", "a")io::write(f, "checkpoint reached\n")io::flush(f)// ... more work ...io::write(f, "done\n")// f is flushed and closed when it goes out of scopeFile Information
Section titled “File Information”Query file metadata without opening a handle:
| Function | Returns | Description |
|---|---|---|
io::exists(path) | bool | File or directory exists |
io::stat(path) | object | File metadata (size, modified, created, is_file, is_dir) |
io::is_file(path) | bool | Path is a regular file |
io::is_dir(path) | bool | Path is a directory |
The object returned by io::stat has the following fields:
let info = io::stat("data.csv")print(info.size) // file size in bytesprint(info.modified) // last modification timestampprint(info.created) // creation timestampprint(info.is_file) // trueprint(info.is_dir) // falseDirectory Operations
Section titled “Directory Operations”| Function | Description |
|---|---|
io::mkdir(path) | Create a single directory |
io::mkdir(path, true) | Create directory and all parents recursively |
io::remove(path) | Remove a file or empty directory |
io::rename(old, new) | Rename or move a file |
io::read_dir(path) | List directory contents as an array of strings |
Example — list directory contents and filter:
let files = io::read_dir("reports")let csvs = files.filter(|name| name.ends_with(".csv"))for path in csvs { print(f"Found: {path}")}Create a nested output directory, then write into it:
io::mkdir("output/reports/weekly", true)
let f = io::open("output/reports/weekly/summary.csv", "w")io::write(f, header + "\n")for row in rows { io::write(f, row.to_csv() + "\n")}// f is closed automatically when it goes out of scopeNetwork Operations
Section titled “Network Operations”Connect to a TCP server with io::tcp_connect. The returned handle supports
reading and writing and is automatically closed when it goes out of scope.
let conn = io::tcp_connect("api.example.com:8080")io::tcp_write(conn, "GET / HTTP/1.0\r\nHost: api.example.com\r\n\r\n")let response = io::tcp_read(conn)print(response)// conn is closed automatically when it goes out of scopeFor server-side TCP, bind a listener and accept connections:
let listener = io::tcp_listen("0.0.0.0:9000")
// Accept a single connectionlet client = io::tcp_accept(listener)let request = io::tcp_read(client)io::tcp_write(client, "ACK: " + request)io::tcp_close(client)
io::tcp_close(listener)TCP function reference:
| Function | Description |
|---|---|
io::tcp_connect(addr) | Connect to a remote address, returns stream handle |
io::tcp_listen(addr) | Bind and listen on an address, returns listener handle |
io::tcp_accept(listener) | Accept a connection, returns stream handle |
io::tcp_read(handle) | Read available data from stream |
io::tcp_write(handle, data) | Write data to stream |
io::tcp_close(handle) | Close connection or listener |
UDP sockets are connectionless. Bind a socket, then send and receive datagrams:
let sock = io::udp_bind("0.0.0.0:5000")io::udp_send(sock, "hello", "192.168.1.10:5000")
let msg = io::udp_recv(sock)print(f"Received '{msg.data}' from {msg.from}")// sock is closed automatically when it goes out of scopeio::udp_recv returns an object with two fields:
| Field | Type | Description |
|---|---|---|
data | string | The received payload |
from | string | The sender’s address ("host:port") |
UDP function reference:
| Function | Description |
|---|---|
io::udp_bind(addr) | Bind a UDP socket to a local address |
io::udp_send(handle, data, addr) | Send a datagram to a remote address |
io::udp_recv(handle) | Receive a datagram, returns { data, from } |
Process Operations
Section titled “Process Operations”Running Commands
Section titled “Running Commands”Use io::exec to run a command to completion and capture its output:
let result = io::exec("ls", ["-la", "/tmp"])print(result.status) // 0print(result.stdout) // directory listingprint(result.stderr) // empty on successThe return value has three fields:
| Field | Type | Description |
|---|---|---|
status | int | Process exit code (0 = success) |
stdout | string | Captured standard output |
stderr | string | Captured standard error |
Example — running an external tool and checking the result:
let result = io::exec("shape", ["check", "src/main.shape"])if result.status != 0 { print(f"Check failed:\n{result.stderr}")}Spawning Processes
Section titled “Spawning Processes”For long-running or interactive processes, use io::spawn to start a subprocess
without waiting for it to finish:
let proc = io::spawn("tail", ["-f", "/var/log/app.log"])
// Read lines from stdout as they arrivelet line = io::read_line(proc)print(line)
// Kill the subprocess when doneio::process_kill(proc)// proc is cleaned up automatically when it goes out of scopeA spawned process handle gives access to the child’s standard streams. The
handle implements Drop, so the process is killed if it is still running when
the handle goes out of scope.
Process management functions:
| Function | Description |
|---|---|
io::spawn(cmd, args) | Start a subprocess, returns process handle |
io::process_wait(handle) | Block until the process exits, returns exit code |
io::process_kill(handle) | Send termination signal to the process |
io::process_write(handle, data) | Write to the subprocess’s stdin |
io::process_read(handle, n?) | Read up to n bytes from stdout |
io::process_read_err(handle, n?) | Read up to n bytes from stderr |
io::process_read_line(handle) | Read one line from stdout |
Standard Streams
Section titled “Standard Streams”Access the current process’s own standard streams:
let stdin = io::stdin()let stdout = io::stdout()let stderr = io::stderr()Read a line of user input:
io::write(io::stdout(), "Enter your name: ")io::flush(io::stdout())let name = io::read_line(io::stdin())print(f"Hello, {name}")Standard stream functions:
| Function | Description |
|---|---|
io::stdin() | Handle to standard input |
io::stdout() | Handle to standard output |
io::stderr() | Handle to standard error |
io::read_line(handle) | Read a single line from a stream |
Async File I/O
Section titled “Async File I/O”For non-blocking file access in async contexts, io exposes async-suffixed
variants of the common operations:
| Function | Description |
|---|---|
io::read_file_async(path) | Read entire file as a string |
io::write_file_async(path, data) | Write string to file |
io::append_file_async(path, data) | Append string to file |
io::read_bytes_async(path) | Read entire file as Array<int> |
io::exists_async(path) | Check if a path exists |
async fn load() -> string { await io::read_file_async("data.csv")}Gzip File Helpers
Section titled “Gzip File Helpers”Convenience wrappers that combine file I/O with gzip compression in one call:
| Function | Description |
|---|---|
io::read_gzip(path) | Read a gzip-compressed file, returns decompressed string |
io::write_gzip(path, data, level) | Compress data and write to path (level 0-9) |
io::write_gzip("backup.txt.gz", "hello world", 6)let restored = io::read_gzip("backup.txt.gz")Path Utilities
Section titled “Path Utilities”These are synchronous helper functions that manipulate path strings without touching the filesystem:
| Function | Description | Example |
|---|---|---|
io::join(a, b, ...) | Join path segments | io::join("/home", "user", "file.txt") returns "/home/user/file.txt" |
io::dirname(path) | Parent directory | io::dirname("/home/user/file.txt") returns "/home/user" |
io::basename(path) | File name component | io::basename("/home/user/file.txt") returns "file.txt" |
io::extension(path) | File extension without dot | io::extension("report.csv") returns "csv" |
io::resolve(path) | Resolve to absolute path | Expands ., .., and symlinks |
Example — build an output path from an input path:
let input = "data/raw/records.csv"let output = io::join( io::dirname(input), io::basename(input).replace(".csv", "_clean.csv"))// output == "data/raw/records_clean.csv"Automatic Resource Cleanup
Section titled “Automatic Resource Cleanup”All I/O handles implement the Drop trait, which means they are cleaned up
automatically when they go out of scope — even when an error occurs. Files are
closed, TCP connections are shut down, and spawned processes are killed.
// File: auto-closed when f goes out of scopelet f = io::open("output.csv", "w")io::write(f, "header1,header2\n")for row in data { io::write(f, row.to_csv() + "\n")}
// TCP: connection closed when conn goes out of scopelet conn = io::tcp_connect("api.example.com:443")io::tcp_write(conn, request)let response = io::tcp_read(conn)
// Process: killed if still running when proc goes out of scopelet proc = io::spawn("long-running-task", [])let output = io::read_line(proc)Use blocks to limit a handle’s lifetime when multiple resources are involved:
fn transform_file(input_path: string, output_path: string) { let cleaned = { let input = io::open(input_path) let content = io::read_to_string(input) process(content) // input is closed here }
let output = io::open(output_path, "w") io::write(output, cleaned) // output is closed when the function returns}Multiple handles in the same scope are dropped in reverse declaration order, ensuring dependents are cleaned up before their dependencies.
Practical Examples
Section titled “Practical Examples”Reading a CSV and Processing It
Section titled “Reading a CSV and Processing It”use std::core::io
let f = io::open("records.csv")let lines = io::read_to_string(f).split("\n")let header = lines[0].split(",")
let rows = lines.slice(1).filter(|l| l.length > 0).map(|line| { let fields = line.split(",") { name: fields[0], value: fields[1].to_number(), count: fields[2].to_int() }})
let total = rows.map(|r| r.value * r.count).sum()print(f"Total: {total}")// f is closed automatically when it goes out of scopeSimple TCP Client
Section titled “Simple TCP Client”use std::core::io
fn send_request(host: string, port: int, message: string) -> string { let addr = f"{host}:{port}" let conn = io::tcp_connect(addr) io::tcp_write(conn, message + "\n") let response = io::tcp_read(conn) response // conn is closed automatically when the function returns}
let reply = send_request("localhost", 8080, "PING")print(f"Server replied: {reply}")Capturing Command Output
Section titled “Capturing Command Output”use std::core::io
let result = io::exec("git", ["log", "--oneline", "-5"])if result.status == 0 { let commits = result.stdout.split("\n").filter(|l| l.length > 0) print(f"Last {commits.length} commits:") for commit in commits { print(f" {commit}") }} else { print(f"git error: {result.stderr}")}File Watcher Pattern
Section titled “File Watcher Pattern”Poll for new files using io::read_dir and a sleep loop:
use std::core::iouse std::core::time
let mut seen = []
fn check_new_files() { let entries = io::read_dir("inbox") let csvs = entries.filter(|name| name.ends_with(".csv")) for name in csvs { let path = io::join("inbox", name) if !seen.contains(path) { seen.push(path) print(f"New file: {path}") process_file(path) } }}
// Poll every 5 secondsloop { check_new_files() time::sleep_sync(5000)}See Also
Section titled “See Also”- Resource Management — automatic scope-based drop and the
Droptrait - Async — combining I/O with async/await and structured concurrency