ref: 21947578303cdb0fcb4b0cdfe80aa9dab5f6b447
dir: /appl/cmd/sh/csv.b/
implement Shellbuiltin; # parse/generate comma-separated values. include "sys.m"; sys: Sys; include "draw.m"; include "sh.m"; sh: Sh; Listnode, Context: import sh; myself: Shellbuiltin; include "bufio.m"; bufio: Bufio; Iobuf: import bufio; initbuiltin(ctxt: ref Context, shmod: Sh): string { sys = load Sys Sys->PATH; sh = shmod; myself = load Shellbuiltin "$self"; if (myself == nil) ctxt.fail("bad module", sys->sprint("csv: cannot load self: %r")); bufio = load Bufio Bufio->PATH; if (bufio == nil) ctxt.fail("bad module", sys->sprint("csv: cannot load: %s: %r", Bufio->PATH)); ctxt.addbuiltin("getcsv", myself); ctxt.addsbuiltin("csv", myself); return nil; } whatis(nil: ref Sh->Context, nil: Sh, nil: string, nil: int): string { return nil; } getself(): Shellbuiltin { return myself; } runbuiltin(c: ref Sh->Context, nil: Sh, cmd: list of ref Sh->Listnode, last: int): string { return builtin_getcsv(c, cmd, last); } runsbuiltin(c: ref Sh->Context, nil: Sh, cmd: list of ref Sh->Listnode): list of ref Listnode { return sbuiltin_csv(c, cmd); } builtin_getcsv(ctxt: ref Context, argv: list of ref Listnode, nil: int) : string { n := len argv; if (n != 2 || !iscmd(hd tl argv)) builtinusage(ctxt, "getcsv {cmd}"); cmd := hd tl argv :: ctxt.get("*"); stdin := bufio->fopen(sys->fildes(0), Sys->OREAD); if (stdin == nil) ctxt.fail("bad input", sys->sprint("getcsv: cannot open stdin: %r")); status := ""; ctxt.push(); for(;;){ { for (;;) { line: list of ref Listnode = nil; sl := readcsvline(stdin); if (sl == nil) break; for (; sl != nil; sl = tl sl) line = ref Listnode(nil, hd sl) :: line; ctxt.setlocal("line", line); status = setstatus(ctxt, ctxt.run(cmd, 0)); } ctxt.pop(); return status; } exception e{ "fail:*" => ctxt.pop(); if (loopexcept(e) == BREAK) return status; ctxt.push(); } } } CONTINUE, BREAK: con iota; loopexcept(ename: string): int { case ename[5:] { "break" => return BREAK; "continue" => return CONTINUE; * => raise ename; } return 0; } iscmd(n: ref Listnode): int { return n.cmd != nil || (n.word != nil && n.word[0] == '{'); } builtinusage(ctxt: ref Context, s: string) { ctxt.fail("usage", "usage: " + s); } setstatus(ctxt: ref Context, val: string): string { ctxt.setlocal("status", ref Listnode(nil, val) :: nil); return val; } # in csv format, is it possible to distinguish between a line containing # one empty field and a line containing no fields at all? # what does each one look like? readcsvline(iob: ref Iobuf): list of string { sl: list of string; for(;;) { (s, eof) := readcsvword(iob); if (sl == nil && s == nil && eof) return nil; c := Bufio->EOF; if (!eof) c = iob.getc(); sl = s :: sl; if (c == '\n' || c == Bufio->EOF) return sl; } } sbuiltin_csv(nil: ref Context, val: list of ref Listnode): list of ref Listnode { val = tl val; if (val == nil) return nil; s := s2qv(word(hd val)); for (val = tl val; val != nil; val = tl val) s += "," + s2qv(word(hd val)); return ref Listnode(nil, s) :: nil; } s2qv(s: string): string { needquote := 0; needscan := 0; for (i := 0; i < len s; i++) { c := s[i]; if (c == '\n' || c == ',') needquote = 1; else if (c == '"') { needquote = 1; needscan = 1; } } if (!needquote) return s; if (!needscan) return "\"" + s + "\""; r := "\""; for (i = 0; i < len s; i++) { c := s[i]; if (c == '"') r[len r] = c; r[len r] = c; } r[len r] = '"'; return r; } readcsvword(iob: ref Iobuf): (string, int) { s := ""; case c := iob.getc() { '"' => for (;;) { case c = iob.getc() { Bufio->EOF => return (s, 1); '"' => case c = iob.getc() { '"' => s[len s] = '"'; '\n' or ',' => iob.ungetc(); return (s, 0); Bufio->EOF => return (s, 1); * => # illegal iob.ungetc(); (t, eof) := readcsvword(iob); return (s + t, eof); } * => s[len s] = c; } } ',' or '\n' => iob.ungetc(); return (s, 0); Bufio->EOF => return (nil, 1); * => s[len s] = c; for (;;) { case c = iob.getc() { ',' or '\n' => iob.ungetc(); return (s, 0); '"' => # illegal iob.ungetc(); (t, eof) := readcsvword(iob); return (s + t, eof); Bufio->EOF => return (s, 1); * => s[len s] = c; } } } } word(n: ref Listnode): string { if (n.word != nil) return n.word; if (n.cmd != nil) n.word = sh->cmd2string(n.cmd); return n.word; }