Filesystem & I/O
The stdlib::fs module wraps the C runtime's file primitives in safer Glide helpers (sorted listings, Vector<string> returns, trimmed trailing newlines, binary-safe reads). The stdlib::io module covers stdin reads, raw stdout writes, flushing, and stderr logging — the pieces the print! / println! macros don't reach.
Import
import stdlib::fs::*; // files, directories, paths
import stdlib::io::*; // stdin / stdout / stderr
Both modules are independent; import only what you use. Note that eprintln lives in stdlib::io, so any file program that logs errors needs both imports.
Public surface at a glance
| Module | Items |
|---|---|
stdlib::fs |
fs_read · fs_read_or_default · fs_read_lines · fs_lines_count · fs_write · fs_exists · fs_is_dir · fs_size · fs_list · fs_list_rec · fs_basename · fs_extension · fs_mkdir_p · fs_remove_file · fs_remove_dir_rec · fs_copy_file · fs_copy_dir_rec · fs_symlink · fs_read_bytes · fs_write_bytes · fs_write_slice |
stdlib::io |
read_line · read_int · read_bytes · prompt · read_yes_no · io_write · io_write_bytes · flush · eprintln |
Both modules expose only free functions — no public structs, enums, or traits. The __glide_* / read_file / write_file externs they are built on are module-private; user code always goes through the fs_* / io_* wrappers.
Reading files
fs_read / fs_read_or_default
pub fn fs_read(path: string) -> string
pub fn fs_read_or_default(path: string, fallback: string) -> string
fs_read returns the entire file as a heap string, or "" on any error (missing file, permission denied). Because both "missing" and "empty" collapse to "", use fs_read_or_default when you need a real fallback — it checks existence first and only returns fallback when the file is absent (an existing-but-empty file still yields "").
This is the canonical "read a config file" pattern: write a default when the file is absent, then read and parse it line by line.
import stdlib::fs::*;
import stdlib::io::*;
fn main() -> i32 {
let path: string = "app.conf";
// Distinguish missing from empty.
if !fs_exists(path) {
eprintln("no config; writing a default");
fs_write(path, "verbose=false\nport=8080\n");
}
let body: string = fs_read(path);
if body.eq("") {
eprintln("config missing or empty");
return 1;
}
// Parse simple key=value lines.
let lines: *Vector<string> = body.split("\n");
defer lines.free();
for let i: i32 = 0; i < lines.len(); i++ {
let line: string = lines.get(i).trim();
if line.eq("") { continue; }
if line.starts_with("#") { continue; }
let kv: *Vector<string> = line.split("=");
defer kv.free();
if kv.len() == 2 {
println!("key:", kv.get(0).trim(), "value:", kv.get(1).trim());
}
}
// Fallback variant: never errors on a missing file.
let raw: string = fs_read_or_default("missing.conf", "default=1\n");
println!("fallback length:", raw.len());
return 0;
}
fs_read_lines / fs_lines_count
pub fn fs_read_lines(path: string) -> *Vector<string>
pub fn fs_lines_count(path: string) -> i32
fs_read_lines reads path and splits on \n, dropping the trailing empty entry caused by a final newline so len() matches the intuitive line count. fs_lines_count returns that same count without allocating a vector (it scans for \n, plus one if the file is non-empty and unterminated); it returns 0 for empty or missing files. A missing file also yields an empty vector from fs_read_lines — no error, no crash.
import stdlib::fs::*;
import stdlib::io::*;
fn main() -> i32 {
fs_write("words.txt", "alpha\nbeta\ngamma\n");
let lines: *Vector<string> = fs_read_lines("words.txt");
defer lines.free();
println!("got", lines.len(), "lines"); // 3, not 4 — trailing "" dropped
for let i: i32 = 0; i < lines.len(); i++ {
let w: string = lines.get(i).trim();
if w.len() >= 5 { println!(w); } // "alpha", "gamma"
}
let n: i32 = fs_lines_count("words.txt");
println!("lines_count:", n); // 3
// Missing file -> 0 / empty vector, no crash.
let missing: *Vector<string> = fs_read_lines("nope.txt");
defer missing.free();
println!("missing len:", missing.len(), "count:", fs_lines_count("nope.txt"));
return 0;
}
The returned vector is a raw *Vector<string> — free it (or defer .free()) when done.
Writing files
fs_write
pub fn fs_write(path: string, content: string) -> bool
Writes content to path in binary mode ("wb"), replacing any existing file. Returns true only when every byte was written. The number of bytes written is content.len() measured as a C string, so an embedded 0x00 truncates the output — use `fs_write_bytes` for binary payloads.
import stdlib::fs::*;
import stdlib::io::*;
fn main() -> i32 {
if !fs_write("out.txt", "hello\n") {
eprintln("write failed");
return 1;
}
return 0;
}
Existence, size & type
| Function | Signature | Description |
|---|---|---|
fs_exists |
fn fs_exists(path: string) -> bool |
true if path is openable for reading. false for missing or unreadable. |
fs_is_dir |
fn fs_is_dir(path: string) -> bool |
true only if path is an existing directory (distinguishes dir from file). |
fs_size |
fn fs_size(path: string) -> i64 |
File size in bytes; -1 if the path is missing or stat fails. One stat syscall, no open. |
import stdlib::fs::*;
import stdlib::io::*;
fn main() -> i32 {
fs_write("favicon.ico", "icon-bytes");
if fs_exists("favicon.ico") { println!("exists"); }
if fs_is_dir(".") { println!("cwd is a dir"); }
if !fs_is_dir("favicon.ico") { println!("favicon is a file, not a dir"); }
let n: i64 = fs_size("favicon.ico");
if n > 0 { println!("serve it,", n, "bytes"); }
let missing: i64 = fs_size("does-not-exist");
if missing == -1 { println!("size of missing path is -1"); }
return 0;
}
Listing directories
fs_list
pub fn fs_list(dir: string, suffix: string) -> *Vector<string>
Lists filenames directly under dir (no recursion). Returns names only, not full paths, sorted ascending for stable output. Filter by suffix (e.g. ".glide"); pass "" to keep all. Directories are excluded; on POSIX, dot-prefixed entries are skipped as well. A missing directory yields an empty vector (no error).
import stdlib::fs::*;
import stdlib::io::*;
fn main() -> i32 {
let files: *Vector<string> = fs_list("src/stdlib", ".glide");
defer files.free();
for let i: i32 = 0; i < files.len(); i++ {
println!("- ", files.get(i));
}
return 0;
}
fs_list_rec
pub fn fs_list_rec(dir: string, suffix: string) -> *Vector<string>
Recursively lists every file under dir whose name ends in suffix (pass "" for all). Returns full relative paths (each prefixed with dir/...), not basenames. Skips dot-prefixed directories plus glide_modules/, build/, target/, and node_modules/ so doc and lint passes don't crawl dependencies or build output. Sorted within each directory level, walked depth-first.
This is the "walk a tree and inspect each file" pattern, combined with the path helpers below:
import stdlib::fs::*;
import stdlib::io::*;
fn main() -> i32 {
// Immediate children, names only, filtered by suffix.
let top: *Vector<string> = fs_list("src", ".glide");
defer top.free();
for let i: i32 = 0; i < top.len(); i++ {
println!("- ", top.get(i));
}
// Recursive, full relative paths.
let all: *Vector<string> = fs_list_rec("src", ".glide");
defer all.free();
println!("found", all.len(), "glide files");
for let i: i32 = 0; i < all.len(); i++ {
let p: string = all.get(i);
println!(fs_basename(p), "ext:", fs_extension(p));
}
return 0;
}
Path helpers
These are pure string functions — they don't touch the filesystem.
| Function | Signature | Description |
|---|---|---|
fs_basename |
fn fs_basename(path: string) -> string |
Last path component (after the final / or \). Returns input unchanged when there's no separator. |
fs_extension |
fn fs_extension(path: string) -> string |
Extension without the leading dot, lower-cased. "" when none. |
import stdlib::fs::*;
import stdlib::io::*;
fn main() -> i32 {
println!(fs_basename("src/stdlib/fs.glide")); // fs.glide
println!(fs_basename("/usr/local/bin/glide")); // glide
println!(fs_basename("noslash.txt")); // noslash.txt
println!(fs_extension("config.toml")); // toml
println!(fs_extension("archive.tar.gz")); // gz
println!(fs_extension("Makefile")); // ""
println!(fs_extension(".bashrc")); // ""
return 0;
}
fs_basename handles both separators, so fs_basename("C:\\Program Files\\Glide\\bin") is "bin".
Creating & mutating directories
| Function | Signature | Description |
|---|---|---|
fs_mkdir_p |
fn fs_mkdir_p(path: string) -> bool |
Create path and all missing parents. Idempotent; false only on a real error. |
fs_remove_file |
fn fs_remove_file(path: string) -> bool |
Delete one file. true on success or if already absent. |
fs_remove_dir_rec |
fn fs_remove_dir_rec(path: string) -> bool |
Recursively delete path. Idempotent (missing → success). Shells out to rm -rf / rmdir /S /Q. |
fs_copy_file |
fn fs_copy_file(src: string, dst: string) -> bool |
Binary-safe file copy, overwriting dst. false on any I/O error. |
fs_copy_dir_rec |
fn fs_copy_dir_rec(src: string, dst: string) -> bool |
Recursively copy src into dst, creating dst. Shells out to cp -r / xcopy /S. |
fs_symlink |
fn fs_symlink(target: string, link: string) -> bool |
Make link a directory-shaped link to target. Idempotent. POSIX symlink(2); Windows directory junction (mklink /J, no admin). |
import stdlib::fs::*;
import stdlib::io::*;
fn main() -> i32 {
if !fs_mkdir_p("cache/sub/leaf") {
eprintln("can't make cache");
return 1;
}
fs_write("cache/a.txt", "hello");
if !fs_copy_file("cache/a.txt", "cache/b.txt") {
eprintln("copy failed");
}
fs_copy_dir_rec("cache", "backup/cache");
fs_symlink("cache/sub", "cache/link");
// Cleanup is idempotent — no pre-checks needed.
fs_remove_file("cache/a.txt");
fs_remove_dir_rec("cache");
fs_remove_dir_rec("backup");
println!("done");
return 0;
}
Binary I/O
These survive embedded NUL bytes, unlike fs_read / fs_write.
fs_read_bytes / fs_write_bytes / fs_write_slice
pub fn fs_read_bytes(path: string, out_len: *i32) -> string
pub fn fs_write_bytes(path: string, data: *void, n: i32) -> bool
pub fn fs_write_slice(path: string, data: string, offset: i32, n: i32) -> bool
fs_read_bytesreadspathas raw bytes into a length-prefixed Glide string;out_lenreceives the exact byte count. Returns""and0on failure. (Internally it frees the C buffer after copying into an arena-tracked string, so there's no leak.)fs_write_byteswritesnbytes from a*voidbuffer in binary mode.fs_write_slicewritesnbytes starting atoffsetwithin a Glide string — handy for carving a file out of an in-memory archive without an extrasubstringcopy. No bounds check: the caller guaranteesoffset + nis in range.
import stdlib::fs::*;
import stdlib::io::*;
fn main() -> i32 {
// Read raw bytes (survives embedded NUL).
let mut n: i32 = 0;
let buf: string = fs_read_bytes("favicon.ico", &n);
if n > 0 {
// Round-trip a slice of the buffer without an extra copy.
fs_write_slice("copy.ico", buf, 0, n);
println!("copied", n, "bytes");
}
// Write from a raw *void buffer.
let blob: *void = malloc(16) as *void;
if fs_write_bytes("out.bin", blob, 16) {
println!("wrote 16 bytes");
}
free(blob);
fs_remove_file("copy.ico");
fs_remove_file("out.bin");
return 0;
}
Standard input
read_line
pub fn read_line() -> string
Reads one line from stdin including the trailing newline if present; "" on EOF. Backed by a fixed 8 KiB buffer (fgets), so a single call returns at most ~8191 bytes of a very long line. Use this when you need the raw line (line-by-line piping); use `prompt` when you want the newline stripped.
read_int
pub fn read_int() -> !i32
Reads a line, trims whitespace, and parses it — the only !T in these modules. Inspect with .ok / .val / .err, or propagate with ?.
read_bytes
pub fn read_bytes(n: i32) -> string
Reads up to n raw bytes; the returned string may be shorter than n near EOF, and is "" for n <= 0.
prompt / read_yes_no
pub fn prompt(msg: string) -> string
pub fn read_yes_no(prompt_msg: string) -> bool
prompt writes msg, flushes, then reads a line and returns it without the trailing newline (handles both \n and \r\n). read_yes_no prompts and returns true when the trimmed reply starts with y or Y.
A complete interactive program touching every stdin helper:
import stdlib::io::*;
fn main() -> i32 {
let name: string = prompt("name? ");
println!("hello,", name);
let raw: string = read_line(); // keeps the trailing newline
println!("raw line len:", raw.len());
let age: !i32 = read_int();
if age.ok { println!("age:", age.val); }
else { println!("not an i32:", age.err); }
if read_yes_no("continue? ") {
println!("continuing");
}
let chunk: string = read_bytes(4096);
println!("read", chunk.len(), "bytes");
return 0;
}
Standard output & stderr
| Function | Signature | Description |
|---|---|---|
io_write |
fn io_write(s: string) |
Write s to stdout, no newline. Named io_write (not write) to avoid colliding with libc write(2). |
io_write_bytes |
fn io_write_bytes(s: string, n: i32) |
Write exactly n bytes from s — safe for embedded NULs. |
flush |
fn flush() |
Force-flush stdout. Matters for prompts/progress when stdout is piped (block-buffered). |
eprintln |
fn eprintln(s: string) |
Write s to stderr with a trailing newline. Stderr is unbuffered, so no flush needed. |
import stdlib::io::*;
fn main() -> i32 {
io_write("> "); // no newline; needs flush to appear before a read
flush();
let header: string = "ABCDEFGHIJKLMNOP";
io_write_bytes(header, 16); // binary-safe stdout write
io_write("\n");
flush();
eprintln("warning: missing config file, using defaults");
return 0;
}