Modules

File_Utilities

moduleSource
macOSLinuxWindowswasm

#import "File_Utilities";

Asking about files, walking and copying folders, and taking paths apart.

Where File opens and reads single files, this module looks at the file system as a whole: file_exists, is_directory and file_modtime_and_size examine a path, visit_files and file_list walk a folder, copy_file and copy_directory copy, delete_directory_tree removes a folder with everything in it, and parse_path turns text into a Path of names that reduce_path, path_contains and path_to_string work on without touching the disk. The procedures that take a path answer false for one with a NUL byte in it. Strings that a procedure returns come from context.allocator and are the caller's to free. On wasm only the directories the host pre-opened can be reached, and files have no owners or permissions (see docs/native/wasm-target.md).

ExampleOpen in playground ↗
#import "Extensions/Environment";
#import "File";
base := environment_variable("TMPDIR");
if base == "" base = environment_variable("TEMP");
if base == "" base = "/tmp";
path := tprint("%/jai-doc-file-utilities.txt", base);
defer file_delete(path);

assert(!file_exists(path));
assert(write_entire_file(path, "x"));
assert(file_exists(path));

Procedures#

Paths#

(_s: string, reduce := true) -> Path

Splits the text _s into a Path, and with reduce (the default) tidies it with reduce_path.

path := parse_path("..//orchard/./maple/../oak/");
assert(path.number_of_leading_slashes == 0 && path.trailing_slash);
assert(path.words.count == 3 && path.words[0] == ".." && path.words[1] == "orchard" && path.words[2] == "oak");

found, position := path_contains(path, .["orchard", "oak"]);
assert(found && position == 1);
array_free(path.words);

raw := parse_path("a/./b", reduce = false);
assert(raw.words.count == 3);
array_free(raw.words);
↗
(path: Path) -> Path

A copy of path with the . words removed and each .. cancelled against the word before it.

(path: Path, words: [] string) -> (contains: bool, position: s32)

Whether the names words appear next to each other, in that order, among the words of path; and where the first match starts.

(path: Path, max_words := -1) -> string

Writes path back as text in a new string that the caller frees: the drive, the leading slashes, the words with the platform separator (/, or \ on Windows) and a closing separator when trailing_slash is set and there are words.

path := parse_path("/usr/local/bin");
defer array_free(path.words);

parent := path_to_string(path, 2);
#if OS == .WINDOWS assert(parent == "\\usr\\local");
else assert(parent == "/usr/local");
free(parent);
↗
(path: Path) -> Path

A deep copy of path: the drive, every word and the array are new allocations from context.allocator, so the copy stays valid after the text it was parsed from is freed. Give it to free_permanent.

(path: Path)

Frees a path made by copy_to_permanent: the drive, each word and the array. Do not use it on the result of parse_path, whose words are not allocated.

Filesystem#

(dir_name: string, recursive: bool, user_data: $T, proc: (info: *File_Visit_Info, user_data: T), visit_files := true, visit_directories := false, visit_symlinks := true, follow_directory_symlinks := true) -> bool

Calls proc with user_data for each entry of the directory dir_name, and returns true if every entry could be examined. With recursive the walk goes into sub-directories too.

#import "Extensions/Environment";
#import "File";
base := environment_variable("TMPDIR");
if base == "" base = environment_variable("TEMP");
if base == "" base = "/tmp";
root := tprint("%/jai-doc-visit-files", base);
defer {
    file_delete(tprint("%/top", root));
    file_delete(tprint("%/sub/leaf", root));
    delete_directory(tprint("%/sub", root));
    delete_directory(root);
}

assert(make_directory_if_it_does_not_exist(tprint("%/sub", root), recursive = true));
assert(write_entire_file(tprint("%/top", root), "top"));
assert(write_entire_file(tprint("%/sub/leaf", root), "leaf"));

Count :: struct {
    files: int;
    directories: int;
    bytes: s64;
}
count: Count;
tally :: (info: *File_Visit_Info, count: *Count) {
    if info.is_directory count.directories += 1;
    else {
        count.files += 1;
        count.bytes += info.size;
    }
}

