Skip to content

File Operations

← Back to Standard Library

File system operations for reading, writing, and managing files.

Import

use <io/files>

Overview

The file operations module provides safe file I/O with explicit error handling. All file operations use POSIX file descriptors under the hood and return FileResult for proper error handling.

Opening Files

open

Open a file with a specific mode.

fn open(string path, FileMode mode) -> FileResult

Parameters: - path - File path (relative or absolute) - mode - File access mode

Returns: - FileResult.Ok(file) - Successfully opened file - FileResult.Err(error) - Error occurred (see Error Handling section)

File Modes:

  • FileMode.Read() - Open for reading only (file must exist)
  • FileMode.Write() - Open for writing only (creates file or truncates existing)
  • FileMode.Append() - Open for appending (creates file if doesn't exist)

Example:

use <io/files>

fn main() i32:
    match open("data.txt", FileMode.Read()):
        FileResult.Ok(f) ->
            println("File opened successfully")
            f.close()
        FileResult.Err(e) ->
            println("Failed to open file")

    return Result.Ok(0)

With error propagation:

use <io/files>

fn read_config() string | FileError:
    let file f = open("config.txt", FileMode.Read())??
    let string content = f.read()
    f.close()
    return Result.Ok(content)

fn main() i32:
    match read_config():
        Result.Ok(config) ->
            println("Config: {config}")
        Result.Err(_) ->
            println("Failed to read config")

    return Result.Ok(0)

File Methods

read

Read entire file contents as a string.

fn file.read() -> string

Returns: - String containing entire file contents

Example:

use <io/files>

fn main() i32:
    match open("data.txt", FileMode.Read()):
        FileResult.Ok(f) ->
            let string content = f.read()
            f.close()
            println("Content: {content}")
        FileResult.Err(_) ->
            println("Failed to read file")

    return Result.Ok(0)

Processing file content:

use <io/files>
use <collections/strings>

fn main() i32 | FileError:
    let file f = open("numbers.txt", FileMode.Read())??
    let string content = f.read()
    f.close()

    let string[] lines = content.split("\n")

    foreach(line in lines.iter()):
        if (not line.is_empty()):
            println("Line: {line}")

    return Result.Ok(0)

readln

Read a single line from the file.

fn file.readln() -> string

Returns: - String containing one line (without newline character)

Example:

use <io/files>

fn main() i32 | FileError:
    let file f = open("data.txt", FileMode.Read())??

    let string first_line = f.readln()
    println("First: {first_line}")

    let string second_line = f.readln()
    println("Second: {second_line}")

    f.close()

    return Result.Ok(0)

Line-by-line processing:

use <io/files>

fn main() i32 | FileError:
    let file f = open("log.txt", FileMode.Read())??
    let i32 line_count = 0

    # Read until empty line (EOF)
    let bool done = false
    while (not done):
        let string line = f.readln()

        if (line.is_empty()):
            done := true
        else:
            line_count := line_count + 1
            println("{line_count}: {line}")

    f.close()
    println("Total lines: {line_count}")

    return Result.Ok(0)

write

Write a string to the file.

fn file.write(string data) -> ~

Parameters: - data - String to write

Example:

use <io/files>

fn main() i32 | FileError:
    let file f = open("output.txt", FileMode.Write())??

    f.write("Hello, World!")
    f.write("\n")
    f.write("Second line")

    f.close()

    return Result.Ok(0)

Writing formatted data:

use <io/files>

fn main() i32 | FileError:
    let file f = open("report.txt", FileMode.Write())??

    f.write("Report\n")
    f.write("======\n\n")

    let i32 count = 42
    let string line = "Items processed: {count}\n"
    f.write(line)

    f.close()

    return Result.Ok(0)

Appending to file:

use <io/files>

fn main() i32 | FileError:
    let file f = open("log.txt", FileMode.Append())??

    f.write("New log entry\n")

    f.close()

    return Result.Ok(0)

close

Close the file and release resources.

fn file.close() -> ~

Example:

use <io/files>

fn main() i32 | FileError:
    let file f = open("data.txt", FileMode.Read())??
    let string content = f.read()
    f.close()  # Always close files

    println(content)

    return Result.Ok(0)

Important: Always close files after use to release system resources. File handles are limited by the operating system.

Error Handling

FileResult Enum

Result type for file operations:

enum FileResult:
    Ok(file)
    Err(FileError)

FileError Enum

Error types for file operations:

enum FileError:
    NotFound()
    PermissionDenied()
    AlreadyExists()
    Other()

Error Patterns

Pattern matching all errors

use <io/files>

fn main() i32:
    match open("config.txt", FileMode.Read()):
        FileResult.Ok(f) ->
            let string data = f.read()
            f.close()
            println(data)
        FileResult.Err(FileError.NotFound()) ->
            println("File not found")
        FileResult.Err(FileError.PermissionDenied()) ->
            println("Permission denied")
        FileResult.Err(_) ->
            println("Other error")

    return Result.Ok(0)

Nested error handling

use <io/files>

fn main() i32:
    match open("data.txt", FileMode.Read()):
        FileResult.Ok(f) ->
            match open("output.txt", FileMode.Write()):
                FileResult.Ok(out) ->
                    let string data = f.read()
                    out.write(data)
                    f.close()
                    out.close()
                    println("File copied")
                FileResult.Err(_) ->
                    println("Failed to open output file")
                    f.close()
        FileResult.Err(FileError.NotFound()) ->
            println("Input file not found")
        FileResult.Err(_) ->
            println("Failed to open input file")

    return Result.Ok(0)

Using error propagation

use <io/files>

fn copy_file(string src, string dst) ~ | FileError:
    let file input = open(src, FileMode.Read())??
    let string content = input.read()
    input.close()

    let file output = open(dst, FileMode.Write())??
    output.write(content)
    output.close()

    return Result.Ok(~)

fn main() i32:
    match copy_file("input.txt", "output.txt"):
        Result.Ok(_) ->
            println("Copy successful")
        Result.Err(_) ->
            println("Copy failed")

    return Result.Ok(0)

File Utility Functions

remove

Delete a file from the filesystem.

fn remove(string path) -> Result<i32>

Parameters: - path - Path to the file to delete

Returns: - Result.Ok(0) - File successfully deleted - Result.Err() - Failed to delete file (doesn't exist, permission denied, etc.)

Example:

use <io/files>

fn main() i32:
    match remove("/tmp/old_file.txt"):
        Result.Ok(_) ->
            println("File deleted")
        Result.Err(_) ->
            println("Failed to delete file")

    return Result.Ok(0)

Important: Only works on files, not directories. Use rmdir() for directories.

rename

Rename or move a file or directory.

fn rename(string old_path, string new_path) -> Result<i32>

Parameters: - old_path - Current path - new_path - New path

Returns: - Result.Ok(0) - Successfully renamed/moved - Result.Err() - Failed (source doesn't exist, permission denied, etc.)

Example:

use <io/files>

fn main() i32:
    match rename("/tmp/old.txt", "/tmp/new.txt"):
        Result.Ok(_) ->
            println("File renamed")
        Result.Err(_) ->
            println("Failed to rename")

    return Result.Ok(0)

Note: Atomically replaces destination if it exists.

mkdir

Create a new directory with specified permissions.

fn mkdir(string path, i32 mode) -> Result<i32>

Parameters: - path - Directory path to create - mode - Permissions in octal format (e.g., 0o755, 0o700)

Returns: - Result.Ok(0) - Directory created successfully - Result.Err() - Failed (already exists, permission denied, parent doesn't exist)

Example:

use <io/files>

fn main() i32:
    match mkdir("/tmp/mydir", 0o755):
        Result.Ok(_) ->
            println("Directory created")
        Result.Err(_) ->
            println("Failed to create directory")

    return Result.Ok(0)

Common permissions: - 0o755 - Owner: rwx, Group: r-x, Others: r-x - 0o700 - Owner: rwx, Group: ---, Others: --- - 0o775 - Owner: rwx, Group: rwx, Others: r-x

Note: Does not create parent directories. All parents must already exist.

rmdir

Remove an empty directory.

fn rmdir(string path) -> Result<i32>

Parameters: - path - Directory path to remove

Returns: - Result.Ok(0) - Directory removed successfully - Result.Err() - Failed (doesn't exist, not empty, permission denied)

Example:

use <io/files>

fn main() i32:
    match rmdir("/tmp/mydir"):
        Result.Ok(_) ->
            println("Directory removed")
        Result.Err(_) ->
            println("Failed to remove directory")

    return Result.Ok(0)

Important: Directory must be empty. Use remove() to delete files inside first.

copy

Copy a file's contents to a new location.

fn copy(string src, string dst) -> Result<i32>

Parameters: - src - Source file path - dst - Destination file path

Returns: - Result.Ok(0) - File copied successfully - Result.Err() - Failed (source doesn't exist, permission denied, I/O error)

Example:

use <io/files>

fn main() i32:
    match copy("/tmp/source.txt", "/tmp/backup.txt"):
        Result.Ok(_) ->
            println("File copied")
        Result.Err(_) ->
            println("Failed to copy file")

    return Result.Ok(0)

Note: Overwrites destination if it exists. Uses efficient 4KB buffering internally.

File Utility Pattern Matching Example

use <io/files>

fn backup_and_cleanup(string path) ~:
    let string backup = "{path}.bak"

    match copy(path, backup):
        Result.Ok(_) ->
            println("Backup created")
        Result.Err(_) ->
            return Result.Err(StdError.Error)

    match remove(path):
        Result.Ok(_) ->
            println("Original removed")
        Result.Err(_) ->
            println("Cleanup failed")
            return Result.Err(StdError.Error)

    return Result.Ok(~)

fn main() i32:
    match backup_and_cleanup("/tmp/data.txt"):
        Result.Ok(_) ->
            println("Operation complete")
        Result.Err(_) ->
            println("Operation failed")

    return Result.Ok(0)

Common Patterns

Reading entire file

use <io/files>

fn read_file(string path) string | FileError:
    let file f = open(path, FileMode.Read())??
    let string content = f.read()
    f.close()
    return Result.Ok(content)

fn main() i32:
    let string content = read_file("data.txt").realise("")
    println(content)

    return Result.Ok(0)

Writing entire file

use <io/files>

fn write_file(string path, string data) ~ | FileError:
    let file f = open(path, FileMode.Write())??
    f.write(data)
    f.close()
    return Result.Ok(~)

fn main() i32 | FileError:
    write_file("output.txt", "Hello, World!")??
    println("File written")

    return Result.Ok(0)

Processing CSV file

use <io/files>
use <collections/strings>

fn main() i32 | FileError:
    let file f = open("data.csv", FileMode.Read())??
    let string content = f.read()
    f.close()

    let string[] lines = content.split("\n")

    foreach(line in lines.iter()):
        if (not line.is_empty()):
            let string[] fields = line.split(",")

            foreach(field in fields.iter()):
                print("{field}\t")

            println("")

    return Result.Ok(0)

Creating log file

use <io/files>

fn log_message(string message) ~ | FileError:
    let file f = open("app.log", FileMode.Append())??
    f.write("{message}\n")
    f.close()
    return Result.Ok(~)

fn main() i32 | FileError:
    log_message("Application started")??
    log_message("Processing data")??
    log_message("Application finished")??

    return Result.Ok(0)

Checking file existence

use <io/files>

fn file_exists(string path) bool:
    match open(path, FileMode.Read()):
        FileResult.Ok(f) ->
            f.close()
            return Result.Ok(true)
        FileResult.Err(_) ->
            return Result.Ok(false)

fn main() i32:
    if (file_exists("config.txt").realise(false)):
        println("Config file found")
    else:
        println("Config file not found")

    return Result.Ok(0)

Platform Behavior

Path Separators

  • Unix/Linux/macOS: Forward slash /
  • Recommended: Use forward slashes for cross-platform compatibility
use <io/files>

fn main() i32 | FileError:
    # Unix-style paths work on all platforms
    let file f = open("data/input.txt", FileMode.Read())??
    f.close()

    return Result.Ok(0)

Line Endings

Different platforms use different line endings: - Unix/Linux/macOS: \n (LF) - Windows: \r\n (CRLF)

Sushi uses \n internally. When reading files, line endings are preserved.

File Permissions

File permissions are platform-specific: - Unix/Linux/macOS: Standard POSIX permissions (user/group/other) - Windows: ACLs

PermissionDenied error occurs when the process lacks required permissions.

Performance Considerations

Buffering

File operations are buffered by the operating system. For large files:

use <io/files>

fn main() i32 | FileError:
    let file f = open("large.txt", FileMode.Read())??

    # Reading line-by-line is more memory-efficient than .read()
    let bool done = false
    while (not done):
        let string line = f.readln()

        if (line.is_empty()):
            done := true
        else:
            # Process line
            println(line)

    f.close()

    return Result.Ok(0)

Memory Usage

  • .read() loads entire file into memory
  • .readln() reads one line at a time (more memory-efficient)

Choose based on file size and use case.

Security Considerations

Path Traversal

Always validate file paths from user input:

use <io/files>
use <collections/strings>

fn is_safe_path(string path) bool:
    # Reject paths with ..
    if (path.contains("..")):
        return Result.Ok(false)

    # Reject absolute paths if needed
    if (path.starts_with("/")):
        return Result.Ok(false)

    return Result.Ok(true)

fn main() i32 | FileError:
    let string user_path = "data.txt"

    if (not is_safe_path(user_path).realise(false)):
        println("Invalid path")
        return Result.Ok(1)

    let file f = open(user_path, FileMode.Read())??
    let string content = f.read()
    f.close()

    println(content)

    return Result.Ok(0)

File Permissions

Be cautious with write operations:

use <io/files>

fn main() i32:
    # Always check if overwriting is intended
    match open("important.txt", FileMode.Write()):
        FileResult.Ok(f) ->
            # This TRUNCATES the existing file!
            f.write("New content")
            f.close()
        FileResult.Err(_) ->
            println("Failed to open file")

    return Result.Ok(0)

Use FileMode.Append() to preserve existing content.

See Also