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 | + |
M | Makefile | | | 3 | ++- |
A | wc.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)?;
+};