light_fw_cli_core/
process.rs1use std::path::Path;
8use std::process::Command;
9
10use crate::error::{Error, Result};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct CommandOutput {
15 pub status: i32,
17 pub stdout: String,
19 pub stderr: String,
21}
22
23impl CommandOutput {
24 pub fn success(&self) -> bool {
26 self.status == 0
27 }
28}
29
30pub trait CommandRunner: Send + Sync {
32 fn run(&self, program: &str, args: &[String], cwd: Option<&Path>) -> Result<CommandOutput>;
34}
35
36#[derive(Debug, Default, Clone, Copy)]
38pub struct SystemRunner;
39
40impl CommandRunner for SystemRunner {
41 fn run(&self, program: &str, args: &[String], cwd: Option<&Path>) -> Result<CommandOutput> {
42 let mut command = Command::new(program);
43 command.args(args);
44 if let Some(dir) = cwd {
45 command.current_dir(dir);
46 }
47 let output = command.output().map_err(|e| {
48 if e.kind() == std::io::ErrorKind::NotFound {
49 Error::CommandNotFound(program.to_string())
50 } else {
51 Error::Io(e)
52 }
53 })?;
54 Ok(CommandOutput {
55 status: output.status.code().unwrap_or(-1),
56 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
57 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
58 })
59 }
60}
61
62pub fn parse_program_version(stdout: &str) -> Option<semver::Version> {
67 let mut parts = stdout.split_whitespace();
68 let _program = parts.next()?;
69 let version = parts.next()?;
70 semver::Version::parse(version).ok()
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn parses_version_line() {
79 let v =
80 parse_program_version("rustc 1.99.0 (5f94df478 2026-08-27)").expect("version parsed");
81 assert_eq!(v, semver::Version::new(1, 99, 0));
82 }
83
84 #[test]
85 fn version_parse_rejects_garbage() {
86 assert!(parse_program_version("no version here").is_none());
87 assert!(parse_program_version("").is_none());
88 }
89}