1use std::{
7 ops::{Deref, DerefMut},
8 process::Stdio,
9};
10
11#[cfg(feature = "async-std")]
12use async_std::{io::WriteExt, process::Command as AsyncCommand};
13#[cfg(feature = "tokio")]
14use tokio::{io::AsyncWriteExt, process::Command as AsyncCommand};
15use tracing::{debug, info};
16
17use crate::{Error, Output, Result};
18
19#[derive(Clone, Debug, Eq, PartialEq)]
23#[cfg_attr(
24 feature = "derive",
25 derive(serde::Serialize, serde::Deserialize),
26 serde(from = "String", into = "String")
27)]
28pub struct Command {
29 inner: String,
31
32 #[cfg_attr(feature = "derive", serde(skip))]
36 piped: bool,
37}
38
39impl Command {
40 pub fn new(cmd: impl ToString) -> Self {
45 Self {
46 inner: cmd.to_string(),
47 piped: true,
48 }
49 }
50
51 pub fn set_output_piped(&mut self, piped: bool) {
56 self.piped = piped;
57 }
58
59 pub fn with_output_piped(mut self, piped: bool) -> Self {
64 self.set_output_piped(piped);
65 self
66 }
67
68 pub fn replace(mut self, from: impl AsRef<str>, to: impl AsRef<str>) -> Self {
73 self.inner = self.inner.replace(from.as_ref(), to.as_ref());
74 self
75 }
76
77 pub async fn run(&self) -> Result<Output> {
81 self.run_with([]).await
82 }
83
84 pub async fn run_with(&self, input: impl AsRef<[u8]>) -> Result<Output> {
91 info!(cmd = self.inner, "run shell command");
92
93 let input = input.as_ref();
94
95 let stdin = if input.is_empty() {
96 debug!("inherit stdin from parent");
97 Stdio::inherit()
98 } else {
99 debug!("stdin piped");
100 Stdio::piped()
101 };
102
103 let mut cmd = new_async_command()
104 .arg(&self.inner)
105 .stdin(stdin)
106 .stdout(if self.piped {
107 debug!("stdout piped");
108 Stdio::piped()
109 } else {
110 debug!("inherit stdout from parent");
111 Stdio::inherit()
112 })
113 .stderr(if self.piped {
114 debug!("stderr piped");
115 Stdio::piped()
116 } else {
117 debug!("inherit stderr from parent");
118 Stdio::inherit()
119 })
120 .spawn()?;
121
122 if !input.is_empty() {
123 cmd.stdin
124 .as_mut()
125 .ok_or(Error::GetStdinError)?
126 .write_all(input)
127 .await?;
128 }
129
130 #[cfg(feature = "async-std")]
131 let output = cmd.output().await?;
132 #[cfg(feature = "tokio")]
133 let output = cmd.wait_with_output().await?;
134
135 let code = output
136 .status
137 .code()
138 .ok_or_else(|| Error::GetExitStatusCodeNotAvailableError(self.to_string()))?;
139
140 if code == 0 {
141 debug!(code, "shell command gracefully exited");
142 } else {
143 let cmd = self.to_string();
144 let err = String::from_utf8_lossy(&output.stderr).to_string();
145 debug!(code, err, "shell command ungracefully exited");
146 return Err(Error::GetExitStatusCodeNonZeroError(cmd, code, err));
147 }
148
149 Ok(Output::from(output.stdout))
150 }
151}
152
153impl Deref for Command {
154 type Target = String;
155
156 fn deref(&self) -> &Self::Target {
157 &self.inner
158 }
159}
160
161impl DerefMut for Command {
162 fn deref_mut(&mut self) -> &mut Self::Target {
163 &mut self.inner
164 }
165}
166
167impl From<String> for Command {
168 fn from(cmd: String) -> Self {
169 Self::new(cmd)
170 }
171}
172
173impl From<Command> for String {
174 fn from(cmd: Command) -> Self {
175 cmd.inner
176 }
177}
178
179impl ToString for Command {
180 fn to_string(&self) -> String {
181 self.inner.clone()
182 }
183}
184
185fn new_async_command() -> AsyncCommand {
187 #[cfg(not(windows))]
188 let windows = false;
189 #[cfg(windows)]
190 let windows = !std::env::var("MSYSTEM")
191 .map(|env| env.starts_with("MINGW"))
192 .unwrap_or_default();
193
194 let (shell, arg) = if windows { ("cmd", "/C") } else { ("sh", "-c") };
195
196 let mut cmd = AsyncCommand::new(shell);
197 cmd.arg(arg);
198 cmd
199}