assert(visit_files(root, true, *count, tally, visit_directories = true));
assert(count.files == 2 && count.directories == 1 && count.bytes == 7);

count = .{};
assert(visit_files(root, false, *count, tally)); // top level only
assert(count.files == 1);
assert(!visit_files(tprint("%/missing", root), false, *count, tally));
↗
(path: string) -> (bool, success: bool)

Whether path is a directory, as (is_directory, success). A link is followed. A path that cannot be examined (missing, no permission, or a NUL byte in it) gives (false, false), so success tells a file from nothing at all.

#import "Extensions/Environment";
#import "File";
base := environment_variable("TMPDIR");
if base == "" base = environment_variable("TEMP");
if base == "" base = "/tmp";
root := tprint("%/jai-doc-is-directory", base);
file := tprint("%/file", root);
defer {
    file_delete(file);
    delete_directory(root);
}

assert(make_directory_if_it_does_not_exist(root));
assert(write_entire_file(file, "x"));

directory, success := is_directory(root);
assert(directory && success);
directory, success = is_directory(file);
assert(!directory && success);
directory, success = is_directory(tprint("%/missing", root));
assert(!directory && !success);
assert(file_exists(file) && !file_exists(tprint("%/missing", root)));
↗
(info: *File_Visit_Info) -> bool

Fills in modification_time and size of info from info.full_name when the time is still APOLLO_TIME_INVALID. Returns whether the details are known: false, with had_error set, when they cannot be read. A record that already has a time is left alone and the answer is !had_error. visit_files fills these in itself on this target.

(path: string) -> bool

Whether something exists at path: a file, a directory or anything else the system can examine. A link is followed, so a link whose target is missing does not count. false also for an empty path or one with a NUL byte.

(file_path: string) -> (modtime: Apollo_Time, file_exists: bool)

When the file was last written, and whether it exists; the same as file_modtime_and_size without the size.

(file_path: string) -> (modtime: Apollo_Time, size: s64, file_exists: bool)

When the file was last written, how many bytes it holds, and whether it exists: (modtime, size, file_exists).

#import "Extensions/Environment";
#import "File";
base := environment_variable("TMPDIR");
if base == "" base = environment_variable("TEMP");
if base == "" base = "/tmp";
path := tprint("%/jai-doc-modtime.txt", base);
defer file_delete(path);

assert(write_entire_file(path, "copper"));
time, size, found := file_modtime_and_size(path);
assert(found && size == 6);
same, still_found := file_modtime(path);
assert(still_found && compare_apollo_times(time, same) == 0);

_, _, found = file_modtime_and_size(tprint("%-missing", path));
assert(!found);
↗
(source: string, dest: string, silent := true) -> bool

Copies the content of the file source to dest, which is created or, if it exists, replaced. Returns whether it all worked.

#import "Extensions/Environment";
#import "File";
base := environment_variable("TMPDIR");
if base == "" base = environment_variable("TEMP");
if base == "" base = "/tmp";
source := tprint("%/jai-doc-copy-source.txt", base);
dest := tprint("%/jai-doc-copy-dest.txt", base);
defer {
    file_delete(source);
    file_delete(dest);
}

assert(write_entire_file(source, "copper"));
assert(write_entire_file(dest, "a longer file that gets replaced"));
assert(copy_file(source, dest));
text, ok := read_entire_file(dest);
assert(ok && text == "copper");
free(text);

assert(!copy_file(source, source, silent = true)); // onto itself
assert(!copy_file(tprint("%-missing", source), dest, silent = true));
↗
(source: string, dest: string) -> bool

Copies the folder source and everything in it to dest, including empty sub-folders. Returns whether it all worked.

#import "Extensions/Environment";
#import "File";
base := environment_variable("TMPDIR");
if base == "" base = environment_variable("TEMP");
if base == "" base = "/tmp";
source := tprint("%/jai-doc-copy-tree", base);
dest := tprint("%/jai-doc-copy-tree-copy", base);
defer {
    file_delete(tprint("%/sub/leaf", source));
    delete_directory(tprint("%/sub", source));
    delete_directory(source);
    file_delete(tprint("%/sub/leaf", dest));
    delete_directory(tprint("%/sub", dest));
    delete_directory(dest);
}

