asimov_cli/commands/
help.rs

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