ref: babf901b4a508c3ec5d1f89655f10377bbdf9637
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;
}