asimov_cli/commands/
help.rs

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