hautils

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 30fa24a2264d49d59c5ab52b1dfdb81e4736b4b4
parent b4069f3954e1a571db9811229ae3ced3fc0dda82
Author: Drew DeVault <sir@cmpwn.com>
Date:   Sun, 26 Dec 2021 10:10:58 +0100

wc: new utility

Diffstat:
M.gitignore | 1+
MMakefile | 3++-
Awc.ha | 149+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 152 insertions(+), 1 deletion(-)

diff --git a/.gitignore b/.gitignore @@ -9,3 +9,4 @@ sleep tee true uname +wc diff --git a/Makefile b/Makefile @@ -14,7 +14,8 @@ utils=\ sleep \ tee \ true \ - uname + uname \ + wc all: $(utils) diff --git a/wc.ha b/wc.ha @@ -0,0 +1,149 @@ +use ascii; +use encoding::utf8; +use fmt; +use getopt; +use io; +use main; +use os; +use strings; +use strio; + +type items = enum uint { + BYTES = 1 << 0, + CHARS = 1 << 1, + LINES = 1 << 2, + WORDS = 1 << 3, + DEFAULT = BYTES | LINES | WORDS, +}; + +type counts = struct { + bytes: uint, + chars: uint, + lines: uint, + words: uint, +}; + +export fn utilmain() (main::error | void) = { + const help: []getopt::help = [ + "word, line, and byte or character count", + ('c', "count bytes"), + ('m', "count characters"), + ('l', "count lines"), + ('w', "count words"), + "[file...]", + ]; + const cmd = getopt::parse(os::args, help...); + defer getopt::finish(&cmd); + + let mode: items = 0; + if (len(cmd.opts) == 0) { + mode = items::DEFAULT; + }; + + for (let i = 0z; i < len(cmd.opts); i += 1) { + const opt = cmd.opts[i]; + switch (opt.0) { + case 'c' => + if (mode & items::CHARS != 0) { + fmt::fatal("Error: -c and -m are mutually exclusive"); + }; + mode |= items::BYTES; + case 'm' => + if (mode & items::BYTES != 0) { + fmt::fatal("Error: -c and -m are mutually exclusive"); + }; + mode |= items::CHARS; + case 'l' => + mode |= items::LINES; + case 'w' => + mode |= items::WORDS; + case => abort(); + }; + }; + + if (len(cmd.args) == 0) { + const result = count(os::stdin, mode)?; + print(mode, &result, "")?; + } else { + let totals = counts { ... }; + for (let i = 0z; i < len(cmd.args); i += 1) { + // TODO: Mention which file failed on error + const in = os::open(cmd.args[i])?; + defer io::close(in); + const result = count(in, mode)?; + print(mode, &result, cmd.args[i])?; + totals.bytes += result.bytes; + totals.chars += result.chars; + totals.words += result.words; + totals.lines += result.lines; + }; + if (len(cmd.args) > 1) { + print(mode, &totals, "total")?; + }; + }; +}; + +fn count(in: io::handle, mode: items) (counts | main::error) = { + static let buf: [os::BUFSIZ]u8 = [0...]; + let result = counts { ... }; + for (true) { + const n = match (io::read(in, buf[..])?) { + case let n: size => + yield n; + case io::EOF => + break; + }; + for (let i = 0z; i < n; i += 1) { + switch (buf[i]) { + case '\n' => + result.lines += 1; + case ' ', '\t' => + // TODO: This is not exactly correct + result.words += 1; + case => void; + }; + result.bytes += 1; + }; + if (mode & items::CHARS > 0) { + countchars(&result, buf[..n]); + }; + }; + return result; +}; + +fn countchars(result: *counts, buf: []u8) void = { + const dec = utf8::decode(buf); + for (true) match (utf8::next(&dec)) { + case let r: rune => + result.chars += 1; + case utf8::invalid => + fmt::fatal("Error: input has invalid UTF-8 sequence"); + case utf8::more => + abort(); // TODO + case void => + break; + }; +}; + +fn print(mode: items, result: *counts, path: str) (void | main::error) = { + static let fmtbuf: [32]u8 = [0...]; + let buf = strio::fixed(fmtbuf); + if (mode & items::LINES > 0) { + strio::concat(buf, "{0: 7} ")!; + }; + if (mode & items::WORDS > 0) { + strio::concat(buf, "{1: 7} ")!; + }; + if (mode & items::BYTES > 0) { + strio::concat(buf, "{2: 7} ")!; + }; + if (mode & items::CHARS > 0) { + strio::concat(buf, "{3: 7} ")!; + }; + if (path != "") { + strio::concat(buf, "{4}")!; + }; + const format = strings::rtrim(strio::string(buf)); + fmt::printfln(format, result.lines, result.words, + result.bytes, result.chars, path)?; +};