Tuesday, May 5, 2015

Commands and channels in Clive: Sam is gone.

Recently I removed the port of the venerable Sam command language in Clive.
This command language can do things like iterate for all portions of text matching addresses indicated by regular expressions and then, for example, for each portion matching another expression, apply a command or loop in some other way.

As part of the Clive application framework (still WIP), I ported most of the commands to the new standard setup: commands have a series of I/O channels that forward interface{} data. When going outside of the Clive world, only []byte is actually written into off-limits channels.

By convention, commands expect data to be just []byte messages, forwarded through std. IO channels (similar to file descriptors in UNIX, but using -modified- Go channels).

Now, the interesting thing is the result for these conventions:

  • All commands forward all messages they don't understand. 
  • Commands finding files send not just file contents, but send a zx.Dir for each file found before actually sending the file data to standard output (more on this later in this post).
  • Only []byte messages are actually considered data, all other types can be used to let friend commands talk without disrupting others in the command pipeline.
To illustrate the consequences, we can run this pipeline in Clive:


gf /tmp/echo,1 | gx  '(^func[^\n]*\n)' '(^}\n)' | gg Run | 
gp '~m.*&1' | trex -u | pf -ws,

This is just an example, I'll explain the bits and pieces; but the interesting thing is how things can work together so in Clive we can use separate commands to do what in UNIX we would have to do in a single process, or would be a lot harder to split into separate commands.

The first command, gf /tmp/echo,1  issues a Find request to the ZX file system and sends to its std. output channel a stream of data for matching files. In this case, we asked for files under /tmp/echo with depth not greater than 1. Here, filtering of interesting files happens at the file server, which sends to the client machine a stream of directory entries for matching files plus data for each one (after its directory entry).

Now, gx is mostly a grep command. In this case, it simply sends to its output all text enclosed in text matched by the two given regexps. It can be used in other ways; the one in the example would report as a []byte in the output each function or method defined in the go source.
The funny thing is that gx (as called) also writes unmatched input to the output, but uses a string data type instead. Furthermore, it posts addresses (text lines and rune ranges) to its output, should the user want to print that.

The next command, gg, receives thus the entire text for the files found, but only the portions matched by the previous gx are []byte, and thus all other data is forwarded but otherwise ignored. This command sends to the output those messages containing the given expression. At this point, only functions containing Run are in the output as []byte.

To make the example more fun, gp greps directory entries in the stream and does not forward any file to the output unless its directory entry matches the predicate given. We could have given it to gf instead, but this is more fun. At this point, only functions containing Run in files with names starting with m and with a depth no greater than 1 are in the output. 

To have more fun, trex translates the input text to uppercase. Note that all unmatched data is still forwarded (but not as []byte), because we didn't suppress it from the stream in any of the previous commands.

Finally, pf -ws is a funny command that, one file at a time, reads all the file data into memory and, once the data is completed, writes the data back to the file (using its Dir entry as found in the stream to locate the file to write). We asked it to write the files and to include also data sent for them in string messages (and not just the "official" []byte messages). Thus, this is like a "save the edited files" command in Sam.

All the files processed by previous commands in the stream are written to disk at this point. Because the command buffers in memory the files going through the stream, we don't have the "cat f > f" effect of truncating the files by mistake.

The pipe-line may be silly, but it shows how bits and pieces fit together in Clive. I was not able to do these things in any other system I used before. Clive is fun. Go is fun.

The output of the pipe is also fun (using pf -x instead):

/tmp/echo/mecho.go:#174,#716:
FUNC RUN() {
        X := &XCMD{CTX: APP.APPCTX()}
        X.FLAGS = OPT.NEW("{ARG}")
        X.NEWFLAG("D", "DEBUG", &X.DEBUG)
        X.NEWFLAG("G", "DON'T ADD A FINAL NEWLINE", &X.NFLAG)
        ARGS, ERR := X.PARSE(X.ARGS)
        IF ERR != NIL {
                X.USAGE()
                APP.EXITS("USAGE")
        }
        VAR B BYTES.BUFFER
        FOR I, ARG := RANGE ARGS {
                B.WRITESTRING(ARG)
                IF I < LEN(ARGS)-1 {
                        B.WRITESTRING(" ")
                }
        }
        IF !X.NFLAG {
                B.WRITESTRING("\N")
        }
        OUT := APP.OUT()
        OK := OUT <- B.BYTES()
        IF !OK {
                APP.WARN("STDOUT: %S", CERROR(OUT))
                APP.EXITS(CERROR(OUT))
        }
        APP.EXITS(NIL)
}


 I admit that Clive is even more user-friendly than UNIX ;)
But I also admit I like that...