dev_prune/commands/
man.rs1use std::fs;
12use std::io::{IsTerminal, Write};
13use std::path::Path;
14
15use anyhow::{Context, Result};
16use clap::CommandFactory;
17use clap_mangen::Man;
18use colored::Colorize;
19
20use crate::Cli;
21use crate::help;
22use crate::output;
23
24pub fn run(command_name: Option<&str>, dir: Option<&str>, roff: bool) -> Result<()> {
29 let mut command = Cli::command().name("devp");
30 command.build();
31
32 if let Some(name) = command_name {
36 let Some(sub) = command
37 .get_subcommands()
38 .find(|s| s.get_name() == name || s.get_all_aliases().any(|a| a == name))
39 .cloned()
40 else {
41 let names: Vec<&str> = command
42 .get_subcommands()
43 .map(|s| s.get_name())
44 .filter(|n| *n != "help")
45 .collect();
46 anyhow::bail!(
47 "no such command: `{name}`. Try one of: {}",
48 names.join(", ")
49 );
50 };
51 if roff || !std::io::stdout().is_terminal() {
52 let page = format!("devp-{name}");
53 let mut out = Vec::new();
54 Man::new(sub.name(page.leak() as &str)).render(&mut out)?;
55 std::io::stdout().write_all(&out)?;
56 return Ok(());
57 }
58 let mut sub = sub;
59 sub.print_long_help()?;
60 return Ok(());
61 }
62
63 let Some(dir) = dir else {
64 if roff || !std::io::stdout().is_terminal() {
70 let mut out = Vec::new();
71 Man::new(command).render(&mut out)?;
72 std::io::stdout().write_all(&out)?;
73 return Ok(());
74 }
75
76 print_contents();
82 return Ok(());
83 };
84
85 let dir = Path::new(dir);
86 fs::create_dir_all(dir)
87 .with_context(|| format!("could not create {}", output::clean_path(dir)))?;
88
89 let mut written = 0usize;
90 let mut render_to = |name: &str, man: Man| -> Result<()> {
91 let path = dir.join(format!("{name}.1"));
92 let mut buf = Vec::new();
93 man.render(&mut buf)?;
94 fs::write(&path, buf)
95 .with_context(|| format!("could not write {}", output::clean_path(&path)))?;
96 written += 1;
97 Ok(())
98 };
99
100 for sub in command.get_subcommands() {
101 if sub.get_name() == "help" {
103 continue;
104 }
105 let name = format!("devp-{}", sub.get_name());
106 render_to(
109 &name,
110 Man::new(sub.clone().name(name.clone().leak() as &str)),
111 )?;
112 }
113 render_to("devp", Man::new(command.clone()))?;
114 render_to("dev-prune", Man::new(command.clone().name("dev-prune")))?;
117
118 output::print_success(&format!(
119 "{written} man pages written to {}",
120 output::clean_path(dir)
121 ));
122 output::print_info(
123 "Install them by copying into a directory on `manpath`, e.g. `/usr/local/share/man/man1/`.",
124 );
125 Ok(())
126}
127
128fn print_contents() {
136 output::print_header("dev-prune manual");
137 println!();
138 output::print_wrapped(
139 " ",
140 "Every page below is generated from the definitions the binary parses arguments \
141 with, so the manual cannot describe a flag the program does not have.",
142 );
143 println!();
144 println!(" {}", "Read one page:".bold());
145 println!(" devp man <command> e.g. `devp man run`, `devp man config`");
146 println!(" devp <command> --help the same text, from the command itself");
147 println!();
148
149 for (title, entries) in help::COMMAND_GROUPS {
150 println!(" {}", title.bold());
151 for (name, line) in entries {
152 println!(" {:<12} {line}", name.cyan());
153 }
154 println!();
155 }
156
157 println!(" {}", "Flags that go before the command".bold());
160 println!(" --dry-run simulate, delete nothing");
161 println!(" --ignore-idle prune repositories you are still working in");
162 println!(" --yes / -y answer yes to confirmations");
163 println!();
164 println!(" {}", "Exit codes".bold());
165 println!(" 0 success 1 failure 2 usage error");
166 println!();
167 output::print_info(
168 "`devp man --roff` prints the roff source; `devp man --dir <DIR>` writes the full set of pages.",
169 );
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn the_full_set_covers_every_subcommand() {
178 let tmp = tempfile::tempdir().unwrap();
179 run(None, Some(tmp.path().to_str().unwrap()), false).unwrap();
180
181 let mut command = Cli::command();
183 command.build();
184 for sub in command.get_subcommands() {
185 if sub.get_name() == "help" {
186 continue;
187 }
188 let page = tmp.path().join(format!("devp-{}.1", sub.get_name()));
189 assert!(page.exists(), "missing {}", page.display());
190 }
191 assert!(tmp.path().join("devp.1").exists());
192 assert!(tmp.path().join("dev-prune.1").exists());
193 }
194
195 #[test]
196 fn a_page_carries_the_long_about_text() {
197 let tmp = tempfile::tempdir().unwrap();
198 run(None, Some(tmp.path().to_str().unwrap()), false).unwrap();
199 let run_page = fs::read_to_string(tmp.path().join("devp-run.1")).unwrap();
200 assert!(run_page.contains("gauntlet"), "{run_page}");
202 }
203
204 #[test]
205 fn the_contents_page_names_every_command_and_no_others() {
206 let mut command = Cli::command();
211 command.build();
212 let real: Vec<&str> = command
213 .get_subcommands()
214 .map(|s| s.get_name())
215 .filter(|n| *n != "help")
216 .collect();
217 let listed: Vec<&str> = help::COMMAND_GROUPS
218 .iter()
219 .flat_map(|(_, e)| e.iter().map(|(n, _)| *n))
220 .collect();
221
222 for name in &real {
223 assert!(listed.contains(name), "`{name}` is in no manual group");
224 }
225 for name in &listed {
226 assert!(
227 real.contains(name),
228 "manual lists `{name}`, which is not a command"
229 );
230 }
231 }
232
233 #[test]
234 fn a_named_command_renders_its_own_page() {
235 let mut command = Cli::command().name("devp");
238 command.build();
239 let sub = command
240 .get_subcommands()
241 .find(|s| s.get_name() == "run")
242 .cloned()
243 .unwrap();
244 let mut out = Vec::new();
245 Man::new(sub.name("devp-run")).render(&mut out).unwrap();
246 let page = String::from_utf8(out).unwrap();
247 assert!(page.contains("devp"), "{page}");
248 assert!(page.contains("gauntlet"), "{page}");
249 }
250
251 #[test]
252 fn an_unknown_command_lists_the_real_ones() {
253 let err = run(Some("nosuchthing"), None, false)
254 .unwrap_err()
255 .to_string();
256 assert!(err.contains("no such command"), "{err}");
257 assert!(err.contains("run"), "{err}");
258 }
259}