tomo/examples/file.tm
2024-09-07 00:03:17 -04:00

245 lines
7.7 KiB
Tcl

# A module for interacting with files
extern builtin_last_err:func()->Text
use <fcntl.h>
use <stdio.h>
use <sys/mman.h>
use <sys/stat.h>
use <unistd.h>
use <unistr.h>
use libunistring.so
enum FileReadResult(Success(text:Text), Failure(reason:Text))
func _wrap_with_finalizer(obj:@Memory, finalizer:func(obj:@Memory))->@Memory:
return inline C (
({
FILE **wrapper = GC_MALLOC(sizeof(FILE*));
*wrapper = $obj;
GC_register_finalizer(wrapper, (void*)$finalizer.fn, wrapper, NULL, NULL);
wrapper;
})
):@Memory
func _close_file(fp:@Memory):
inline C {
if (*(FILE**)$fp)
fclose(*(FILE**)$fp);
*(FILE**)$fp = NULL;
}
func read(path:Text)->FileReadResult:
inline C {
int fd = open(Text$as_c_string($path), O_RDONLY);
if (fd != -1) {
struct stat sb;
if (fstat(fd, &sb) == -1) {
const char *mem = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
char *gc_mem = GC_MALLOC_ATOMIC(sb.st_size+1);
memcpy(gc_mem, mem, sb.st_size);
gc_mem[sb.st_size] = '\0';
close(fd);
return file$FileReadResult$tagged$Success(Text$from_strn(gc_mem, sb.st_size));
} else {
const int chunk_size = 256;
char *buf = GC_MALLOC_ATOMIC(chunk_size);
Text_t contents = Text("");
size_t just_read;
do {
just_read = read(fd, buf, chunk_size);
if (just_read > 0) {
if (u8_check(buf, just_read) != NULL)
break;
contents = Texts(contents, Text$from_strn(buf, just_read));
buf = GC_MALLOC_ATOMIC(chunk_size);
}
} while (just_read > 0);
close(fd);
return file$FileReadResult$tagged$Success(contents);
}
}
}
return Failure(builtin_last_err())
func resolve_path(path:Text, relative_to=".")->Text:
path_c_str := path:as_c_string()
relative_to_c_str := relative_to:as_c_string()
inline C {
extern char *resolve_path(char*, char*, char*);
}
resolved := inline C (
resolve_path($path_c_str, $relative_to_c_str, $relative_to_c_str)
): CString
return Text.from_c_string(resolved)
func relative_path(path:Text, relative_to=".")->Text:
path = resolve_path(path, relative_to)
relative_to = resolve_path(relative_to)
if path:matches($/{start}$relative_to$"/"{..}/):
return path:slice(relative_to.length + 2, -1)
return path
struct WriteHandle(_file:@Memory):
func write(h:WriteHandle, text:Text, flush=yes):
inline C {
fputs(Text$as_c_string($text), *(FILE**)$h.$_file);
if ($flush)
fflush(*(FILE**)$h.$_file);
}
func close(h:WriteHandle):
_close_file(h._file)
enum FileWriteResult(Open(h:WriteHandle), Failure(reason:Text))
func writing(path:Text)->FileWriteResult:
maybe_f := inline C (
fopen(Text$as_c_string($path), "w")
):@Memory?
when maybe_f is @f:
obj := _wrap_with_finalizer(f, _close_file)
return Open(WriteHandle(obj))
else:
return Failure(builtin_last_err())
func appending(path:Text)->FileWriteResult:
maybe_f := inline C (
fopen(Text$as_c_string($path), "a")
):@Memory?
when maybe_f is @f:
return Open(WriteHandle(f))
else:
return Failure(builtin_last_err())
struct LineReader(_file:@Memory):
func stdin()->LineReader:
f := inline C (
stdin
):@Memory
return LineReader(_wrap_with_finalizer(f, _close_file))
func is_finished(r:LineReader)->Bool:
return inline C (
feof(*(FILE**)$r.$_file) != 0;
):Bool
func next_line(r:LineReader)->FileReadResult:
line := inline C (
({
if (*(FILE**)$r.$_file == NULL) fail("File has already been closed!");
char *buf = NULL;
size_t space = 0;
ssize_t len = getline(&buf, &space, *(FILE**)$r.$_file);
if (len < 0) {
file$_close_file($r.$_file);
return (file$FileReadResult_t){1, .$Failure={Text("End of file")}};
}
if (len > 0 && buf[len-1] == '\n') --len;
char *line = GC_MALLOC_ATOMIC(len + 1);
memcpy(line, buf, len);
line[len] = '\0';
if (buf) free(buf);
u8_check(line, len) ? Text("") : Text$from_strn(line, len);
})
):Text
return Success(line)
func from_file(path:Text)->FileLineReaderResult:
maybe_f := inline C (
fopen(Text$as_c_string($path), "r")
):@Memory?
when maybe_f is @f:
obj := _wrap_with_finalizer(f, _close_file)
return Open(LineReader(obj))
else:
return Failure(builtin_last_err())
func from_command(cmd:Text)->FileLineReaderResult:
maybe_f := inline C (
popen(Text$as_c_string($cmd), "r")
):@Memory?
when maybe_f is @f:
obj := _wrap_with_finalizer(f, _close_file)
return Open(LineReader(obj))
else:
return Failure(builtin_last_err())
func close(r:LineReader):
_close_file(r._file)
enum FileLineReaderResult(Open(reader:LineReader), Failure(reason:Text))
func command(cmd:Text)->FileReadResult:
maybe_f := inline C (
popen(Text$as_c_string($cmd), "r")
):@Memory?
when maybe_f is @f:
text := inline C (
({
const int chunk_size = 256;
char *buf = GC_MALLOC_ATOMIC(chunk_size);
Text_t contents = Text("");
size_t just_read;
do {
just_read = fread(buf, sizeof(char), chunk_size, $f);
if (just_read > 0) {
contents = Texts(contents, Text$from_strn(buf, just_read));
buf = GC_MALLOC_ATOMIC(chunk_size);
}
} while (just_read > 0);
pclose($f);
contents;
})
):Text
text = text:replace($/$(\n){end}/, "")
return Success(text)
else:
return Failure(builtin_last_err())
func main(path:Text, relative_to="."):
>> resolve_path(path, relative_to)
>> relative_path(resolve_path(path, relative_to))
# word := ""
# when command("shuf -n 1 /usr/share/dict/words") is Success(w):
# >> word = w
# is Failure(msg):
# fail(msg)
# when writing("test.txt") is Open(f):
# say("Writing {word} to test.txt")
# f:write("Hello {word}!{\n}")
# is Failure(msg):
# fail(msg)
# when read("test.txt") is Success(text):
# say("Roundtrip: {text}")
# is Failure(msg):
# fail(msg)
# say("Reading stdin:")
# reader := LineReader.stdin()
# while yes:
# when reader:next_line() is Success(line):
# >> line
# else: stop
# say("Reading cmd:")
# when LineReader.from_command("ping google.com") is Open(reader):
# while yes:
# when reader:next_line() is Success(line):
# >> line
# else: stop
# is Failure(msg):
# fail("{msg}")
# say("Reading /dev/stdin:")
# when LineReader.from_file("/dev/stdin") is Open(reader):
# while yes:
# when reader:next_line() is Success(line):
# >> line
# else: stop
# is Failure(msg):
# fail("{msg}")