Filesystem Operations

Part of: Reef Language Reference Last reviewed on version: 0.8.0

This guide covers Reef's filesystem module family (fs.*) for native file operations. These modules replace shell-out patterns in programs like coral.

Result/Option migration (Phase C): as of the Result/Option migration, every fs.* function that can fail returns Result[T, core.error.Error] (or, for values that may be absent rather than failed, Option[T]) instead of a sentinel like -1, "", or false. Import core.result and core.error (and core.option where noted) alongside the fs.* module you're using:

import core.result as result
import core.error as error
import core.option as option

Overview

The fs module family provides four focused modules:

Module Purpose Shell Equivalent
fs.stat File type queries and metadata stat, test -f/-d/-L
fs.perm Permission modification and testing chmod, chown, test -r/-w/-x
fs.link Symbolic and hard link operations ln -s, readlink, ln
fs.ops Copy, remove, rename (single and recursive) cp, cp -rp, rm, rm -rf, mv

These complement existing I/O modules:

Module Purpose
io.file Read/write file contents (readFile, writeFile)
io.dir Directory creation and listing (create_dir, create_dir_all, list_dir)

File Metadata (fs.stat)

Query file type and metadata via stat/lstat.

import fs.stat
import core.result as result
import core.error as error

proc main()
    let path = "/etc/hosts"

    // Type checks
    if stat.is_file(path)
        println("regular file")
    end if

    if stat.is_directory("/tmp")
        println("directory")
    end if

    if stat.is_symlink("/usr/bin/python3")
        println("symlink")
    end if

    // Existence check
    if stat.exists(path)
        println("exists")
    end if

    // Metadata — each of these returns a Result, since the underlying
    // stat()/lstat() call can fail (e.g. ENOENT, EACCES).
    let size_r = stat.file_size(path)
    if result.is_err(size_r)
        let msg = error.error_message(result.unwrap_err(size_r))
        println("stat failed: ${msg}")
    else
        let size = result.unwrap_ok(size_r)
        println("size: ${size}")
    end if

    let mode = result.unwrap_or(stat.file_mode(path), 0)
    let uid = result.unwrap_or(stat.file_uid(path), -1)
    let gid = result.unwrap_or(stat.file_gid(path), -1)
    let mtime = result.unwrap_or(stat.file_mtime(path), 0i64)
end main

Function Reference

Function Return Description
is_file(path) bool Regular file (not symlink/dir/device)
is_directory(path) bool Directory
is_symlink(path) bool Symbolic link (uses lstat)
is_pipe(path) bool Named pipe (FIFO)
is_socket(path) bool Unix domain socket file
is_block_device(path) bool Block device
is_char_device(path) bool Character device
exists(path) bool Path exists (any type)
file_size(path) Result[int64, Error] Size in bytes; Err if stat fails
file_mode(path) Result[int, Error] Permission bits; Err if stat fails
file_uid(path) Result[int, Error] Owner UID; Err if stat fails
file_gid(path) Result[int, Error] Owner GID; Err if stat fails
file_mtime(path) Result[int64, Error] Modification time as Unix timestamp; Err if stat fails
uid_name(uid) Option[string] Username for UID; None if not resolvable
gid_name(gid) Option[string] Group name for GID; None if not resolvable
file_uid_name(path) Result[Option[string], Error] Owner username; Err if stat fails, Ok(None) if UID unresolvable
file_gid_name(path) Result[Option[string], Error] Owner group name; Err if stat fails, Ok(None) if GID unresolvable

Permissions (fs.perm)

Modify and test file permissions.

import fs.perm
import io.file
import core.result as result
import core.error as error

proc main()
    let path = "/tmp/myfile.txt"
    if result.is_err(file.writeFile(path, "hello"))
        return
    end if

    // Set specific permission mode (octal 0644 = decimal 420)
    if result.is_err(perm.chmod(path, 420))
        println("chmod failed")
    end if

    // Test permissions
    if perm.is_readable(path)
        println("readable")
    end if
    if perm.is_writable(path)
        println("writable")
    end if

    // Convenience: add execute bits for all
    perm.set_executable(path)

    // Convenience: remove write bits for all
    perm.set_readonly(path)

    // Change ownership (requires root)
    let chown_r = perm.chown(path, 0, 0)
    if result.is_err(chown_r)
        let msg = error.error_message(result.unwrap_err(chown_r))
        println("chown failed: ${msg}")
    end if

    // Change ownership of symlink itself
    perm.lchown("/tmp/mylink", 1000, 1000)
