mirror of
https://git.pumpin.online/funkpower/catena.git
synced 2026-10-22 18:57:14 +00:00
122 lines
3.5 KiB
Markdown
122 lines
3.5 KiB
Markdown
# 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.
|