Skip to main content

ostool/
utils.rs

1//! Common utilities and helper functions.
2//!
3//! This module provides utility types and functions used throughout ostool,
4//! including command execution helpers and string processing utilities.
5
6use std::{
7    ffi::OsStr,
8    ops::{Deref, DerefMut},
9    path::Path,
10};
11
12use anyhow::Context;
13use anyhow::bail;
14use colored::Colorize;
15
16/// A command builder wrapper with variable substitution support.
17///
18/// `Command` wraps `std::process::Command` and adds support for automatic
19/// variable replacement in arguments and environment values.
20pub struct Command {
21    inner: std::process::Command,
22    value_replace: Box<dyn Fn(&OsStr) -> String>,
23}
24
25impl Deref for Command {
26    type Target = std::process::Command;
27
28    fn deref(&self) -> &Self::Target {
29        &self.inner
30    }
31}
32
33impl DerefMut for Command {
34    fn deref_mut(&mut self) -> &mut Self::Target {
35        &mut self.inner
36    }
37}
38
39impl Command {
40    /// Creates a new command builder.
41    ///
42    /// # Arguments
43    ///
44    /// * `program` - The program to execute.
45    /// * `workdir` - The working directory for the command.
46    /// * `value_replace` - Function to perform variable substitution on arguments.
47    pub fn new<S>(
48        program: S,
49        workdir: &Path,
50        value_replace: impl Fn(&OsStr) -> String + 'static,
51    ) -> Command
52    where
53        S: AsRef<OsStr>,
54    {
55        let mut cmd = std::process::Command::new(program);
56        cmd.current_dir(workdir);
57        cmd.env("WORKSPACE_FOLDER", workdir.display().to_string());
58
59        Self {
60            inner: cmd,
61            value_replace: Box::new(value_replace),
62        }
63    }
64
65    /// Prints the command to stdout with colored formatting.
66    pub fn print_cmd(&self) {
67        let program = self.get_program().to_string_lossy();
68        let mut cmd_str = program.into_owned();
69        for arg in self.get_args() {
70            cmd_str.push(' ');
71            cmd_str.push_str(arg.to_string_lossy().as_ref());
72        }
73
74        println!("{}", cmd_str.purple().bold());
75    }
76
77    pub fn into_std(self) -> std::process::Command {
78        self.inner
79    }
80
81    /// Executes the command and waits for it to complete.
82    ///
83    /// # Errors
84    ///
85    /// Returns an error if the command fails to execute or exits with non-zero status.
86    pub fn run(&mut self) -> anyhow::Result<()> {
87        self.print_cmd();
88        let status = self.status()?;
89        if !status.success() {
90            bail!("failed with status: {status}");
91        }
92        Ok(())
93    }
94
95    /// Adds an argument to the command with variable substitution.
96    pub fn arg<S>(&mut self, arg: S) -> &mut Command
97    where
98        S: AsRef<OsStr>,
99    {
100        let value = (self.value_replace)(arg.as_ref());
101        self.inner.arg(value);
102        self
103    }
104
105    /// Adds multiple arguments to the command with variable substitution.
106    pub fn args<I, S>(&mut self, args: I) -> &mut Command
107    where
108        I: IntoIterator<Item = S>,
109        S: AsRef<OsStr>,
110    {
111        for arg in args {
112            self.arg(arg.as_ref());
113        }
114        self
115    }
116
117    /// Sets an environment variable for the command with variable substitution.
118    pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Command
119    where
120        K: AsRef<OsStr>,
121        V: AsRef<OsStr>,
122    {
123        let value = (self.value_replace)(val.as_ref());
124        self.inner.env(key, value);
125        self
126    }
127}
128
129/// Adds file-system path context to fallible operations.
130pub trait PathResultExt<T> {
131    /// Attach an operation label and the relevant path while preserving the original error as source.
132    fn with_path<P>(self, action: &'static str, path: P) -> anyhow::Result<T>
133    where
134        P: AsRef<Path>;
135}
136
137impl<T, E> PathResultExt<T> for Result<T, E>
138where
139    E: std::error::Error + Send + Sync + 'static,
140{
141    fn with_path<P>(self, action: &'static str, path: P) -> anyhow::Result<T>
142    where
143        P: AsRef<Path>,
144    {
145        let path = path.as_ref().to_path_buf();
146        self.with_context(move || format!("{action}: {}", path.display()))
147    }
148}
149
150/// Replaces environment variable placeholders in a string.
151///
152/// Placeholders use the format `${env:VAR_NAME}` where `VAR_NAME` is the
153/// name of an environment variable. If the variable is not set, the
154/// placeholder is replaced with an empty string.
155///
156/// # Example
157///
158/// ```rust
159/// use ostool::utils::replace_env_placeholders;
160///
161/// unsafe { std::env::set_var("MY_VAR", "hello"); }
162/// let result = replace_env_placeholders("Value: ${env:MY_VAR}").unwrap();
163/// assert_eq!(result, "Value: hello");
164/// ```
165pub fn replace_env_placeholders(input: &str) -> anyhow::Result<String> {
166    replace_placeholders(input, |placeholder| {
167        if let Some(env_var_name) = placeholder.strip_prefix("env:") {
168            return Ok(Some(std::env::var(env_var_name).unwrap_or_default()));
169        }
170
171        Ok(None)
172    })
173}
174
175/// Replaces placeholders in a string using a caller-provided resolver.
176///
177/// Placeholders use the format `${name}`. The resolver can choose to replace
178/// a placeholder by returning `Some(value)` or keep it unchanged with `None`.
179/// This function preserves malformed or unknown placeholders as-is.
180pub fn replace_placeholders<F>(input: &str, mut resolver: F) -> anyhow::Result<String>
181where
182    F: FnMut(&str) -> anyhow::Result<Option<String>>,
183{
184    let mut result = String::new();
185    let mut chars = input.chars().peekable();
186
187    while let Some(ch) = chars.next() {
188        if ch == '$' && chars.peek() == Some(&'{') {
189            // 开始可能的占位符
190            chars.next(); // 消耗 '{'
191            let mut placeholder = String::new();
192            let mut brace_count = 1;
193            let mut found_closing_brace = false;
194
195            // 收集占位符内容
196            for ch in chars.by_ref() {
197                if ch == '{' {
198                    brace_count += 1;
199                    placeholder.push(ch);
200                } else if ch == '}' {
201                    brace_count -= 1;
202                    if brace_count == 0 {
203                        found_closing_brace = true;
204                        break;
205                    } else {
206                        placeholder.push(ch);
207                    }
208                } else {
209                    placeholder.push(ch);
210                }
211            }
212
213            if found_closing_brace {
214                if let Some(value) = resolver(&placeholder)? {
215                    result.push_str(&value);
216                } else {
217                    result.push_str("${");
218                    result.push_str(&placeholder);
219                    result.push('}');
220                }
221            } else {
222                result.push_str("${");
223                result.push_str(&placeholder);
224            }
225        } else {
226            result.push(ch);
227        }
228    }
229
230    Ok(result)
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use std::env;
237
238    #[test]
239    fn test_replace_placeholders_supports_custom_variables() {
240        unsafe {
241            env::set_var("OSTOOL_TEST_CUSTOM_ENV", "env-value");
242        }
243
244        let result = replace_placeholders(
245            "workspace=${workspace}, package=${package}, env=${env:OSTOOL_TEST_CUSTOM_ENV}",
246            |placeholder| {
247                Ok(match placeholder {
248                    "workspace" => Some("/tmp/workspace".into()),
249                    "package" => Some("/tmp/workspace/kernel".into()),
250                    p if p.starts_with("env:") => Some(env::var(&p[4..]).unwrap_or_default()),
251                    _ => None,
252                })
253            },
254        )
255        .unwrap();
256
257        assert_eq!(
258            result,
259            "workspace=/tmp/workspace, package=/tmp/workspace/kernel, env=env-value"
260        );
261    }
262
263    #[test]
264    fn test_replace_env_placeholders() {
265        // 设置测试环境变量
266        unsafe {
267            env::set_var("OSTOOL_TEST_HOME_REPLACE", "/home/test");
268            env::set_var("OSTOOL_TEST_PATH_REPLACE", "/usr/local/bin");
269        }
270
271        // 测试简单的环境变量替换
272        assert_eq!(
273            replace_env_placeholders("${env:OSTOOL_TEST_HOME_REPLACE}").unwrap(),
274            "/home/test"
275        );
276
277        // 测试多个环境变量
278        assert_eq!(
279            replace_env_placeholders(
280                "${env:OSTOOL_TEST_HOME_REPLACE}:${env:OSTOOL_TEST_PATH_REPLACE}"
281            )
282            .unwrap(),
283            "/home/test:/usr/local/bin"
284        );
285
286        // 测试不存在的环境变量 - 应该返回空字符串而不是错误
287        let result = replace_env_placeholders("${env:NON_EXISTENT}");
288        assert!(result.is_ok());
289        assert_eq!(result.unwrap(), "");
290
291        // 测试混合内容
292        assert_eq!(
293            replace_env_placeholders("Path: ${env:OSTOOL_TEST_HOME_REPLACE}/bin").unwrap(),
294            "Path: /home/test/bin"
295        );
296
297        // 测试非环境变量占位符
298        assert_eq!(
299            replace_env_placeholders("${not_env:placeholder}").unwrap(),
300            "${not_env:placeholder}"
301        );
302
303        // 测试无占位符的字符串
304        assert_eq!(
305            replace_env_placeholders("Just a normal string").unwrap(),
306            "Just a normal string"
307        );
308
309        // 测试空字符串
310        assert_eq!(replace_env_placeholders("").unwrap(), "");
311    }
312
313    #[test]
314    fn test_nested_braces() {
315        unsafe {
316            env::set_var("OSTOOL_TEST_VAR_NESTED", "value");
317        }
318
319        // 测试嵌套大括号的情况
320        assert_eq!(
321            replace_env_placeholders("${env:OSTOOL_TEST_VAR_NESTED} and ${other:placeholder}")
322                .unwrap(),
323            "value and ${other:placeholder}"
324        );
325    }
326
327    #[test]
328    fn test_replace_placeholders_keeps_unknown_and_legacy_placeholders() {
329        let result = replace_placeholders(
330            "${workspaceFolder}:${unknown}:${workspace}",
331            |placeholder| {
332                Ok(match placeholder {
333                    "workspaceFolder" => Some("/legacy".into()),
334                    "workspace" => Some("/modern".into()),
335                    _ => None,
336                })
337            },
338        )
339        .unwrap();
340
341        assert_eq!(result, "/legacy:${unknown}:/modern");
342    }
343
344    #[test]
345    fn test_real_env_vars() {
346        // 测试真实的环境变量(如果存在)
347        if let Ok(home) = env::var("HOME") {
348            assert_eq!(replace_env_placeholders("${env:HOME}").unwrap(), home);
349        }
350    }
351
352    #[test]
353    fn test_edge_cases() {
354        // 测试不完整的占位符
355        assert_eq!(replace_env_placeholders("${").unwrap(), "${");
356        assert_eq!(replace_env_placeholders("${env").unwrap(), "${env");
357        assert_eq!(replace_env_placeholders("${env:").unwrap(), "${env:");
358        assert_eq!(replace_env_placeholders("${env:VAR").unwrap(), "${env:VAR");
359
360        // 测试空的env变量名 - 应该返回空字符串而不是错误
361        let result = replace_env_placeholders("${env:}");
362        assert!(result.is_ok());
363        assert_eq!(result.unwrap(), "");
364
365        // 测试只包含$的字符串
366        assert_eq!(replace_env_placeholders("$").unwrap(), "$");
367        assert_eq!(replace_env_placeholders("$$").unwrap(), "$$");
368
369        // 测试包含特殊字符的环境变量名
370        unsafe {
371            env::set_var("OSTOOL-TEST-VAR-EDGE", "dash-value");
372            env::set_var("OSTOOL_TEST_VAR_EDGE", "underscore-value");
373        }
374        assert_eq!(
375            replace_env_placeholders("${env:OSTOOL-TEST-VAR-EDGE}").unwrap(),
376            "dash-value"
377        );
378        assert_eq!(
379            replace_env_placeholders("${env:OSTOOL_TEST_VAR_EDGE}").unwrap(),
380            "underscore-value"
381        );
382
383        // 测试空的环境变量值
384        unsafe {
385            env::set_var("OSTOOL_EMPTY_VAR_EDGE", "");
386        }
387        assert_eq!(
388            replace_env_placeholders("${env:OSTOOL_EMPTY_VAR_EDGE}").unwrap(),
389            ""
390        );
391    }
392
393    #[test]
394    fn test_malformed_placeholders() {
395        // 测试格式错误的占位符
396        assert_eq!(replace_env_placeholders("${env:VAR").unwrap(), "${env:VAR");
397        assert_eq!(replace_env_placeholders("${env}").unwrap(), "${env}");
398        assert_eq!(replace_env_placeholders("${:VAR}").unwrap(), "${:VAR}");
399
400        // 设置测试环境变量
401        unsafe {
402            env::set_var("OSTOOL_VAR_MALFORMED", "value");
403        }
404
405        // 测试混合的大括号
406        // 当遇到完整的占位符后停止,剩余字符由主循环继续处理
407        assert_eq!(
408            replace_env_placeholders("${env:OSTOOL_VAR_MALFORMED}}").unwrap(),
409            "value}"
410        );
411
412        // 测试其他格式错误的情况
413        assert_eq!(replace_env_placeholders("{env:VAR}").unwrap(), "{env:VAR}");
414        assert_eq!(replace_env_placeholders("$env:VAR}").unwrap(), "$env:VAR}");
415    }
416}