Command-line Programs

A program run from a terminal reads its flags, finds the files it works on, runs other programs, and sometimes keeps running while files change. Each of those has a builtin type; the outside world comes through context values - &args, &fs, &stdout - so tests inject their own.

Flags

@args on a struct makes its fields the program's flags, and a @main taking the struct as its parameter gets them filled in from the command line before its body runs. Each field is a flag named after it - maxConnections would be --max-connections - read as the type the field is declared with, so the check that a port fits a U16 comes with it, and the error names the flag. A field keeps its default when its flag isn't given; one without a default, like token, has to be given, unless its type is optional (String?). @arg says what a flag is for, and can give it a short letter of its own.

The struct gets parse(args), which tests call with made-up arguments, and help(). Run with --help, the program prints the help and stops; given a flag that's wrong, it prints what's wrong and the help to &stderr and ends with exit code 2:

How flags are read

--name=value and --name value both work. A Boolean field is set by its flag alone and cleared by --no-name; an Array field takes its flag again and again; -p works for a field that's alone in starting with p, or that @arg(short ≔ "p") gives it. Arguments that aren't flags, and everything after --, go to a field named rest - without one they're an error.

Flags with an optional value

@arg(alone ≔ value) lets a flag be given without a value - the field is then value - so --test and --test=e2e both work. Such a flag takes a value only after =, so the word after it is never mistaken for one. A list field with separator ≔ "," splits a value on commas, each piece trimmed, and adds up every time the flag is given. A field named passed takes what follows --, kept apart from rest - for a program that runs another and hands it arguments. about is a line before the usage, and autoShort ≔ false leaves out the first-letter short flags, so only those @arg gives exist - for a program with so many flags that first letters would be a surprise. -h asks for the help too, when no field has h.

Without @args

Arguments.parse reads flags into any struct the same way, given a value of it to start from - for a struct you'd rather not mark, or several sets of flags in one program.

Arguments.parse(args, defaults) gives defaults with each flag given set - every field keeps its value in defaults when no flag sets it.

Arguments.help writes the usage text from the same struct:

Finding files

&fs.glob(pattern, from) gives every path under from the pattern matches, relative to it and sorted. A pattern is /-separated segments: * is any run of characters but /, ? any one, [abc] or [a-z] one of a set ([!abc] one not in it), {a,b} either alternative, and a whole ** segment any number of directories, none included. Only directories the pattern can reach are listed, so src/*.rfn doesn't walk the whole tree. Every file system has glob - the real one and MemoryFileSystem alike - and Path.matchesGlob tests one path.

Running other programs

Command.run() runs a command to its end and gives everything it wrote (see Command). start() runs it beside the program instead, as a Process: write goes to its input, readLine and read give its output as it comes, closeInput tells it there's no more, and wait gives its exit code. pipe joins commands the way a shell's | does, without a shell reading the words. Reading waits a little at a time and asks &cancellation in between, so a cancelled program stops waiting. Processes need a JavaScript host - Bun's or Node's, as --run has.

Watching files

A FileWatcher reports files created, changed and removed under a path as it happens. next() waits for the next change; changes(within) gathers every change in a short while, each once - an editor saving a file often writes it several times. Changes made once FileWatcher.new has returned are always seen.