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
10pub 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 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 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 #[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}