end main

All five mutating functions above (chmod, chown, lchown, set_executable, set_readonly) return Result[bool, Error]Ok(true) on success, Err carrying the failure reason (e.g. PermissionDenied, NotFound). Discarding the result (as set_executable/set_readonly do above) is fine for best-effort calls; check it when the caller needs to know whether the change actually happened.

Permission Constants

POSIX permission bit constants are available as functions:

import fs.perm

proc main()
    // Build custom mode: rwxr-xr-x = 0755
    let mode = perm.S_IRUSR() + perm.S_IWUSR() + perm.S_IXUSR()
             + perm.S_IRGRP() + perm.S_IXGRP()
             + perm.S_IROTH() + perm.S_IXOTH()
    perm.chmod("/tmp/myfile", mode)
end main
Constant Octal Description
S_IRUSR() 0400 Owner read
S_IWUSR() 0200 Owner write
S_IXUSR() 0100 Owner execute
S_IRGRP() 0040 Group read
S_IWGRP() 0020 Group write
S_IXGRP() 0010 Group execute
S_IROTH() 0004 Others read
S_IWOTH() 0002 Others write
S_IXOTH() 0001 Others execute
S_ISUID() 4000 Set-user-ID
S_ISGID() 2000 Set-group-ID
S_ISVTX() 1000 Sticky bit

Function Reference

Function Return Description
chmod(path, mode) Result[bool, Error] Set permission bits
chown(path, uid, gid) Result[bool, Error] Change owner/group
lchown(path, uid, gid) Result[bool, Error] Change symlink owner/group (no dereference)
set_executable(path) Result[bool, Error] Add execute bits for user/group/other
set_readonly(path) Result[bool, Error] Remove write bits for user/group/other
is_readable(path) bool Check read access for current user
is_writable(path) bool Check write access for current user
is_executable(path) bool Check execute access for current user

Create and read symbolic and hard links.

import fs.link
import fs.stat
import core.result as result
import core.error as error

proc main()
    // Create a symbolic link
    if result.is_err(link.symlink("/etc/hosts", "/tmp/hosts_link"))
        println("symlink failed")
        return
    end if

    // Read symlink target
    let target_r = link.readlink("/tmp/hosts_link")
    if result.is_err(target_r)
        let msg = error.error_message(result.unwrap_err(target_r))
        println("readlink failed: ${msg}")
    else
        let target = result.unwrap_ok(target_r)
        println("target: ${target}")  // target: /etc/hosts
    end if

    // Verify it's a symlink
    if stat.is_symlink("/tmp/hosts_link")
        println("is symlink")
    end if

    // Create a hard link (same inode)
    link.hardlink("/tmp/file.txt", "/tmp/file_hard.txt")
end main

Function Reference

Function Return Description
symlink(target, link_path) Result[bool, Error] Create symbolic link
readlink(path) Result[string, Error] Read symlink target
hardlink(old_path, new_path) Result[bool, Error] Create hard link

File Operations (fs.ops)

Copy, remove, and rename files and directory trees.

Single File Operations

import fs.ops
import io.file
import core.result as result
import core.error as error

proc main()
    if result.is_err(file.writeFile("/tmp/src.txt", "hello"))
        return
    end if

    // Copy file (content only, default permissions)
    if result.is_err(ops.copy_file("/tmp/src.txt", "/tmp/dst.txt"))
        println("copy failed")
    end if

    // Copy file preserving permissions (like cp -p)
    ops.copy_file_preserve("/tmp/src.txt", "/tmp/dst2.txt")

    // Copy a symlink without dereferencing
    ops.copy_symlink("/tmp/mylink", "/tmp/mylink_copy")

    // Remove a single file (like rm -f, silent on ENOENT)
    ops.remove_file("/tmp/dst.txt")

    // Rename/move a file
    let rename_r = ops.rename("/tmp/dst2.txt", "/tmp/moved.txt")
    if result.is_err(rename_r)
        let msg = error.error_message(result.unwrap_err(rename_r))
        println("rename failed: ${msg}")
    end if
