command_stream/zx/
util.rs1use std::path::{Path, PathBuf};
5use std::time::Duration;
6
7use super::error::ZxError;
8use super::output::ProcessOutput;
9pub use crate::local_bin::{path_key, prefer_local_bin, PATH_DELIMITER};
10
11pub type QuoteFn = fn(&str) -> String;
13
14pub fn is_safe_word(arg: &str) -> bool {
17 !arg.is_empty()
18 && arg.chars().all(|c| {
19 c.is_ascii_alphanumeric()
20 || matches!(c, '_' | '/' | '.' | '-' | '+' | '@' | ':' | '=' | ',' | '%')
21 })
22}
23
24pub fn quote(arg: &str) -> String {
31 if arg.is_empty() {
32 return "$''".to_string();
33 }
34 if is_safe_word(arg) {
35 return arg.to_string();
36 }
37 let mut out = String::with_capacity(arg.len() + 3);
38 out.push_str("$'");
39 for c in arg.chars() {
40 match c {
41 '\\' => out.push_str("\\\\"),
42 '\'' => out.push_str("\\'"),
43 '\u{0C}' => out.push_str("\\f"),
44 '\n' => out.push_str("\\n"),
45 '\r' => out.push_str("\\r"),
46 '\t' => out.push_str("\\t"),
47 '\u{0B}' => out.push_str("\\v"),
48 '\0' => out.push_str("\\0"),
49 other => out.push(other),
50 }
51 }
52 out.push('\'');
53 out
54}
55
56pub fn quote_powershell(arg: &str) -> String {
59 if arg.is_empty() {
60 return "''".to_string();
61 }
62 if is_safe_word(arg) {
63 return arg.to_string();
64 }
65 format!("'{}'", arg.replace('\'', "''"))
66}
67
68pub fn duration_from_millis(ms: f64) -> Result<Duration, ZxError> {
70 if ms.is_nan() || ms < 0.0 || ms.is_infinite() {
71 return Err(ZxError::new(format!("Invalid duration: \"{ms}\".")));
72 }
73 Ok(Duration::from_secs_f64(ms / 1000.0))
74}
75
76pub fn parse_duration(input: &str) -> Result<Duration, ZxError> {
78 let digits_end = input
79 .find(|c: char| !c.is_ascii_digit())
80 .unwrap_or(input.len());
81 let (digits, unit) = input.split_at(digits_end);
82 let factor = match unit {
83 "" | "ms" => 1u64,
84 "s" => 1_000,
85 "m" => 60_000,
86 _ => return Err(ZxError::new(format!("Unknown duration: \"{input}\"."))),
87 };
88 if digits.is_empty() {
89 return Err(ZxError::new(format!("Unknown duration: \"{input}\".")));
90 }
91 let value: u64 = digits
92 .parse()
93 .map_err(|_| ZxError::new(format!("Invalid duration: \"{input}\".")))?;
94 Ok(Duration::from_millis(value.saturating_mul(factor)))
95}
96
97pub fn to_camel_case(input: &str) -> String {
102 let chars: Vec<char> = input.to_lowercase().chars().collect();
103 let mut out = String::with_capacity(chars.len());
104 let mut i = 0;
105 while i < chars.len() {
106 let c = chars[i];
107 if c.is_ascii_lowercase() {
108 let mut j = i + 1;
109 while j < chars.len() && matches!(chars[j], '_' | '-') {
110 j += 1;
111 }
112 if j > i + 1 && j < chars.len() && chars[j].is_ascii_lowercase() {
113 out.push(c);
114 out.push(chars[j].to_ascii_uppercase());
115 i = j + 1;
116 continue;
117 }
118 }
119 out.push(c);
120 i += 1;
121 }
122 out
123}
124
125pub fn parse_bool(value: &str) -> Option<bool> {
127 match value {
128 "true" => Some(true),
129 "false" => Some(false),
130 _ => None,
131 }
132}
133
134pub fn random_id() -> String {
136 use std::collections::hash_map::RandomState;
137 use std::hash::{BuildHasher, Hasher};
138 use std::sync::atomic::{AtomicU64, Ordering};
139 static COUNTER: AtomicU64 = AtomicU64::new(0);
140 let mut hasher = RandomState::new().build_hasher();
141 hasher.write_u64(COUNTER.fetch_add(1, Ordering::Relaxed));
142 hasher.write_u128(
143 std::time::SystemTime::now()
144 .duration_since(std::time::UNIX_EPOCH)
145 .map(|d| d.as_nanos())
146 .unwrap_or_default(),
147 );
148 let mut n = hasher.finish();
149 let alphabet = b"0123456789abcdefghijklmnopqrstuvwxyz";
150 let mut out = String::new();
151 for _ in 0..10 {
152 out.push(alphabet[(n % 36) as usize] as char);
153 n /= 36;
154 }
155 out
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
160pub enum ZxArg {
161 One(String),
163 Many(Vec<String>),
165}
166
167impl ZxArg {
168 pub fn render(&self, quote_fn: QuoteFn) -> String {
170 match self {
171 ZxArg::One(s) => quote_fn(s),
172 ZxArg::Many(items) => items
173 .iter()
174 .map(|s| quote_fn(s))
175 .collect::<Vec<_>>()
176 .join(" "),
177 }
178 }
179
180 fn into_words(self) -> Vec<String> {
181 match self {
182 ZxArg::One(s) => vec![s],
183 ZxArg::Many(v) => v,
184 }
185 }
186}
187
188pub trait IntoZxArg {
194 fn into_zx_arg(self) -> ZxArg;
196}
197
198impl IntoZxArg for ZxArg {
199 fn into_zx_arg(self) -> ZxArg {
200 self
201 }
202}
203
204macro_rules! display_args {
205 ($($t:ty),* $(,)?) => {
206 $(
207 impl IntoZxArg for $t {
208 fn into_zx_arg(self) -> ZxArg {
209 ZxArg::One(self.to_string())
210 }
211 }
212 impl IntoZxArg for &$t {
213 fn into_zx_arg(self) -> ZxArg {
214 ZxArg::One(self.to_string())
215 }
216 }
217 )*
218 };
219}
220
221display_args!(
222 String, char, bool, i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64
223);
224
225impl IntoZxArg for &str {
226 fn into_zx_arg(self) -> ZxArg {
227 ZxArg::One(self.to_string())
228 }
229}
230
231impl IntoZxArg for &&str {
232 fn into_zx_arg(self) -> ZxArg {
233 ZxArg::One((*self).to_string())
234 }
235}
236
237impl IntoZxArg for &Path {
238 fn into_zx_arg(self) -> ZxArg {
239 ZxArg::One(self.to_string_lossy().into_owned())
240 }
241}
242
243impl IntoZxArg for PathBuf {
244 fn into_zx_arg(self) -> ZxArg {
245 ZxArg::One(self.to_string_lossy().into_owned())
246 }
247}
248
249impl IntoZxArg for &PathBuf {
250 fn into_zx_arg(self) -> ZxArg {
251 ZxArg::One(self.to_string_lossy().into_owned())
252 }
253}
254
255impl IntoZxArg for &ProcessOutput {
256 fn into_zx_arg(self) -> ZxArg {
257 let out = self.stdout.strip_suffix('\n').unwrap_or(&self.stdout);
258 ZxArg::One(out.to_string())
259 }
260}
261
262impl IntoZxArg for ProcessOutput {
263 fn into_zx_arg(self) -> ZxArg {
264 (&self).into_zx_arg()
265 }
266}
267
268impl<T: IntoZxArg> IntoZxArg for Vec<T> {
269 fn into_zx_arg(self) -> ZxArg {
270 ZxArg::Many(
271 self.into_iter()
272 .flat_map(|item| item.into_zx_arg().into_words())
273 .collect(),
274 )
275 }
276}
277
278impl<T: IntoZxArg + Clone> IntoZxArg for &Vec<T> {
279 fn into_zx_arg(self) -> ZxArg {
280 self.as_slice().into_zx_arg()
281 }
282}
283
284impl<T: IntoZxArg + Clone> IntoZxArg for &[T] {
285 fn into_zx_arg(self) -> ZxArg {
286 self.to_vec().into_zx_arg()
287 }
288}
289
290impl<T: IntoZxArg, const N: usize> IntoZxArg for [T; N] {
291 fn into_zx_arg(self) -> ZxArg {
292 Vec::from(self).into_zx_arg()
293 }
294}
295
296pub fn zx_arg<T: IntoZxArg>(value: T) -> ZxArg {
298 value.into_zx_arg()
299}
300
301pub fn build_cmd(quote_fn: QuoteFn, pieces: &[&str], args: &[ZxArg]) -> Result<String, ZxError> {
305 if pieces.len() != args.len() + 1 {
306 return Err(ZxError::new(format!(
307 "Malformed command: {} template pieces for {} arguments",
308 pieces.len(),
309 args.len()
310 )));
311 }
312 let mut cmd = pieces[0].to_string();
313 for (arg, piece) in args.iter().zip(&pieces[1..]) {
314 cmd.push_str(&arg.render(quote_fn));
315 cmd.push_str(piece);
316 }
317 Ok(cmd)
318}
319
320pub fn split_template(template: &str) -> Vec<String> {
322 let mut pieces = vec![String::new()];
323 let mut rest = template;
324 while !rest.is_empty() {
325 if let Some(tail) = rest.strip_prefix("{{}}") {
326 pieces.last_mut().unwrap().push_str("{}");
327 rest = tail;
328 } else if let Some(tail) = rest.strip_prefix("{}") {
329 pieces.push(String::new());
330 rest = tail;
331 } else {
332 let c = rest.chars().next().unwrap();
333 pieces.last_mut().unwrap().push(c);
334 rest = &rest[c.len_utf8()..];
335 }
336 }
337 pieces
338}