asimov_cli/commands/
help.rs

1// This is free and unencumbered software released into the public domain.
2
3use clientele::SubcommandsProvider;
4use rayon::prelude::*;
5use std::process::Stdio;
6
7use crate::Result;
8
9pub struct CommandDescription {
10    pub name: String,
11    pub description: String,
12    pub usage: Option<String>,
13}
14
15/// Prints extensive help message, executing `help` command for each subcommand.
16pub struct Help;
17
18impl Help {
19    pub fn execute(&self) -> Vec<CommandDescription> {
20        let output = self.collect_output();
21
22        let mut result = vec![];
23        for (name, description) in output {
24            let lines = description.lines().collect::<Vec<_>>();
25            let usage = lines.iter().find(|line| line.starts_with("Usage:"));
26
27            // Parse description until the end or an empty line.
28            let description = lines
29                .iter()
30                .map_while(|line| {
31                    if !line.trim().is_empty() {
32                        Some(line.to_string())
33                    } else {
34                        None
35                    }
36                })
37                .collect::<Vec<_>>()
38                .join("\n");
39
40            result.push(CommandDescription {
41                name,
42                description,
43                usage: usage.map(|usage| usage.to_string()),
44            });
45        }
46
47        result
48    }
49
50    fn is_child_running(&self, child: &mut std::process::Child) -> Result<bool, std::io::Error> {
51        Ok(child.try_wait()?.is_none())
52    }
53
54    fn collect_output(&self) -> Vec<(String, String)> {
55        const MAX_WAIT_TIME: std::time::Duration = std::time::Duration::from_secs(1);
56
57        let provider = SubcommandsProvider::collect("asimov-", 1);
58        let commands = provider.get_commands();
59
60        let start_time = std::time::Instant::now();
61
62        commands
63            .par_iter()
64            .filter_map(|cmd| {
65                let Ok(mut child) = std::process::Command::new(&cmd.path)
66                    .args(["--help"])
67                    .stdin(Stdio::inherit())
68                    .stdout(Stdio::piped())
69                    .stderr(Stdio::piped())
70                    .spawn()
71                else {
72                    return None;
73                };
74
75                loop {
76                    let Ok(is_running) = self.is_child_running(&mut child) else {
77                        return None;
78                    };
79
80                    if !is_running {
81                        break;
82                    }
83
84                    let now = std::time::Instant::now();
85                    if now.duration_since(start_time) > MAX_WAIT_TIME {
86                        child.kill().ok();
87                        drop(child);
88                        return None;
89                    }
90
91                    rayon::yield_local();
92                }
93
94                let output = child.wait_with_output().unwrap();
95                if !output.status.success() {
96                    return None;
97                }
98
99                let stdout = String::from_utf8_lossy(&output.stdout);
100                Some((cmd.name.clone(), stdout.to_string()))
101            })
102            .collect()
103    }
104}