diff options
| -rw-r--r-- | examples/file.tm | 213 | ||||
| -rw-r--r-- | examples/wrap.tm | 92 |
2 files changed, 305 insertions, 0 deletions
diff --git a/examples/file.tm b/examples/file.tm new file mode 100644 index 00000000..6b8807eb --- /dev/null +++ b/examples/file.tm @@ -0,0 +1,213 @@ +# 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> + +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'; + 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) { + 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()) + +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) 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); + 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); + contents; + }) + ):Text + text = text:replace($/$(\n){end}/, "") + return Success(text) + else: + return Failure(builtin_last_err()) + +func main(): + 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}") diff --git a/examples/wrap.tm b/examples/wrap.tm new file mode 100644 index 00000000..59218de7 --- /dev/null +++ b/examples/wrap.tm @@ -0,0 +1,92 @@ +HELP := " + wrap: A tool for wrapping lines of text that are piped in through standard input + + usage: wrap [--help] [--width=80] [--min_split=3] [--no-rewrap] [--hyphen='-'] + --help: Print this message and exit + --width=N: The width to wrap the text + --min-split=N: The minimum amount of text on either end of a hyphenation split + --rewrap|--no-rewrap: Whether to rewrap text that is already wrapped or only split long lines + --hyphen='...': The text to use for hyphenation +" + +file := use ./file.tm + +UNICODE_HYPHEN := Text.from_codepoint_names(["hyphen"]) + +func unwrap(text:Text, preserve_paragraphs=yes, hyphen="-")->Text: + if preserve_paragraphs: + paragraphs := text:split($/{2+ nl}/) + if paragraphs.length > 1: + return \n\n:join([unwrap(p, hyphen=hyphen, preserve_paragraphs=no) for p in paragraphs]) + + return text:replace($/$(hyphen)$(\n)/, "") + +func wrap(text:Text, width:Int, min_split=3, hyphen="-")->Text: + if width <= 0: + fail("Width must be a positive integer, not $width") + + if 2*min_split - hyphen.length > width: + fail(" + Minimum word split length ($min_split) is too small for the given wrap width ($width)! + + I can't fit a $(2*min_split - hyphen.length)-wide word on a line without splitting it, + ... and I can't split it without splitting into chunks smaller than $min_split. + ") + + lines := [:Text] + line := "" + for word in text:split($/{whitespace}/): + letters := word:split() + skip if letters.length == 0 + + while not _can_fit_word(line, letters, width): + line_space := width - line.length + if line != "": line_space -= 1 + + if min_split > 0 and line_space >= min_split + hyphen.length and letters.length >= 2*min_split: + # Split word with a hyphen: + split := line_space - hyphen.length + split = split _max_ min_split + split = split _min_ (letters.length - min_split) + if line != "": line ++= " " + line ++= ((++) letters:to(split)) ++ hyphen + letters = letters:from(split + 1) + else if line == "": + # Force split word without hyphenation: + if line != "": line ++= " " + line ++= ((++) letters:to(line_space)) + letters = letters:from(line_space + 1) + else: + pass # Move to next line + + lines:insert(line) + line = "" + + if letters.length > 0: + if line != "": line ++= " " + line ++= (++) letters + + if line != "": + lines:insert(line) + + return \n:join(lines) + +func _can_fit_word(line:Text, letters:[Text], width:Int; inline)->Bool: + if line == "": + return letters.length <= width + else: + return line.length + 1 + letters.length <= width + +func main(width=80, min_split=3, rewrap=yes, hyphen="-"): + when file.read("/dev/stdin") is Failure(reason): + fail(reason) + is Success(text): + if rewrap: + text = unwrap(text) + + first := yes + for paragraph in text:split($/{2+ nl}/): + if not first: + say(\n, newline=no) + say(wrap(paragraph, width=width, min_split=min_split, hyphen=hyphen)) + first = no |
