mirror of
https://git.pumpin.online/funkpower/catena.git
synced 2026-10-23 03:07:13 +00:00
each crate carries its own readme, and the contract describes rather than instructs
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
# catena-cli
|
||||
|
||||
The parts of a filter every command does the same way.
|
||||
|
||||
Part of [catena](../../README.md).
|
||||
|
||||
## What it is
|
||||
|
||||
A library. Not a framework — a handful of functions, each of which exists
|
||||
because getting it wrong is easy and the consequence is a pipeline that
|
||||
misreports why it stopped.
|
||||
|
||||
Every command in this workspace is built on it, which is why they all behave
|
||||
the same at the edges.
|
||||
|
||||
## When you would use it
|
||||
|
||||
- **Writing a filter that joins this pipeline**, and wanting it to behave like
|
||||
the others without rereading [the contract](../../docs/contract.md).
|
||||
- **Anywhere a small Unix filter is wanted**, in or out of this workspace.
|
||||
|
||||
## Parsing a command line
|
||||
|
||||
```rust
|
||||
fn main() -> std::process::ExitCode {
|
||||
let args: Args = match catena_cli::parse() {
|
||||
Ok(args) => args,
|
||||
Err(code) => return code,
|
||||
};
|
||||
// …
|
||||
}
|
||||
```
|
||||
|
||||
`--help` and `--version` are not errors and leave with `0`; anything else
|
||||
wrong with the arguments leaves with `64`, which is what the rest of the suite
|
||||
means by "the command line was wrong". Argument parsers tend to pick their own
|
||||
number for this, and a pipeline that routes on exit codes then has two
|
||||
different meanings for the same one.
|
||||
|
||||
## Reading the input
|
||||
|
||||
```rust
|
||||
let text = match catena_cli::read_text(args.path.as_deref()) {
|
||||
Ok(text) => text,
|
||||
Err((code, why)) => {
|
||||
eprintln!("mytool: {why}");
|
||||
return ExitCode::from(code);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
`read_text` tells a file that cannot be read from one that is not text: the
|
||||
first is an I/O failure (`74`) and the second is bad input (`65`). A caller
|
||||
routing on the exit code needs to tell those apart — one is worth retrying and
|
||||
the other never is.
|
||||
|
||||
`read_lossy` is the same but replaces anything that is not UTF-8, for formats
|
||||
that routinely arrive in another encoding. A subtitle file downloaded from
|
||||
anywhere is as likely to be Latin-1, and a cue with one wrong character still
|
||||
has its timing and most of its words.
|
||||
|
||||
Both read stdin when given no path.
|
||||
|
||||
## Writing the output
|
||||
|
||||
```rust
|
||||
catena_cli::stream("mytool", &records) // NDJSON, one per line, flushed
|
||||
catena_cli::text("mytool", &cleaned) // plain text
|
||||
```
|
||||
|
||||
Both return the exit code to leave with. Both treat a consumer that closed the
|
||||
pipe as the end of the job rather than as a failure — `… | head -1` exits `0`,
|
||||
like every other filter — and both flush as they go, so a slow stage
|
||||
downstream sees a record as soon as it exists.
|
||||
|
||||
For a loop that needs to do something between records:
|
||||
|
||||
```rust
|
||||
let mut out = std::io::BufWriter::new(std::io::stdout().lock());
|
||||
for record in records {
|
||||
// …
|
||||
}
|
||||
catena_cli::finish("mytool", out)
|
||||
```
|
||||
|
||||
## A whole filter
|
||||
|
||||
```rust
|
||||
use std::process::ExitCode;
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(version, about)]
|
||||
struct Args {
|
||||
path: Option<std::path::PathBuf>,
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args: Args = match catena_cli::parse() {
|
||||
Ok(args) => args,
|
||||
Err(code) => return code,
|
||||
};
|
||||
let text = match catena_cli::read_text(args.path.as_deref()) {
|
||||
Ok(text) => text,
|
||||
Err((code, why)) => {
|
||||
eprintln!("mytool: {why}");
|
||||
return ExitCode::from(code);
|
||||
}
|
||||
};
|
||||
catena_cli::stream("mytool", &do_the_work(&text))
|
||||
}
|
||||
```
|
||||
|
||||
That is the whole shape. Everything a tool here actually *does* lives in its
|
||||
library crate; the command is argument parsing, input, output and an exit
|
||||
code, and should stay under a hundred and fifty lines.
|
||||
|
||||
## See also
|
||||
|
||||
- [The contract](../../docs/contract.md) — what these functions are enforcing.
|
||||
- [`corpus-core`](../corpus-core/README.md) — the records and the exit-code constants.
|
||||
Reference in New Issue
Block a user