Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Files and directories

The io module reads and writes files, lists and creates directories, and inspects and changes what the file system records about them: permission bits, timestamps and symbolic links. Every call that touches the file system returns an IoResult, whose error says what went wrong portably (is_not_found(), is_permission_denied(), is_already_exists(), …; see Error handling with Result).

  • Path is a file-system path. Joining, parent(), file_name(), extension() and normalize() work on its text alone; exists(), metadata(), canonicalize() and the set_* methods below ask the file system.
  • File is an open file. File.open reads, File.create writes a new or emptied file, and OpenOptions describes anything else (append, create only if missing, …). read_to_string, write_text, read_bytes, write_bytes, copy, rename and remove work on a whole file by path.
  • Directory creates and removes directories (create, create_all, remove, remove_all) and lists one, all at once with entries() or one entry at a time with for.
  • Metadata is a snapshot of a path’s kind, size, permission bits and timestamps, from Path.metadata() (which follows symlinks), Path.symlink_metadata() (which does not) or File.metadata().
# Write, read, list and remove files with io.
import io;

main(): int {
    var folder = io.Directory.temp().join("pepper-files-basics-example");
    io.Directory.remove_all(folder);
    io.Directory.create_all(folder.join("notes")).value();

    # Whole files at once.
    var path = folder.join("notes").join("today.txt");
    io.File.write_text(path, "buy milk\n").value();
    assert(io.File.read_to_string(path).value() == "buy milk\n", "read back");

    # An open File, with OpenOptions for anything but read or replace.
    var log = io.OpenOptions().append(true).create(true).open(folder.join("log.txt")).value();
    log.write_all("started\n").value();
    log.close().value();

    # Metadata, and a directory's entries.
    var m = path.metadata().value();
    assert(m.is_file && m.size == 9, "a 9-byte file");
    var names = Array<String>();
    for (var entry in io.Directory.open(folder).value()) {
        names.append(entry.value().name);
    }
    assert(names.count == 2, "log.txt and notes");

    io.Directory.remove_all(folder).value();
    return 0;
}

Permissions

A file’s permission bits say who may read, write and run it. They are the platform’s own bits, as Metadata.permissions reports them:

  • On Linux and macOS, the POSIX mode_t bits: read, write and execute for the owner, the group and everyone else, plus setuid, setgid and sticky. Pepper has no octal literals, so rwxr-xr-x (octal 755) is written 493, and a comment gives the octal. On a directory, read lets one list it, write lets one add and remove entries, and execute lets one reach anything inside it.
  • On Windows, the FILE_ATTRIBUTE_* word. Only FILE_ATTRIBUTE_READONLY is a permission; whether a file runs depends on its extension.
  • WASI has no permission bits.

Three ways to change them:

  • set_permissions(mode) on a Path or an open File sets the raw bits (chmod and fchmod). On Windows it applies only the read-only bit of mode, and on WASI it returns an error.
  • set_read_only(bool) is the portable read-only switch. On POSIX, read-only clears every write bit and writable sets only the owner’s, so this never opens a file to others. On Windows it sets or clears FILE_ATTRIBUTE_READONLY.
  • set_executable(bool) adds an execute bit wherever there is a read bit, as chmod +x does, or clears them all. It does nothing on Windows and WASI.

A file or directory can also be created with its bits, so that it never exists with the default ones even for a moment: OpenOptions.mode(m) for a new file, and Directory.create(path, mode = m) or create_all(path, mode = m) for a directory. The process umask still applies, as it does to open and mkdir. These are ignored on Windows and WASI.

Timestamps

Metadata.modified and Metadata.accessed are nanoseconds since 1970-01-01 UTC. set_modified(time) and set_accessed(time) on a Path or an open File set one and leave the other (utimensat and futimens; SetFileTime on Windows, which keeps 100 ns). Writing to a file sets its modification time, and adding or removing an entry sets a directory’s, so set a time after the contents are final.

# Permission bits and timestamps: make a script executable, protect a file,
# and copy a modification time.
import io;

