mirror of
https://git.pumpin.online/funkpower/catena.git
synced 2026-10-22 18:57:14 +00:00
every tool writes its own manual page
This commit is contained in:
Generated
+17
@@ -204,6 +204,7 @@ name = "catena-cli"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
|
"clap_mangen",
|
||||||
"corpus-core",
|
"corpus-core",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@@ -295,6 +296,16 @@ version = "1.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
|
checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_mangen"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "211d617eaa4b735c96c9e0228fcbdb5120ef623f2b8cb67ffb84c3e02dbc28a4"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
"roff",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "colorchoice"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
@@ -1098,6 +1109,12 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "roff"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "323c417e1d9665a65b263ec744ba09030cfb277e9daa0b018a4ab62e57bc8189"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rrf"
|
name = "rrf"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ booksniff = { path = "crates/booksniff", version = "0.1.0" }
|
|||||||
emb = { path = "crates/emb", version = "0.1.0" }
|
emb = { path = "crates/emb", version = "0.1.0" }
|
||||||
|
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
clap_mangen = "0.3"
|
||||||
deunicode = "1"
|
deunicode = "1"
|
||||||
fancy-regex = "0.16"
|
fancy-regex = "0.16"
|
||||||
# 0.38 is the last release that builds on the pinned toolchain; the ones
|
# 0.38 is the last release that builds on the pinned toolchain; the ones
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ these go into a pipeline without first reading how it works:
|
|||||||
- The same input gives the same output, on every run and every machine.
|
- The same input gives the same output, on every run and every machine.
|
||||||
- `--json` is the stable form, and does not change shape without a major
|
- `--json` is the stable form, and does not change shape without a major
|
||||||
version.
|
version.
|
||||||
|
- `--man` writes the program's manual page, generated from the same definition
|
||||||
|
`--help` is.
|
||||||
|
|
||||||
The whole of it, with the reasoning: [what these programs
|
The whole of it, with the reasoning: [what these programs
|
||||||
promise](docs/contract.md).
|
promise](docs/contract.md).
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ repository.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
|
clap_mangen.workspace = true
|
||||||
corpus-core.workspace = true
|
corpus-core.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
//! - a consumer that closed the pipe is the end of the job, not a failure;
|
//! - a consumer that closed the pipe is the end of the job, not a failure;
|
||||||
//! - a record is flushed as it is written, so a slow stage downstream sees it
|
//! - a record is flushed as it is written, so a slow stage downstream sees it
|
||||||
//! immediately.
|
//! immediately.
|
||||||
|
//!
|
||||||
|
//! It also gives every tool `--man`, so that the manual page and the help
|
||||||
|
//! text can never say different things.
|
||||||
|
|
||||||
use std::io::{BufWriter, Write};
|
use std::io::{BufWriter, Write};
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
@@ -21,13 +24,68 @@ use serde::Serialize;
|
|||||||
/// wrong with the arguments leaves with 64, which is what the rest of the
|
/// wrong with the arguments leaves with 64, which is what the rest of the
|
||||||
/// suite means by "the command line was wrong".
|
/// suite means by "the command line was wrong".
|
||||||
pub fn parse<T: clap::Parser>() -> Result<T, ExitCode> {
|
pub fn parse<T: clap::Parser>() -> Result<T, ExitCode> {
|
||||||
T::try_parse().map_err(|e| {
|
let command = T::command().arg(
|
||||||
|
clap::Arg::new("man")
|
||||||
|
.long("man")
|
||||||
|
.action(clap::ArgAction::SetTrue)
|
||||||
|
.help("Write this page as a manual page, in roff"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Looked for before parsing, the way `--help` is: a mode, not an option,
|
||||||
|
// and not blocked by an argument the mode does not need. Anything after a
|
||||||
|
// bare `--` is a value and never a flag.
|
||||||
|
if std::env::args()
|
||||||
|
.skip(1)
|
||||||
|
.take_while(|argument| argument != "--")
|
||||||
|
.any(|argument| argument == "--man")
|
||||||
|
{
|
||||||
|
return Err(man(command));
|
||||||
|
}
|
||||||
|
|
||||||
|
let leave = |e: clap::Error| {
|
||||||
let _ = e.print();
|
let _ = e.print();
|
||||||
match e.use_stderr() {
|
match e.use_stderr() {
|
||||||
true => ExitCode::from(exit::USAGE),
|
true => ExitCode::from(exit::USAGE),
|
||||||
false => ExitCode::from(exit::OK),
|
false => ExitCode::from(exit::OK),
|
||||||
}
|
}
|
||||||
})
|
};
|
||||||
|
let matches = command.try_get_matches().map_err(leave)?;
|
||||||
|
T::from_arg_matches(&matches).map_err(leave)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write the manual page for a command and leave.
|
||||||
|
///
|
||||||
|
/// Generated from the same definition `--help` is, because a manual page kept
|
||||||
|
/// by hand is a manual page that is wrong.
|
||||||
|
fn man(command: clap::Command) -> ExitCode {
|
||||||
|
// A package holding two commands would otherwise put the package's name
|
||||||
|
// at the head of both pages, and `man vocabmerge` would open bibclean.
|
||||||
|
let command = match invoked_as() {
|
||||||
|
// Leaked because clap holds names for the life of the program,
|
||||||
|
// which from here is one page of roff and an exit.
|
||||||
|
Some(name) => command.name(&*name.leak()),
|
||||||
|
None => command,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut roff = Vec::new();
|
||||||
|
if let Err(e) = clap_mangen::Man::new(command).render(&mut roff) {
|
||||||
|
eprintln!("man: {e}");
|
||||||
|
return ExitCode::from(exit::SOFTWARE);
|
||||||
|
}
|
||||||
|
match String::from_utf8(roff) {
|
||||||
|
Ok(page) => text("man", &page),
|
||||||
|
Err(_) => ExitCode::from(exit::SOFTWARE),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What this program was called, without a path and without an extension.
|
||||||
|
fn invoked_as() -> Option<String> {
|
||||||
|
let argv0 = std::env::args_os().next()?;
|
||||||
|
let name = std::path::Path::new(&argv0).file_stem()?.to_str()?;
|
||||||
|
match name.is_empty() {
|
||||||
|
true => None,
|
||||||
|
false => Some(name.to_string()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write records to stdout as NDJSON, one per line, flushed as they go.
|
/// Write records to stdout as NDJSON, one per line, flushed as they go.
|
||||||
@@ -159,4 +217,39 @@ mod tests {
|
|||||||
format!("{:?}", ExitCode::from(exit::OK))
|
format!("{:?}", ExitCode::from(exit::OK))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A command shaped like the tools here: a required path and a flag.
|
||||||
|
#[derive(clap::Parser)]
|
||||||
|
#[command(name = "example", about = "An example.")]
|
||||||
|
struct Example {
|
||||||
|
/// The file to read.
|
||||||
|
path: std::path::PathBuf,
|
||||||
|
/// Emit a record.
|
||||||
|
#[arg(long)]
|
||||||
|
json: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn page() -> String {
|
||||||
|
let command = <Example as clap::CommandFactory>::command();
|
||||||
|
let mut roff = Vec::new();
|
||||||
|
clap_mangen::Man::new(command)
|
||||||
|
.render(&mut roff)
|
||||||
|
.expect("a rendered page");
|
||||||
|
String::from_utf8(roff).expect("text")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_manual_page_says_what_the_help_says() {
|
||||||
|
let page = page();
|
||||||
|
assert!(page.contains(".TH example 1"), "{page}");
|
||||||
|
assert!(page.contains("An example."), "{page}");
|
||||||
|
assert!(page.contains("\\-\\-json"), "{page}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_manual_page_describes_arguments_the_command_requires() {
|
||||||
|
// The page has to describe a command whose arguments are required,
|
||||||
|
// which is why it cannot be reached by parsing them first.
|
||||||
|
assert!(page().contains("PATH"), "{}", page());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-3
@@ -61,11 +61,16 @@ same as any other filter.
|
|||||||
## Nothing reaches outside the process
|
## Nothing reaches outside the process
|
||||||
|
|
||||||
No network, no database, no configuration file, no state kept between runs.
|
No network, no database, no configuration file, no state kept between runs.
|
||||||
Two exceptions, both declared in `--help`:
|
Four exceptions, each declared in `--help`:
|
||||||
|
|
||||||
- [`emb`](../crates/emb/README.md) makes an HTTP call, which is the whole of what it does.
|
- [`emb`](../crates/emb/README.md) makes an HTTP call, which is the whole of what it does.
|
||||||
- [`pdftriage`](../crates/pdftriage/README.md) runs `pdftotext` and `pdfinfo`, and exits `69`
|
- [`pdftriage`](../crates/pdftriage/README.md) runs `pdftotext` and `pdfinfo`.
|
||||||
when either is missing rather than guessing.
|
- [`pdfgutter`](../crates/pdfgutter/README.md) runs `pdftoppm` to look at a page.
|
||||||
|
- [`avplan`](../crates/avplan/README.md) runs `ffprobe` to read a file's streams, and
|
||||||
|
`ffmpeg` to ask which encoders the machine has.
|
||||||
|
|
||||||
|
The three that call another program exit `69` when it is missing, rather than
|
||||||
|
guessing at what it would have said.
|
||||||
|
|
||||||
## Nothing is assumed about where it is running
|
## Nothing is assumed about where it is running
|
||||||
|
|
||||||
@@ -87,3 +92,14 @@ Where a program prints something for a person to read, `--json` prints the
|
|||||||
same answer as a record, and that record does not change shape without a major
|
same answer as a record, and that record does not change shape without a major
|
||||||
version. Programs whose only sensible output is a stream of records write that
|
version. Programs whose only sensible output is a stream of records write that
|
||||||
stream and have no `--json` to add.
|
stream and have no `--json` to add.
|
||||||
|
|
||||||
|
## Every one writes its own manual page
|
||||||
|
|
||||||
|
`--man` writes the program's manual page, in roff, to stdout:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pdfgutter --man | gzip > /usr/local/share/man/man1/pdfgutter.1.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
It is generated from the same definition `--help` is, so the two can never
|
||||||
|
come to say different things.
|
||||||
|
|||||||
Reference in New Issue
Block a user