assert(make_directory_if_it_does_not_exist(tprint("%/sub", source), recursive = true));
assert(write_entire_file(tprint("%/sub/leaf", source), "copper"));

assert(!copy_directory(source, tprint("%/inside", source))); // into itself
assert(copy_directory(source, dest));
text, ok := read_entire_file(tprint("%/sub/leaf", dest));
assert(ok && text == "copper");
free(text);
↗
(path: string, recursive := false, follow_directory_symlinks := true) -> [] string

The files below the folder path, as full paths: path joined with each name with /. Directories are not listed; links to files and links with a missing target are. With recursive it also lists the files of every sub-folder. follow_directory_symlinks is as in visit_files.

#import "Extensions/Environment";
#import "File";
base := environment_variable("TMPDIR");
if base == "" base = environment_variable("TEMP");
if base == "" base = "/tmp";
root := tprint("%/jai-doc-file-list", base);
defer {
    file_delete(tprint("%/a.txt", root));
    file_delete(tprint("%/sub/b.txt", root));
    delete_directory(tprint("%/sub", root));
    delete_directory(root);
}

assert(make_directory_if_it_does_not_exist(tprint("%/sub", root), recursive = true));
assert(write_entire_file(tprint("%/a.txt", root), "a"));
assert(write_entire_file(tprint("%/sub/b.txt", root), "b"));

top := file_list(root);
assert(top.count == 1);
for name: top free(name);
array_free(top);

all := file_list(root, recursive = true);
assert(all.count == 2);
for name: all free(name);
array_free(all);
↗
(path: Paths.Path) -> (fully_exists: bool, how_much_exists: s32)

How many leading words of path exist: (fully_exists, how_much_exists). It checks a, then a/b, then a/b/c and so on, and stops at the first that is missing. The count is 0 when even the first is missing and the number of words when all of them exist. An empty path is (true, 0).

path := parse_path("jai-doc-no-such-folder/inner/file");
defer array_free(path.words);
exists, count := how_much_of_path_exists_on_the_local_filesystem(path);
assert(!exists && count == 0);
↗
(old_filename: string, new_filename: string) -> bool

Moves old_filename to new_filename, replacing a file already there. Returns whether it worked.

#import "Extensions/Environment";
#import "File";
base := environment_variable("TMPDIR");
if base == "" base = environment_variable("TEMP");
if base == "" base = "/tmp";
current := tprint("%/jai-doc-replace.txt", base);
fresh := tprint("%/jai-doc-replace.new", base);
defer {
    file_delete(current);
    file_delete(fresh);
}

assert(write_entire_file(current, "old"));
assert(write_entire_file(fresh, "new"));
assert(atomic_file_replace_or_just_rename_if_destination_file_does_not_exist(fresh, current));
assert(!file_exists(fresh));
text, ok := read_entire_file(current);
assert(ok && text == "new");
free(text);
↗

General#

(path: string) -> bool

Deletes the folder path and everything in it. Returns whether the folder is gone.

#import "Extensions/Environment";
#import "File";
base := environment_variable("TMPDIR");
if base == "" base = environment_variable("TEMP");
if base == "" base = "/tmp";
root := tprint("%/jai-doc-delete-tree", base);

assert(make_directory_if_it_does_not_exist(tprint("%/a/b", root), recursive = true));
assert(write_entire_file(tprint("%/a/b/leaf", root), "x"));
assert(!delete_directory(root)); // not empty

assert(delete_directory_tree(root));
assert(!file_exists(root));
assert(!delete_directory_tree(root)); // already gone
↗

Types#

Pathstruct
:: struct

A path taken apart: its drive, how many slashes open it, whether one closes it, and the names between.

:: struct

What visit_files reports about one directory entry, and what add_modtime_and_size_if_missing fills in.

info: File_Visit_Info;
assert(info.short_name.count == 0 && info.size == 0);
assert(!info.had_error && !info.is_symlink && !info.is_directory && !info.descend_into_directory);
↗