main(): int {
    var folder = io.Directory.temp().join("pepper-files-permissions-example");
    io.Directory.remove_all(folder);
    # 448 is octal 700: only the owner may list or enter it (POSIX only).
    io.Directory.create(folder, mode = 448).value();

    # Created with mode 493 (octal 755, rwxr-xr-x) less the umask, so the
    # script is never readable by others without being executable.
    var script = folder.join("run.sh");
    var file = io.OpenOptions().write(true).create_new(true).mode(493).open(script).value();
    file.write_all("#!/bin/sh\necho hello\n").value();
    file.close().value();

    # The portable way: add an execute bit where there is a read bit.
    var notes = folder.join("notes.sh");
    io.File.write_text(notes, "#!/bin/sh\n").value();
    notes.set_executable(true).value();
    [Target(OS(Linux) || OS(Mac))] {
        assert((notes.metadata().value().permissions & 64) != 0, "the owner may run it");
    }

    # Read-only: no write bits on POSIX, FILE_ATTRIBUTE_READONLY on Windows.
    [Target(!OS(Wasi))] {
        notes.set_read_only(true).value();
        assert(notes.metadata().value().read_only, "read-only");
        notes.set_read_only(false).value();
    }

    # Give the script the same modification time as the notes.
    var modified = notes.metadata().value().modified;
    script.set_modified(modified).value();
    assert(script.metadata().value().modified == modified, "same time");

    io.Directory.remove_all(folder).value();
    return 0;
}

On Windows, an open File must have been opened for writing to change its read-only bit or its times.

File.symlink(target, link) and Directory.symlink(target, link) create a symbolic link at link pointing to target. POSIX makes one kind of link either way; Windows makes a file link and a directory link differently, and a file link to a directory does not open, so use the one that matches the target. The target is stored as given and need not exist; a relative target is resolved from the directory holding the link.

Path.read_link() returns the target a link stores, without following it. Path.symlink_metadata() describes the link itself, and metadata() what it points to. Directory.remove_all removes links and never follows them.

# Symbolic links: make one to a file and one to a directory, and read them.
import io;

main(): int {
    var folder = io.Directory.temp().join("pepper-files-symlinks-example");
    io.Directory.remove_all(folder);
    io.Directory.create_all(folder.join("releases").join("1.2")).value();
    io.File.write_text(folder.join("app-1.2.conf"), "port = 80\n").value();

    # Windows needs Developer Mode or an administrator to make symlinks.
    [Target(!OS(Windows))] {
        # A relative target is resolved from the folder holding the link.
        var config = folder.join("app.conf");
        io.File.symlink(io.Path("app-1.2.conf"), config).value();
        assert(io.File.read_to_string(config).value() == "port = 80\n", "read through the link");
        assert(config.read_link().value() == io.Path("app-1.2.conf"), "the stored target");
        assert(config.symlink_metadata().value().is_symlink, "the link itself");

        var current = folder.join("current");
        io.Directory.symlink(io.Path("releases").join("1.2"), current).value();
        assert(current.is_directory(), "a directory through the link");
    }

    # remove_all removes links, never what they point to.
    io.Directory.remove_all(folder).value();
    return 0;
}

Making a symlink on Windows needs Developer Mode or an administrator; without either it fails with is_permission_denied().

Moving a file to another thread

A File is not thread-safe, so a thread cannot capture one. To hand an open file to another thread, such as the pipe from a child process’s output to a thread that reads it while this one writes to the child, file.into_transfer() moves its handle into an io.FileTransfer, a thread-safe token that a shared function can capture or a Channel can carry, and io.File.adopt(transfer) takes it out again on the other thread. The File it came from is closed afterwards, only the first adopt gets the handle, and a token no one adopts closes the file when it goes. A stream moves the same way, as a SocketTransfer (see Moving a stream to another worker).

# Hand an open File to another thread with into_transfer and adopt.
import async;
import io;

main(): int {
    var path = io.Directory.temp().join("pepper-files-transfer-example.txt");
    io.File.write_text(path, "read on another thread").value();
    var file = io.File.open(path).value();
    # A File is not thread-safe; its transfer token is.
    var transfer = file.into_transfer();
    var size = async.Atomic<ulong>(0);
    var reader = async.Thread.spawn(() => {
        var mine = io.File.adopt(transfer).value();
        size.store(mine.read_all().value().count);
    });
    reader.join();
    assert(size.load() == 22, "the other thread read the file");
    assert(!file.is_open(), "the File it came from is closed");
    io.File.remove(path);
    return 0;
}

Archives

archive.TarReader.extract uses all of the above: it restores each file’s mode and modification time and makes the archive’s symlinks, as long as they stay inside the directory it extracts into. See Compression and archives.