end main

Recursive Operations

import fs.ops
import io.file
import io.dir
import core.result as result
import core.error as error

proc main()
    // Build a directory tree
    dir.create_dir_all("/tmp/myproject/src/lib")
    file.writeFile("/tmp/myproject/src/main.reef", "proc main() end main")
    file.writeFile("/tmp/myproject/src/lib/util.reef", "// utils")

    // Copy entire tree (preserves permissions and symlinks)
    if result.is_err(ops.copy_tree("/tmp/myproject", "/tmp/myproject_backup"))
        println("copy_tree failed")
        return
    end if

    // Remove entire tree (like rm -rf)
    let rm_r = ops.remove_tree("/tmp/myproject_backup")
    if result.is_err(rm_r)
        let msg = error.error_message(result.unwrap_err(rm_r))
        println("remove_tree failed: ${msg}")
    end if
end main

Safety

remove_tree has built-in safety checks that now surface as Err(error(InvalidInput, ...)) rather than silently failing:

  • Refuses to remove "/" (root directory)
  • Refuses paths with fewer than 2 components (e.g., "/tmp" is rejected)
  • Maximum recursion depth of 256

copy_tree also has a recursion depth limit of 256 and copies symlinks as symlinks (no dereferencing).

Function Reference

Function Return Description
copy_file(src, dst) Result[bool, Error] Copy file contents (default permissions)
copy_file_preserve(src, dst) Result[bool, Error] Copy file preserving permissions
copy_symlink(src, dst) Result[bool, Error] Copy symlink without dereferencing
remove_file(path) Result[bool, Error] Remove single file
rename(old_path, new_path) Result[bool, Error] Rename/move file or directory
remove_tree(path) Result[bool, Error] Recursively remove directory tree; Err(InvalidInput) on an unsafe path
copy_tree(src, dst) Result[bool, Error] Recursively copy directory tree

Common Patterns

Check Before Operate

import fs.stat
import fs.ops
import core.result as result
import core.error as error

proc safe_copy(src: string, dst: string)
    if not stat.exists(src)
        println("source not found: ${src}")
        return
    end if
    if stat.exists(dst)
        println("destination exists: ${dst}")
        return
    end if
    let copy_r = ops.copy_file(src, dst)
    if result.is_err(copy_r)
        let msg = error.error_message(result.unwrap_err(copy_r))
        println("copy failed: ${msg}")
    end if
end safe_copy

Deploy Script (Replace Shell-Outs)

import fs.stat
import fs.perm
import fs.ops
import io.dir

proc deploy(src_dir: string, dst_dir: string)
    // Create destination if needed
    if not stat.is_directory(dst_dir)
        dir.create_dir_all(dst_dir)
    end if

    // Copy project tree
    ops.copy_tree(src_dir, dst_dir)

    // Make scripts executable
    perm.set_executable(dst_dir + "/bin/start.sh")

    println("deployed to ${dst_dir}")
end deploy

Backup With Timestamp

import fs.ops
import fs.stat
import time.time as time
import core.convert as convert

proc backup(path: string)
    if not stat.exists(path)
        return
    end if

    let backup_path = path + "." + convert.to_string(time.time_now()) + ".bak"

    if stat.is_directory(path)
        ops.copy_tree(path, backup_path)
    else
        ops.copy_file_preserve(path, backup_path)
    end if
end backup

Module Relationship

fs.stat  ─── metadata queries (read-only)
fs.perm  ─── permission changes (chmod/chown)
fs.link  ─── symlink/hardlink operations
fs.ops   ─── copy/remove/rename (destructive operations)

io.file  ─── file content read/write
io.dir   ─── directory create/list

All fs.* modules delegate to C runtime functions in reef_fs.c via the FFI hub at sys.platform.runtime.