Skip to main content

lezeh_common/
command.rs

1use std::collections::VecDeque;
2use std::process::Stdio;
3use tokio::process::Child as ChildProcess;
4use tokio::process::Command;
5
6use crate::types::ResultAnyError;
7use crate::utils;
8use anyhow::anyhow;
9
10/// A command that has some presets such as:
11/// - Working directory
12pub struct PresetCommand {
13  pub working_dir: String,
14}
15
16impl PresetCommand {
17  pub async fn exec(&self, command_str: &str) -> ResultAnyError<String> {
18    let command_result = self
19      .spawn_command_from_str(command_str, None, None)
20      .await?
21      .wait_with_output()
22      .await?;
23
24    if !command_result.stderr.is_empty() {
25      return stderr_to_err(command_result.stderr);
26    }
27
28    return utils::bytes_to_string(command_result.stdout);
29  }
30
31  pub async fn spawn_command_from_str(
32    &self,
33    command_str: &str,
34    stdin: Option<Stdio>,
35    stdout: Option<Stdio>,
36  ) -> ResultAnyError<ChildProcess> {
37    let mut command_parts: VecDeque<String> =
38      PresetCommand::create_command_parts_from_string(command_str);
39
40    let command = command_parts
41      .pop_front()
42      .ok_or(anyhow!("Invalid command: {}", command_str))?;
43
44    let handle = Command::new(command)
45      .args(command_parts)
46      .current_dir(&self.working_dir)
47      .stdin(stdin.unwrap_or(Stdio::piped()))
48      .stdout(stdout.unwrap_or(Stdio::piped()))
49      .spawn()?;
50
51    return Ok(handle);
52  }
53}
54
55impl PresetCommand {
56  /// As of now this function does not work for param value that contains
57  /// whitespace, for example: `git log --oneline --pretty='format:%h %s'`
58  /// the `--pretty='format:%h %s` will fail.
59  fn create_command_parts_from_string(command_str: &str) -> VecDeque<String> {
60    let command_parts_raw: Vec<String> = command_str.split(' ').map(String::from).collect();
61    let mut command_parts: VecDeque<String> = Default::default();
62    let mut has_unpaired_string_quote: bool = false;
63
64    for (_, token) in command_parts_raw.iter().enumerate() {
65      if command_parts.len() > 1 && has_unpaired_string_quote {
66        let previous_token = command_parts.pop_back().unwrap();
67        let previous_token = format!("{} {}", previous_token, token);
68
69        command_parts.push_back(previous_token);
70
71        if token.contains("\"") {
72          has_unpaired_string_quote = false;
73        }
74      } else {
75        if has_unpaired_string_quote == false && token.contains("\"") {
76          has_unpaired_string_quote = true;
77        }
78
79        command_parts.push_back(token.to_owned());
80      }
81    }
82
83    return command_parts;
84  }
85}
86
87pub fn stderr_to_err(stderr: Vec<u8>) -> ResultAnyError<String> {
88  let output_err = utils::bytes_to_string(stderr)?;
89
90  return Err(anyhow!(output_err));
91}
92
93pub fn handle_command_output(output: std::process::Output) -> ResultAnyError<String> {
94  if !output.stderr.is_empty() {
95    // Convert explicitly to Err.
96    return stderr_to_err(output.stderr);
97  }
98
99  return utils::bytes_to_string(output.stdout);
100}
101
102#[cfg(test)]
103mod test {
104  use super::*;
105
106  mod create_command_parts_from_string {
107    use super::*;
108
109    // Deliberately comment it out
110    // #[test]
111    // fn it_should_parse_string_params_containing_space() {
112    //   // 1 space
113    //   let command_parts: VecDeque<String> = PresetCommand::create_command_parts_from_string(
114    //     "git log --oneline --pretty='format:%h %s'",
115    //   );
116
117    //   assert_eq!(
118    //     vec![
119    //       "git".to_owned(),
120    //       "log".to_owned(),
121    //       "--oneline".to_owned(),
122    //       "--pretty='format:%h %s'".to_owned()
123    //     ],
124    //     command_parts.into_iter().collect::<Vec<String>>()
125    //   );
126
127    //   // 2 spaces
128    //   let command_parts: VecDeque<String> =
129    //     PresetCommand::create_command_parts_from_string("grep 'Merge pull request' --invert-match");
130
131    //   assert_eq!(
132    //     vec![
133    //       "grep".to_owned(),
134    //       "'Merge pull request'".to_owned(),
135    //       "--invert-match".to_owned(),
136    //     ],
137    //     command_parts.into_iter().collect::<Vec<String>>()
138    //   );
139    // }
140
141    #[test]
142    fn it_should_parse_string_params() {
143      let command_parts: VecDeque<String> =
144        PresetCommand::create_command_parts_from_string("git log --oneline --pretty=format:%s");
145
146      assert_eq!(
147        vec![
148          "git".to_owned(),
149          "log".to_owned(),
150          "--oneline".to_owned(),
151          "--pretty=format:%s".to_owned()
152        ],
153        command_parts.into_iter().collect::<Vec<String>>()
154      );
155    }
156  }
157}