lib+test: add shlex (POSIX split/quote) + strings.freeall
Surface mirrors ref/hare/shlex/{split,escape}.ha:
shlex.syntaxerr !void
shlex.strerror(syntaxerr) str
shlex.split(str) ([]str | syntaxerr)
shlex.quote(*io.stream, s) (i32 | io.closed)
shlex.quotestr(s) str
strings.freeall([]str) added as the natural disposer (placed next
to strings.dup, the natural creator). Skips empty {nil,0} elements
and the header free when cap==0.
POSIX rules:
- whitespace separators ' '/'\t'/'\n' (collapse runs).
- single-quote: literal until closing "'" (no escapes inside).
- double-quote: '\<c>' processed inside, any <c> (Hare-faithful;
more permissive than POSIX strict). Unterminated → syntaxerr.
- outside quotes: '\<c>' → literal <c>; '\<newline>' deleted
(line continuation); trailing bare '\' → syntaxerr.
- "" / '' preserve a literal empty-string token (dirty flag).
Divergences from Hare (all documented in shlex.ww header):
- drop nomem (os.alloc aborts on OOM, same precedent as
strings.dup, getopt.appendoption).
- byte-wise cursor instead of strings::iterator (no UTF-8 rune
iteration in the language stack yet; same precedent as fnmatch).
- *io.stream (not io::handle); (i32 | io.closed) (lib/io's
stream vtable doesn't model wider io::error yet).
- appendstr / dupstr workarounds graduate when task #17
(cgen mod-mangles fn labels) lands.
Test: 4 @test fns (test_split / test_quote / test_quotestr /
test_strerror), table-driven via check1/check2/check3/checkerr/
checkquote helpers. 12 split rows + 4 quote rows ported verbatim
from ref/hare/shlex/+test.ha; empty-input ([]) and empty-quote
('') edges added per documented behaviour. @test fns prefixed
test_* to avoid the use-shlex flat-concat namespace collision
on bare split / quote / quotestr / strerror names.
This commit is contained in:
5
Makefile
5
Makefile
@@ -233,6 +233,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
|
||||
$(BIN)/test_w6c_ww $(BIN)/test_ww_ww $(BIN)/test_self_rebuild \
|
||||
$(BIN)/test_dyn_ww $(BIN)/test_selfcheck $(BIN)/test_at_test_ww \
|
||||
$(BIN)/test_fmt_run $(BIN)/test_log_run $(BIN)/test_fnmatch_run \
|
||||
$(BIN)/test_shlex_run \
|
||||
$(BIN)/test_memio_run $(BIN)/test_temp_run $(BIN)/test_getopt_run \
|
||||
$(BIN)/test_base32_run $(BIN)/test_base64_run \
|
||||
$(BIN)/test_adler32_run $(BIN)/test_crc16_run \
|
||||
@@ -436,6 +437,10 @@ $(BIN)/test_fnmatch_run: test/wcc/972_fnmatch_run.c $(BIN)/ww $(BIN)/w6c \
|
||||
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
$(BIN)/test_shlex_run: test/wcc/973_shlex_run.c $(BIN)/ww $(BIN)/w6c \
|
||||
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
$(BIN)/test_memio_run: test/wcc/980_memio_run.c $(BIN)/ww $(BIN)/w6c \
|
||||
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
452
lib/shlex/shlex.ww
Normal file
452
lib/shlex/shlex.ww
Normal file
@@ -0,0 +1,452 @@
|
||||
// shlex — POSIX shell tokenizer + quoter. Port of Hare's lib/shlex
|
||||
// (ref/hare/shlex/{split.ha,escape.ha}) using ww byte-indexed cursors
|
||||
// in place of Hare's strings::iterator.
|
||||
//
|
||||
// Surface today:
|
||||
//
|
||||
// shlex.syntaxerr — !void; bad shell syntax
|
||||
// shlex.strerror(syntaxerr) — "Invalid shell syntax"
|
||||
// shlex.split(in: str) — tokenize; returns ([]str | syntaxerr)
|
||||
// shlex.quote(*io.stream, s) — write `s` shell-quoted to a sink
|
||||
// shlex.quotestr(s: str) str — quote into a fresh str
|
||||
//
|
||||
// Owning model: split() returns a fresh `[]str` of strings.dup'd
|
||||
// elements; release with [[strings.freeall]] (Hare's natural disposer
|
||||
// shape). quotestr() returns an os.alloc'd str; release via
|
||||
// `os.free(r.ptr, r.len: u64)` exactly like [[strings.dup]].
|
||||
//
|
||||
// Quoting rules (POSIX shell, byte-wise):
|
||||
//
|
||||
// - whitespace separators ' ' / '\t' / '\n' (collapse runs)
|
||||
// - '\\<c>' outside quotes: literal <c>; '\\<newline>' deleted;
|
||||
// trailing bare '\\' → syntaxerr
|
||||
// - '"..."': '\\<c>' processed inside (any <c>); unterminated → syntaxerr
|
||||
// - "'...'": literal until closing "'"; no escapes inside;
|
||||
// unterminated → syntaxerr
|
||||
// - "Empty ''" yields a literal empty token (preserves quoting
|
||||
// intent — distinguishes `cmd ''` from `cmd`)
|
||||
//
|
||||
// Divergences from Hare:
|
||||
//
|
||||
// - Drop nomem: ww os.alloc aborts on OOM (same precedent as
|
||||
// strings.dup, getopt.appendoption). Hare's
|
||||
// (...|syntaxerr|nomem) collapses to (...|syntaxerr).
|
||||
//
|
||||
// - Byte-wise iteration via i32 cursor instead of Hare's
|
||||
// strings::iterator (no UTF-8 rune iteration in the language
|
||||
// stack yet; same precedent as fnmatch). scan_double / scan_single
|
||||
// / scan_backslash work on bytes; an escaped multibyte sequence
|
||||
// round-trips as the byte sequence it appears in the input.
|
||||
//
|
||||
// - memio glue uses ww's caller-supplies-state shape:
|
||||
// let mst: memio.state; let s: io.stream;
|
||||
// memio.dynamic(&mst, &s);
|
||||
// ... io.write(&s, ...) ...
|
||||
// let view: str = memio.string(&mst); // borrowed
|
||||
// instead of Hare's `let s = memio::dynamic()` return-by-value.
|
||||
// Locked in by lib/memio's commit until cgen ships >24B return-
|
||||
// by-value (graduates "in one go" per lib/CLAUDE.md).
|
||||
//
|
||||
// - quote() takes `*io.stream` (not Hare's `io::handle`) — that's
|
||||
// already the ww-divergent shape lib/io ships. Returns
|
||||
// `(i32 | io.closed)` instead of Hare's wider io::error: lib/io's
|
||||
// stream vtable carries only `closed` on the write arm.
|
||||
//
|
||||
// - quote() emits raw bytes directly into the sink; without
|
||||
// [[memio.appendrune]] / [[memio.concat]] in lib/memio yet, the
|
||||
// "write one byte" path goes through `io.write(s, buf[0:1])`
|
||||
// against a stack `[N]u8`. Same shape as fmt's rune-arm.
|
||||
//
|
||||
// - `[]str` grown via direct rt_ensure rather than the `append`
|
||||
// builtin: cgen lowers `append(slice, element)` to a single MOVQ
|
||||
// of the first 8 bytes, which drops the `len` half of a 16B str
|
||||
// element. [[appendstr]] writes both halves through `*str`.
|
||||
// Same gap, same workaround, same comment shape as
|
||||
// [[getopt.appendoption]] (lib/getopt/getopt.ww:96-106) — kept
|
||||
// greppable across modules. When the append-builtin is fixed,
|
||||
// all three (getopt, shlex, anywhere else) collapse in one go.
|
||||
//
|
||||
// - Internal [[dupstr]] inlined rather than `use strings;`: lib/io
|
||||
// and lib/os both export read/write/close as C symbols with
|
||||
// different signatures, and `use strings;` would transitively
|
||||
// pull `use os;` (strings:6) which collides with `use io;` here.
|
||||
// Same defensive shape lib/fmt and lib/memio use until task #17
|
||||
// (cgen module-mangles fn labels) lands; at that point dupstr
|
||||
// graduates to strings.dup and the workaround retires.
|
||||
//
|
||||
// Caller layout — split:
|
||||
//
|
||||
// match (shlex.split("ls -l '/etc/passwd'")) {
|
||||
// case let toks: []str => {
|
||||
// let i: i32 = 0;
|
||||
// for (i < toks.len) { /* toks[i] */ i += 1; };
|
||||
// strings.freeall(toks);
|
||||
// };
|
||||
// case shlex.syntaxerr => { /* bad input */ };
|
||||
// };
|
||||
//
|
||||
// Caller layout — quote into a fixed buffer:
|
||||
//
|
||||
// let buf: [128]u8;
|
||||
// let mst: memio.state;
|
||||
// let s: io.stream;
|
||||
// memio.fixed(&mst, &s, buf[0:128]);
|
||||
// shlex.quote(&s, "hello world"); // writes 'hello world'
|
||||
// let view: str = memio.string(&mst);
|
||||
|
||||
use io;
|
||||
use memio;
|
||||
|
||||
// Direct rt_alloc / rt_free / rt_ensure bindings rather than `use os;`
|
||||
// — os exports read/write/close, which collide with io.read/write/close
|
||||
// under the driver's flat-scope concat. Same workaround as lib/memio,
|
||||
// lib/fmt, lib/log; retires when task #17 (cgen mod-mangling) lands.
|
||||
//
|
||||
// rt_ensure is the runtime slice-growth helper invoked by the
|
||||
// `append(s, v)` builtin. We bind it directly because the builtin's
|
||||
// expansion stores only 8 bytes of the new element (cgen emits a
|
||||
// single MOVQ), losing the `len` half of a `str` (16B).
|
||||
@symbol("rt_alloc") fn rtalloc(n: u64) *void;
|
||||
@symbol("rt_free") fn rtfree(p: *void, n: u64) void;
|
||||
@symbol("rt_ensure") fn rtensure(s: *void, membsz: u64) void;
|
||||
|
||||
// syntaxerr — the input wasn't a valid shell-tokenizable string
|
||||
// (unterminated quote / bare trailing backslash). Mirrors Hare's
|
||||
// shlex::syntaxerr; a `!void` so it can ride a tagged-union return
|
||||
// alongside `[]str`.
|
||||
export type syntaxerr = !void;
|
||||
|
||||
// strerror — human-friendly rendering of [[syntaxerr]]. Mirrors
|
||||
// Hare's shlex::strerror(syntaxerr) str.
|
||||
export fn strerror(err: syntaxerr) str = {
|
||||
return "Invalid shell syntax";
|
||||
};
|
||||
|
||||
// dupstr — local strings.dup. Inlined to keep `use strings;` out of
|
||||
// this module (would re-trip the lib/io ↔ lib/os C-symbol collision;
|
||||
// see file header). Same algorithm and same {nil, 0} handling for
|
||||
// empty input. Retires when task #17 lands.
|
||||
fn dupstr(s: str) str = {
|
||||
let r: str;
|
||||
r.ptr = nil;
|
||||
r.len = 0;
|
||||
if (s.len == 0) { return r; };
|
||||
let buf: *u8 = rtalloc(s.len: u64): *u8;
|
||||
let i: i32 = 0;
|
||||
for (i < s.len) { buf[i] = s[i]; i += 1; };
|
||||
r.ptr = buf;
|
||||
r.len = s.len;
|
||||
return r;
|
||||
};
|
||||
|
||||
// appendstr — grow `*slice` by one and store `item` (16B). Bypasses
|
||||
// the `append` builtin's first-8B-only store gap; mirror of
|
||||
// [[getopt.appendoption]] for the `[]option` case.
|
||||
fn appendstr(slice: *[]str, item: str) void = {
|
||||
let newlen: i32 = slice.len + 1;
|
||||
slice.len = newlen;
|
||||
rtensure(slice: *void, 16u64);
|
||||
let dst: *str = &slice.ptr[newlen - 1];
|
||||
dst.ptr = item.ptr;
|
||||
dst.len = item.len;
|
||||
};
|
||||
|
||||
// freepartial — drop a partially built [[split]] result on the
|
||||
// syntaxerr path. Hare uses a `defer if (!ok)` guard; ww has no defer,
|
||||
// so we hand-roll the cleanup at every error return. Mirror of
|
||||
// [[strings.freeall]] (skip empty {nil, 0} elements; skip the header
|
||||
// free if cap == 0).
|
||||
fn freepartial(slice: []str) void = {
|
||||
let i: i32 = 0;
|
||||
for (i < slice.len) {
|
||||
if (slice[i].len > 0) {
|
||||
rtfree(slice[i].ptr: *void, slice[i].len: u64);
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
if (slice.cap > 0) {
|
||||
rtfree(slice.ptr: *void, (slice.cap: u64) * 16u64);
|
||||
};
|
||||
};
|
||||
|
||||
// writebyte — send one byte to `s` via [[io.write]]. The `[1]u8` slice
|
||||
// dance mirrors fmt's rune-arm (lib/fmt/fmt.ww:210-218); memio doesn't
|
||||
// ship an appendbyte/appendrune in the ww port yet, so this is the
|
||||
// uniform write-one-byte primitive across the module.
|
||||
//
|
||||
// The dynamic memio sink never returns `io.closed`; the [[split]] +
|
||||
// [[quotestr]] callers ignore the error arm because their stream is
|
||||
// always memio.dynamic. [[quote]] (caller-supplied sink) propagates
|
||||
// it through its `(i32 | io.closed)` return.
|
||||
fn writebyte(s: *io.stream, b: u8) (i32 | io.closed) = {
|
||||
let buf: [1]u8;
|
||||
buf[0] = b;
|
||||
return io.write(s, buf[0:1]);
|
||||
};
|
||||
|
||||
// scan_backslash — consume one `\<c>` escape at `*pos`. POSIX:
|
||||
// "<backslash> followed by <newline> shall be removed" — we eat the
|
||||
// newline silently. A trailing bare backslash (`*pos == in.len`) is
|
||||
// `syntaxerr`. Otherwise the next byte is appended verbatim. Mirrors
|
||||
// Hare's scan_backslash (split.ha:86).
|
||||
fn scan_backslash(out: *io.stream, in: str, pos: *i32) (void | syntaxerr) = {
|
||||
if (*pos >= in.len) { let e: syntaxerr; return e; };
|
||||
let r: u8 = in[*pos];
|
||||
*pos += 1;
|
||||
if (r == 10u8) { return; }; // '\n' deleted
|
||||
let _w = writebyte(out, r);
|
||||
return;
|
||||
};
|
||||
|
||||
// scan_double — consume a `"..."` group at `*pos`. Closes on '"';
|
||||
// '\<c>' inside processed by [[scan_backslash]]; unterminated → err.
|
||||
// Mirrors Hare's scan_double (split.ha:110).
|
||||
fn scan_double(out: *io.stream, in: str, pos: *i32) (void | syntaxerr) = {
|
||||
let loop: bool = true;
|
||||
for (loop) {
|
||||
if (*pos >= in.len) { let e: syntaxerr; return e; };
|
||||
let r: u8 = in[*pos];
|
||||
*pos += 1;
|
||||
if (r == 34u8) { loop = false; } // '"'
|
||||
else { if (r == 92u8) { // '\\'
|
||||
let er = scan_backslash(out, in, pos);
|
||||
match (er) {
|
||||
case let e: syntaxerr => return e;
|
||||
case void => {};
|
||||
};
|
||||
} else {
|
||||
let _w = writebyte(out, r);
|
||||
}; };
|
||||
};
|
||||
return;
|
||||
};
|
||||
|
||||
// scan_single — consume a `'...'` group at `*pos`. Closes on "'";
|
||||
// no escapes inside (POSIX: single-quoted text is fully literal);
|
||||
// unterminated → err. Mirrors Hare's scan_single (split.ha:135).
|
||||
fn scan_single(out: *io.stream, in: str, pos: *i32) (void | syntaxerr) = {
|
||||
let loop: bool = true;
|
||||
for (loop) {
|
||||
if (*pos >= in.len) { let e: syntaxerr; return e; };
|
||||
let r: u8 = in[*pos];
|
||||
*pos += 1;
|
||||
if (r == 39u8) { loop = false; } // "'"
|
||||
else { let _w = writebyte(out, r); };
|
||||
};
|
||||
return;
|
||||
};
|
||||
|
||||
// split — tokenize `in` according to POSIX shell quoting rules.
|
||||
// Returns a fresh `[]str` of [[dupstr]]'d elements; on success, free
|
||||
// with [[strings.freeall]]. Empty input returns an empty slice (not
|
||||
// an error). Mirrors Hare's split (split.ha:16).
|
||||
//
|
||||
// Algorithm: a single forward pass over the input, accumulating bytes
|
||||
// into a memio.dynamic sink. Whitespace runs flush the buffer as one
|
||||
// token (skipping the flush before the first token, so leading
|
||||
// whitespace doesn't push an empty entry). The `dirty` flag tracks
|
||||
// whether the current iteration began processing input — it's the
|
||||
// signal for "an empty quote group was the only content of this
|
||||
// token", which still pushes a literal "" (e.g. `cmd ''`).
|
||||
export fn split(in: str) ([]str | syntaxerr) = {
|
||||
let mst: memio.state;
|
||||
let snk: io.stream;
|
||||
memio.dynamic(&mst, &snk);
|
||||
|
||||
let slice: []str;
|
||||
slice.ptr = nil: *str;
|
||||
slice.len = 0;
|
||||
slice.cap = 0;
|
||||
|
||||
let first: bool = true;
|
||||
let dirty: bool = false;
|
||||
let pos: i32 = 0;
|
||||
|
||||
for (pos < in.len) {
|
||||
let r: u8 = in[pos];
|
||||
pos += 1;
|
||||
dirty = true;
|
||||
|
||||
if (r == 32u8 || r == 9u8 || r == 10u8) {
|
||||
// Collapse a run of whitespace.
|
||||
let inner: bool = true;
|
||||
for (inner) {
|
||||
if (pos >= in.len) { inner = false; }
|
||||
else {
|
||||
let r2: u8 = in[pos];
|
||||
if (r2 != 32u8 && r2 != 9u8 && r2 != 10u8) {
|
||||
inner = false;
|
||||
} else {
|
||||
pos += 1;
|
||||
};
|
||||
};
|
||||
};
|
||||
if (!first) {
|
||||
let view: str = memio.string(&mst);
|
||||
let owned: str = dupstr(view);
|
||||
appendstr(&slice, owned);
|
||||
memio.reset(&mst);
|
||||
};
|
||||
dirty = false;
|
||||
} else { if (r == 92u8) { // '\\'
|
||||
let er = scan_backslash(&snk, in, &pos);
|
||||
match (er) {
|
||||
case let e: syntaxerr => {
|
||||
let _c = io.close(&snk);
|
||||
freepartial(slice);
|
||||
return e;
|
||||
};
|
||||
case void => {};
|
||||
};
|
||||
} else { if (r == 34u8) { // '"'
|
||||
let er = scan_double(&snk, in, &pos);
|
||||
match (er) {
|
||||
case let e: syntaxerr => {
|
||||
let _c = io.close(&snk);
|
||||
freepartial(slice);
|
||||
return e;
|
||||
};
|
||||
case void => {};
|
||||
};
|
||||
} else { if (r == 39u8) { // "'"
|
||||
let er = scan_single(&snk, in, &pos);
|
||||
match (er) {
|
||||
case let e: syntaxerr => {
|
||||
let _c = io.close(&snk);
|
||||
freepartial(slice);
|
||||
return e;
|
||||
};
|
||||
case void => {};
|
||||
};
|
||||
} else {
|
||||
let _w = writebyte(&snk, r);
|
||||
}; }; }; };
|
||||
|
||||
if (first) { first = false; };
|
||||
};
|
||||
|
||||
if (dirty) {
|
||||
let view: str = memio.string(&mst);
|
||||
let owned: str = dupstr(view);
|
||||
appendstr(&slice, owned);
|
||||
};
|
||||
|
||||
let _c = io.close(&snk);
|
||||
return slice;
|
||||
};
|
||||
|
||||
// issafe — true if `s` is composed entirely of bytes that pass through
|
||||
// shell tokenisation unmodified (alnum + `@%+=:,./-`). Used by
|
||||
// [[quote]] to skip emitting quotes around boring strings. Mirrors
|
||||
// Hare's is_safe (escape.ha:9), narrowed to ASCII byte tests because
|
||||
// ww has no rune iteration.
|
||||
fn issafe(s: str) bool = {
|
||||
let i: i32 = 0;
|
||||
for (i < s.len) {
|
||||
let c: u8 = s[i];
|
||||
// Hare's switch list: '@', '%', '+', '=', ':', ',', '.', '/', '-'.
|
||||
if (c == 64u8 || c == 37u8 || c == 43u8 || c == 61u8
|
||||
|| c == 58u8 || c == 44u8 || c == 46u8 || c == 47u8
|
||||
|| c == 45u8) {
|
||||
i += 1;
|
||||
} else {
|
||||
let alnum: bool = false;
|
||||
if (c >= 48u8 && c <= 57u8) { alnum = true; }; // 0-9
|
||||
if (c >= 65u8 && c <= 90u8) { alnum = true; }; // A-Z
|
||||
if (c >= 97u8 && c <= 122u8) { alnum = true; }; // a-z
|
||||
if (!alnum) { return false; };
|
||||
i += 1;
|
||||
};
|
||||
};
|
||||
return true;
|
||||
};
|
||||
|
||||
// quote — emit `s` shell-quoted to `sink`. Returns total bytes written,
|
||||
// or `io.closed` if the sink rejects mid-write. Mirrors Hare's
|
||||
// shlex::quote (escape.ha:25), modulo the narrower `io.closed`-only
|
||||
// error set on lib/io's stream vtable (Hare's `io::error` would carry
|
||||
// underread/etc., which lib/io doesn't yet model).
|
||||
//
|
||||
// Strategy:
|
||||
// - empty `s` → `''`
|
||||
// - is_safe(s) → emit raw, no quoting
|
||||
// - otherwise → wrap in single quotes; an embedded `'`
|
||||
// becomes `'"'"'` (close, double-quoted
|
||||
// literal `'`, reopen).
|
||||
export fn quote(sink: *io.stream, s: str) (i32 | io.closed) = {
|
||||
if (s.len == 0) {
|
||||
let buf: [2]u8;
|
||||
buf[0] = 39u8; buf[1] = 39u8;
|
||||
let r = io.write(sink, buf[0:2]);
|
||||
match (r) {
|
||||
case let n: i32 => return n;
|
||||
case let c: io.closed => return c;
|
||||
};
|
||||
};
|
||||
if (issafe(s)) {
|
||||
let raw: []u8;
|
||||
raw.ptr = s.ptr;
|
||||
raw.len = s.len;
|
||||
let r = io.write(sink, raw);
|
||||
match (r) {
|
||||
case let n: i32 => return n;
|
||||
case let c: io.closed => return c;
|
||||
};
|
||||
};
|
||||
|
||||
let total: i32 = 0;
|
||||
let r1 = writebyte(sink, 39u8); // "'"
|
||||
match (r1) {
|
||||
case let n: i32 => { total += n; };
|
||||
case let c: io.closed => return c;
|
||||
};
|
||||
let i: i32 = 0;
|
||||
for (i < s.len) {
|
||||
let c: u8 = s[i];
|
||||
if (c == 39u8) { // "'" → '"'"'
|
||||
let pat: [5]u8;
|
||||
pat[0] = 39u8; pat[1] = 34u8; pat[2] = 39u8;
|
||||
pat[3] = 34u8; pat[4] = 39u8;
|
||||
let rr = io.write(sink, pat[0:5]);
|
||||
match (rr) {
|
||||
case let n: i32 => { total += n; };
|
||||
case let c2: io.closed => return c2;
|
||||
};
|
||||
} else {
|
||||
let rr = writebyte(sink, c);
|
||||
match (rr) {
|
||||
case let n: i32 => { total += n; };
|
||||
case let c2: io.closed => return c2;
|
||||
};
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
let r2 = writebyte(sink, 39u8); // "'"
|
||||
match (r2) {
|
||||
case let n: i32 => { total += n; };
|
||||
case let c: io.closed => return c;
|
||||
};
|
||||
return total;
|
||||
};
|
||||
|
||||
// quotestr — convenience: [[quote]] into a fresh memio.dynamic sink
|
||||
// and return the dup'd result as an owned `str`. Caller releases via
|
||||
// `os.free(r.ptr, r.len: u64)`. Mirrors Hare's shlex::quotestr
|
||||
// (escape.ha:50).
|
||||
//
|
||||
// memio.dynamic's writes never return io.closed, so the `match` on
|
||||
// `quote(...)` discards the error arm — it can't reach this caller's
|
||||
// sink. quote()'s `(i32 | io.closed)` shape is preserved for the
|
||||
// caller-supplied-sink path.
|
||||
export fn quotestr(s: str) str = {
|
||||
let mst: memio.state;
|
||||
let snk: io.stream;
|
||||
memio.dynamic(&mst, &snk);
|
||||
|
||||
let _r = quote(&snk, s);
|
||||
let view: str = memio.string(&mst);
|
||||
let owned: str = dupstr(view);
|
||||
|
||||
let _c = io.close(&snk);
|
||||
return owned;
|
||||
};
|
||||
198
lib/shlex/shlextest.ww
Normal file
198
lib/shlex/shlextest.ww
Normal file
@@ -0,0 +1,198 @@
|
||||
// shlextest — exercises lib/shlex. Run with
|
||||
// `out/bin/ww run lib/shlex/shlextest.ww`.
|
||||
//
|
||||
// Two cohorts, grouped Hare-style — one @test fn per cohort, table-
|
||||
// driven inside via per-arity helpers:
|
||||
//
|
||||
// • split rows go through [[check1]] / [[check2]] / [[check3]] /
|
||||
// [[checkerr]]. Inputs are lifted from ref/hare/shlex/+test.ha
|
||||
// @test fn split() (the de-facto spec for this port). Per-row
|
||||
// arity is fixed (1, 2, or 3 expected tokens across the Hare
|
||||
// cases), so we ship per-arity helpers rather than a full
|
||||
// variadic check that would obscure the row data.
|
||||
//
|
||||
// • quote rows go through [[checkquote]] — uniform (input, expected)
|
||||
// shape against a memio.dynamic sink, mirroring Hare's escape.ha
|
||||
// testquote table.
|
||||
//
|
||||
// Failure path: each @test fn bumps `signalled` to its slot index,
|
||||
// the helpers do `exit(signalled + 10)` on miscompare so the harness
|
||||
// reports `WEXITSTATUS = 11..N` pointing at the failing scenario.
|
||||
// Same convention as fnmatchtest / logtest.
|
||||
|
||||
use shlex;
|
||||
use io;
|
||||
use memio;
|
||||
|
||||
// Direct rt_syscall binding rather than `use os;` — os exports
|
||||
// read/write/close, which collide with io.read/write/close under the
|
||||
// driver's flat-scope concat. Mirrors fnmatchtest / logtest / fmttest
|
||||
// / bufiotest.
|
||||
@symbol("rt_syscall") fn syscall1ww(num: i64, a: i64) i64;
|
||||
fn doexit(code: i32) void = {
|
||||
syscall1ww(60i64, code: i64);
|
||||
};
|
||||
|
||||
let signalled: i32 = 0;
|
||||
|
||||
fn fail() void = { doexit(signalled + 10); };
|
||||
|
||||
fn streq(a: str, b: str) bool = {
|
||||
if (a.len != b.len) { return false; };
|
||||
let i: i32 = 0;
|
||||
for (i < a.len) {
|
||||
if (a[i] != b[i]) { return false; };
|
||||
i += 1;
|
||||
};
|
||||
return true;
|
||||
};
|
||||
|
||||
// checkN — split `in` and assert the result is a slice of length N
|
||||
// matching the named expected tokens. Leaks the result (test process
|
||||
// is short-lived; same precedent as fnmatchtest).
|
||||
|
||||
fn check1(in: str, e0: str) void = {
|
||||
let r = shlex.split(in);
|
||||
match (r) {
|
||||
case shlex.syntaxerr => { fail(); };
|
||||
case let s: []str => {
|
||||
if (s.len != 1) { fail(); };
|
||||
if (!streq(s[0], e0)) { fail(); };
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
fn check2(in: str, e0: str, e1: str) void = {
|
||||
let r = shlex.split(in);
|
||||
match (r) {
|
||||
case shlex.syntaxerr => { fail(); };
|
||||
case let s: []str => {
|
||||
if (s.len != 2) { fail(); };
|
||||
if (!streq(s[0], e0)) { fail(); };
|
||||
if (!streq(s[1], e1)) { fail(); };
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
fn check3(in: str, e0: str, e1: str, e2: str) void = {
|
||||
let r = shlex.split(in);
|
||||
match (r) {
|
||||
case shlex.syntaxerr => { fail(); };
|
||||
case let s: []str => {
|
||||
if (s.len != 3) { fail(); };
|
||||
if (!streq(s[0], e0)) { fail(); };
|
||||
if (!streq(s[1], e1)) { fail(); };
|
||||
if (!streq(s[2], e2)) { fail(); };
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
fn checkerr(in: str) void = {
|
||||
let r = shlex.split(in);
|
||||
match (r) {
|
||||
case shlex.syntaxerr => {};
|
||||
case let s: []str => { fail(); };
|
||||
};
|
||||
};
|
||||
|
||||
fn checkempty(in: str) void = {
|
||||
let r = shlex.split(in);
|
||||
match (r) {
|
||||
case shlex.syntaxerr => { fail(); };
|
||||
case let s: []str => {
|
||||
if (s.len != 0) { fail(); };
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
fn checkquote(in: str, expected: str) void = {
|
||||
let mst: memio.state;
|
||||
let snk: io.stream;
|
||||
memio.dynamic(&mst, &snk);
|
||||
|
||||
let r = shlex.quote(&snk, in);
|
||||
let n: i32 = 0;
|
||||
match (r) {
|
||||
case let v: i32 => { n = v; };
|
||||
case io.closed => { fail(); };
|
||||
};
|
||||
if (n != expected.len) { fail(); };
|
||||
|
||||
let view: str = memio.string(&mst);
|
||||
if (!streq(view, expected)) { fail(); };
|
||||
|
||||
let _c = io.close(&snk);
|
||||
};
|
||||
|
||||
// ---- split: Hare's @test fn split() table --------------------------
|
||||
//
|
||||
// 9 success rows + 3 syntaxerr rows ported VERBATIM from
|
||||
// ref/hare/shlex/+test.ha; plus one ww-specific edge (empty input →
|
||||
// empty []str) confirmed by drew.
|
||||
//
|
||||
// Local @test fns are `test_*`-prefixed because `use shlex;` flat-
|
||||
// concats shlex's exported names (split / quote / quotestr / strerror)
|
||||
// into the fixture's namespace, and bare `fn split() ...` would
|
||||
// duplicate-define them. Retires when task #17 (cgen mod-mangles fn
|
||||
// labels) lands.
|
||||
|
||||
@test fn test_split() void = {
|
||||
check1("hello\\ world", "hello world");
|
||||
check1("'hello\\ world'", "hello\\ world");
|
||||
check1("\"hello\\\\world\"", "hello\\world");
|
||||
// "hello "'"'"world"'"' → hello "world"
|
||||
check1("\"hello \"'\"'\"world\"'\"'", "hello \"world\"");
|
||||
check3("hello '' world", "hello", "", "world");
|
||||
check2("Empty ''", "Empty", "");
|
||||
check2(" Leading spaces", "Leading", "spaces");
|
||||
check3("with\\ backslashes 'single quoted' \"double quoted\"",
|
||||
"with backslashes", "single quoted", "double quoted");
|
||||
check2("'multiple spaces' 42", "multiple spaces", "42");
|
||||
|
||||
// Invalid
|
||||
checkerr("\"dangling double quote");
|
||||
checkerr("'dangling single quote");
|
||||
checkerr("unterminated\\ backslash \\");
|
||||
|
||||
// Empty input → empty []str (ww edge confirmed by drew).
|
||||
checkempty("");
|
||||
};
|
||||
|
||||
// ---- quote: Hare's testquote rows + the empty-input edge ----------
|
||||
//
|
||||
// 4 rows from ref/hare/shlex/+test.ha @test fn quote(). The empty-
|
||||
// input row (→ `''`) is implementation-specific (Hare's testquote
|
||||
// doesn't cover it) but is documented behaviour per shlex.ww's
|
||||
// quote() header — exercised here so the contract is load-bearing.
|
||||
|
||||
@test fn test_quote() void = {
|
||||
checkquote("hello", "hello");
|
||||
checkquote("hello world", "'hello world'");
|
||||
checkquote("'hello' \"world\"", "''\"'\"'hello'\"'\"' \"world\"'");
|
||||
checkquote("hello\\world", "'hello\\world'");
|
||||
checkquote("", "''");
|
||||
};
|
||||
|
||||
// ---- quotestr ------------------------------------------------------
|
||||
|
||||
@test fn test_quotestr() void = {
|
||||
let r: str = shlex.quotestr("hello world");
|
||||
if (!streq(r, "'hello world'")) { fail(); };
|
||||
// leak r — short-lived test process, same precedent as fnmatchtest.
|
||||
};
|
||||
|
||||
// ---- strerror ------------------------------------------------------
|
||||
|
||||
@test fn test_strerror() void = {
|
||||
let e: shlex.syntaxerr;
|
||||
let s: str = shlex.strerror(e);
|
||||
if (!streq(s, "Invalid shell syntax")) { fail(); };
|
||||
};
|
||||
|
||||
export fn main() i32 = {
|
||||
signalled = 1; test_split();
|
||||
signalled = 2; test_quote();
|
||||
signalled = 3; test_quotestr();
|
||||
signalled = 4; test_strerror();
|
||||
return 0;
|
||||
};
|
||||
@@ -122,6 +122,32 @@ export fn dup(s: str) str = {
|
||||
return r;
|
||||
};
|
||||
|
||||
// freeall — release every str element in `s` (those that were
|
||||
// individually allocated) plus the slice's backing storage. Mirrors
|
||||
// Hare's strings::freeall — the natural disposer for any function
|
||||
// returning a fresh `[]str` of dup'd elements (e.g. shlex.split).
|
||||
//
|
||||
// Each element is freed via os.free at its own length; the slice
|
||||
// header storage is freed at `cap * 16` bytes (one str = 16B). Empty
|
||||
// elements (`{nil, 0}` from a zero-length dup) are skipped — calling
|
||||
// os.free on a nil pointer at len 0 would tickle the rt_free guard
|
||||
// that the runtime treats as a logic bug.
|
||||
//
|
||||
// `cap == 0` means the slice was never grown (empty `[]str` with no
|
||||
// backing allocation); skip the header free in that case too.
|
||||
export fn freeall(s: []str) void = {
|
||||
let i: i32 = 0;
|
||||
for (i < s.len) {
|
||||
if (s[i].len > 0) {
|
||||
os.free(s[i].ptr: *void, s[i].len: u64);
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
if (s.cap > 0) {
|
||||
os.free(s.ptr: *void, (s.cap: u64) * 16u64);
|
||||
};
|
||||
};
|
||||
|
||||
// rbyteindex — last byte position of `needle` in `s`. Mirrors Hare's
|
||||
// strings::rbyteindex. Rune needle scans for the byte that encodes it
|
||||
// (ASCII only); str needle scans for the substring. Empty str needle
|
||||
|
||||
52
test/wcc/973_shlex_run.c
Normal file
52
test/wcc/973_shlex_run.c
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 973_shlex_run — execute the lib/shlex @test fixture under the
|
||||
* C-side `ww run` driver and assert exit 0.
|
||||
*
|
||||
* shlex is a leaf over io + memio (split returns []str of dup'd
|
||||
* elements; quote writes through *io.stream). The fixture
|
||||
* (shlextest.ww) carries its own `export fn main()` that drives the
|
||||
* @test fns and signals which case failed via the exit code, so this
|
||||
* file is a thin wrapper — no @test scanning, no synthetic main
|
||||
* generation.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
static int
|
||||
runwait(const char *cmd)
|
||||
{
|
||||
int rc = system(cmd);
|
||||
if (rc == -1) return -1;
|
||||
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
const char *bin = getenv("BIN");
|
||||
if (!bin) bin = "out/bin";
|
||||
char absbin[1024];
|
||||
if (bin[0] != '/') {
|
||||
char cwd[1024];
|
||||
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
|
||||
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
|
||||
bin = absbin;
|
||||
}
|
||||
char cwd[1024];
|
||||
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
|
||||
|
||||
const char *src = "lib/shlex/shlextest.ww";
|
||||
char path[1024], cmd[2048];
|
||||
snprintf(path, sizeof path, "%s/%s", cwd, src);
|
||||
snprintf(cmd, sizeof cmd, "%s/ww run %s", bin, path);
|
||||
int rc = runwait(cmd);
|
||||
if (rc != 0) {
|
||||
fprintf(stderr, "shlex_run FAIL: %s exited %d\n", src, rc);
|
||||
return 1;
|
||||
}
|
||||
printf("shlex_run: %s ok\n", src);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user