Skip to main content

framework_system/
command.rs

1//! `std::process::Command` 的扩展。
2//!
3//! 方法统一使用 `command_` 前缀。执行失败的错误信息带上命令行、退出码与完整输出,
4//! 便于直接展示给用户;探测类方法把「无法执行」与「未成功退出」统一按失败处理。
5
6use std::process::{Child, Command, ExitStatus, Output};
7
8use anyhow::{Context, Result, anyhow};
9
10/// `std::process::Command` 的扩展。
11///
12/// [`CommandExt::command_run`]、[`CommandExt::command_output`]、[`CommandExt::command_status`]
13/// 与 [`CommandExt::command_spawn`] 只在命令无法执行时报错,退出码由调用方判断;
14/// [`CommandExt::command_success`]、[`CommandExt::command_stdout`] 与 [`CommandExt::command_stderr`]
15/// 则把「无法执行」与「未成功退出」统一按失败处理,用于探测类调用。
16pub trait CommandExt {
17    /// 执行命令并返回是否成功;命令无法执行或退出码非零时为假,不向上报错。
18    fn command_success(&mut self) -> bool;
19
20    /// 执行命令并检查退出码;失败时把命令行、退出码与完整输出一起报错。
21    fn command_run(&mut self) -> Result<()>;
22
23    /// 执行命令并返回完整输出;仅命令无法执行时报错。
24    fn command_output(&mut self) -> Result<Output>;
25
26    /// 执行命令并返回退出状态;仅命令无法执行时报错。
27    fn command_status(&mut self) -> Result<ExitStatus>;
28
29    /// 执行命令并返回标准输出;命令无法执行或退出码非零时返回 `None`。
30    ///
31    /// 用于状态探测:探测失败与「探测结果为否」由调用方统一按否处理,不作为错误向上传播。
32    fn command_stdout(&mut self) -> Option<String>;
33
34    /// 执行命令并返回标准错误;命令无法执行或退出码非零时返回 `None`。
35    fn command_stderr(&mut self) -> Option<String>;
36
37    /// 启动命令并立即返回子进程句柄,不等待其结束。
38    fn command_spawn(&mut self) -> Result<Child>;
39
40    /// 命令行文本:程序与参数以空格连接,用于展示与错误信息。
41    fn command_line(&self) -> String;
42}
43
44impl CommandExt for Command {
45    fn command_success(&mut self) -> bool {
46        self.output().is_ok_and(|output| output.status.success())
47    }
48
49    fn command_run(&mut self) -> Result<()> {
50        let output = self.command_output()?;
51        if output.status.success() {
52            return Ok(());
53        }
54
55        Err(failure(self, &output))
56    }
57
58    fn command_output(&mut self) -> Result<Output> {
59        let program = program(self);
60
61        self.output()
62            .with_context(|| format!("执行 {program} 失败,请确认该命令存在且可用"))
63    }
64
65    fn command_status(&mut self) -> Result<ExitStatus> {
66        let program = program(self);
67
68        self.status()
69            .with_context(|| format!("执行 {program} 失败,请确认该命令存在且可用"))
70    }
71
72    fn command_stdout(&mut self) -> Option<String> {
73        Some(text(&self.command_output().ok()?.stdout))
74    }
75
76    fn command_stderr(&mut self) -> Option<String> {
77        Some(text(&self.command_output().ok()?.stderr))
78    }
79
80    fn command_spawn(&mut self) -> Result<Child> {
81        let program = program(self);
82
83        self.spawn()
84            .with_context(|| format!("启动 {program} 失败,请确认该命令存在且可用"))
85    }
86
87    fn command_line(&self) -> String {
88        let mut line = program(self);
89        for arg in self.get_args() {
90            line.push(' ');
91            line.push_str(&arg.to_string_lossy());
92        }
93
94        line
95    }
96}
97
98/// 命令的程序名。
99fn program(command: &Command) -> String {
100    command.get_program().to_string_lossy().into_owned()
101}
102
103/// 退出码非零的错误:带上命令行、退出码与完整输出。
104fn failure(command: &Command, output: &Output) -> anyhow::Error {
105    let code = output
106        .status
107        .code()
108        .map_or_else(|| "未知".to_string(), |code| code.to_string());
109
110    anyhow!(
111        "{} 执行失败\n  命令:{}\n  退出码:{code}\n  标准输出:{}\n  标准错误:{}",
112        program(command),
113        command.command_line(),
114        output_text(&output.stdout),
115        output_text(&output.stderr),
116    )
117}
118
119/// 外部命令输出转文本。
120fn text(bytes: &[u8]) -> String {
121    String::from_utf8_lossy(bytes).into_owned()
122}
123
124/// 外部命令输出转文本;空输出用占位符表示,避免错误信息里出现空白。
125fn output_text(bytes: &[u8]) -> String {
126    let text = text(bytes);
127    let text = text.trim();
128    if text.is_empty() {
129        "(空)".to_string()
130    } else {
131        text.to_string()
132    }
133}