pub mod dispatch;
pub mod docs;
pub mod doctor;
pub mod family;
pub mod output;
pub mod version;
pub use clap;
pub use family::{Family, Tool};
pub use output::{CliError, Format, Json, Outcome};
use std::ffi::OsString;
use std::io::Write as _;
use std::process::ExitCode;
use clap::{Arg, ArgAction, Command as Cmd};
pub fn main(family: &'static Family) -> ExitCode {
run(family, std::env::args_os().collect())
}
pub fn run(family: &'static Family, argv: Vec<OsString>) -> ExitCode {
respond(family, argv).emit()
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Response {
pub stdout: String,
pub stderr: String,
pub code: u8,
}
impl Response {
fn out(text: String, code: u8) -> Response {
Response {
stdout: text,
stderr: String::new(),
code,
}
}
fn err(text: String, code: u8) -> Response {
Response {
stdout: String::new(),
stderr: text,
code,
}
}
pub fn emit(self) -> ExitCode {
if !self.stdout.is_empty() {
let mut stdout = std::io::stdout().lock();
let _ = stdout.write_all(self.stdout.as_bytes());
let _ = stdout.flush();
}
if !self.stderr.is_empty() {
let mut stderr = std::io::stderr().lock();
let _ = stderr.write_all(self.stderr.as_bytes());
let _ = stderr.flush();
}
ExitCode::from(self.code)
}
}
pub fn respond(family: &'static Family, argv: Vec<OsString>) -> Response {
match dispatch::resolve(family, argv) {
dispatch::Target::Tool(tool, argv) => respond_tool(family, tool, argv),
dispatch::Target::Repo(argv) => respond_repo(family, argv),
}
}
pub fn tool_command(family: &'static Family, tool: &Tool) -> Cmd {
(tool.command)()
.name(tool.name)
.bin_name(tool.name)
.version(version::clap_version(family))
}
pub fn repo_command(family: &'static Family) -> Cmd {
let mut cmd = Cmd::new(family.repo)
.bin_name(family.repo)
.version(version::clap_version(family))
.about(family.about)
.subcommand_required(true)
.arg_required_else_help(true)
.after_help(repo_examples(family));
for tool in family.tools {
cmd = cmd.subcommand((tool.command)().name(tool.verb).about(format!(
"{} (the same program as `{}`)",
tool.about, tool.name
)));
}
cmd.subcommand(doctor::command(family)).subcommand(
Cmd::new("generate")
.about("Print what packaging needs from the binary itself")
.hide(true)
.subcommand_required(true)
.subcommand(
Cmd::new("names").about("The dotted names to link to this binary, one per line"),
)
.subcommand(
Cmd::new("man")
.about("Write a man page per name under SHARE/man/man<section>/")
.arg(share_arg()),
)
.subcommand(
Cmd::new("completions")
.about("Write zsh, bash and fish completions per name under SHARE/")
.arg(share_arg()),
),
)
}
fn share_arg() -> Arg {
Arg::new("share")
.value_name("SHARE")
.value_parser(clap::value_parser!(OsString))
.required(true)
}
fn repo_examples(family: &Family) -> String {
let mut rows: Vec<(String, String)> = family
.tools
.iter()
.map(|tool| {
(
format!("{} {} --help", family.repo, tool.verb),
format!("same as `{} --help`", tool.name),
)
})
.collect();
rows.push((
format!("{} doctor", family.repo),
"is every tool on PATH ours?".to_string(),
));
let width = rows.iter().map(|(c, _)| c.len()).max().unwrap_or(0);
let mut text = String::from("Examples:\n");
for (cmd, what) in rows {
text.push_str(&format!(" {cmd:<width$} {what}\n"));
}
text
}
fn respond_tool(family: &'static Family, tool: &'static Tool, argv: Vec<OsString>) -> Response {
let text_requested = output::text_requested(&argv);
let argv_copy = argv.clone();
match tool_command(family, tool).try_get_matches_from(argv) {
Err(error) => clap_failure(tool.name, error, text_requested, tool.usage_exit),
Ok(matches) => {
let format = Format::of(&matches, &argv_copy);
let result = (tool.run)(&matches);
output::render(tool.name, format, result)
}
}
}
fn respond_repo(family: &'static Family, argv: Vec<OsString>) -> Response {
let text_requested = output::text_requested(&argv);
let argv_copy = argv.clone();
let matches = match repo_command(family).try_get_matches_from(argv) {
Ok(matches) => matches,
Err(error) => return clap_failure(family.repo, error, text_requested, output::EXIT_USAGE),
};
match matches.subcommand() {
Some(("doctor", sub)) => {
let format = Format::of(sub, &argv_copy);
output::render(family.repo, format, Ok(doctor::run(family)))
}
Some(("generate", sub)) => match sub.subcommand() {
Some(("names", _)) => {
let names: String = family
.tools
.iter()
.map(|tool| format!("{}\n", tool.name))
.collect();
Response::out(names, 0)
}
Some((what @ ("man" | "completions"), args)) => {
let share = std::path::Path::new(
args.get_one::<OsString>("share")
.expect("clap requires the share directory"),
);
let written = if what == "man" {
docs::man_pages(family, share)
} else {
docs::completions(family, share)
};
let result = written
.map(|paths| {
Outcome::report(Json::Arr(
paths
.iter()
.map(|p| Json::from(p.display().to_string()))
.collect(),
))
})
.map_err(|e| CliError::failed(format!("generate {what}: {e}")));
output::render(family.repo, Format::Json, result)
}
_ => unreachable!("clap requires a generate subcommand"),
},
_ => unreachable!("clap requires a known subcommand"),
}
}
fn clap_failure(
program: &str,
error: clap::Error,
text_requested: bool,
usage_exit: u8,
) -> Response {
use clap::error::ErrorKind;
let rendered = error.render().to_string();
match error.kind() {
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => Response::out(rendered, 0),
ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand => Response::err(rendered, usage_exit),
_ if text_requested => Response::err(rendered, usage_exit),
_ => {
let message = rendered
.trim()
.strip_prefix("error: ")
.unwrap_or(rendered.trim())
.to_string();
output::render(
program,
Format::Json,
Err(CliError::usage(message).with_code(usage_exit)),
)
}
}
}
pub fn format_args() -> [Arg; 2] {
[
Arg::new("json")
.long("json")
.help("Report as JSON on stdout (the default)")
.action(ArgAction::SetTrue)
.overrides_with("text"),
Arg::new("text")
.long("text")
.help("Report as text for a person, instead of JSON")
.action(ArgAction::SetTrue)
.overrides_with("json"),
]
}