Chapter 17 14 min read

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_bytes reads path as raw bytes into a length-prefixed Glide string; out_len receives the exact byte count. Returns "" and 0 on failure. (Internally it frees the C buffer after copying into an arena-tracked string, so there's no leak.)
  • fs_write_bytes writes n bytes from a *void buffer in binary mode.
  • fs_write_slice writes n bytes starting at offset within a Glide string — handy for carving a file out of an in-memory archive without an extra substring copy. No bounds check: the caller guarantees offset + n is 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;
}