doum_cli/tools/
executor.rs

1use crate::system::env::{OsType, ShellType, SystemInfo};
2use crate::system::error::{DoumError, DoumResult};
3use std::process::{Command, Output};
4use std::time::Duration;
5
6/// 명령 실행 결과
7#[derive(Debug)]
8pub struct CommandOutput {
9    pub success: bool,
10    pub exit_code: i32,
11    pub stdout: Vec<u8>,
12    pub stderr: Vec<u8>,
13}
14
15/// OS와 쉘에 맞게 명령 실행
16pub fn execute(command: &str, system_info: &SystemInfo) -> DoumResult<CommandOutput> {
17    execute_with_timeout(command, system_info, None)
18}
19
20/// 타임아웃을 지정하여 명령 실행
21pub fn execute_with_timeout(
22    command: &str,
23    system_info: &SystemInfo,
24    timeout: Option<Duration>,
25) -> DoumResult<CommandOutput> {
26    let output = match system_info.os {
27        OsType::Windows => execute_windows(command, &system_info.shell, timeout)?,
28        OsType::Linux | OsType::MacOS => execute_unix(command, &system_info.shell, timeout)?,
29    };
30
31    let exit_code = output.status.code().unwrap_or(-1);
32    let success = output.status.success();
33
34    Ok(CommandOutput {
35        stdout: output.stdout,
36        stderr: output.stderr,
37        exit_code,
38        success,
39    })
40}
41
42/// Windows에서 명령 실행
43fn execute_windows(
44    command: &str,
45    shell: &ShellType,
46    _timeout: Option<Duration>,
47) -> DoumResult<Output> {
48    let mut cmd = match shell {
49        ShellType::PowerShell => {
50            let mut c = Command::new("powershell.exe");
51            c.arg("-NoProfile");
52            c.arg("-Command");
53            // PowerShell은 기본적으로 UTF-8 처리
54            c.arg(format!(
55                "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; {}",
56                command
57            ));
58            c
59        }
60        ShellType::Cmd
61        | ShellType::Bash
62        | ShellType::Zsh
63        | ShellType::Fish
64        | ShellType::Unknown => {
65            // Windows에서는 기본값으로 cmd.exe 사용
66            let mut c = Command::new("cmd.exe");
67            c.arg("/C");
68            // cmd는 chcp 65001로 UTF-8 설정
69            c.arg(format!("chcp 65001 >nul && {}", command));
70            c
71        }
72    };
73
74    // 타임아웃 구현은 향후 개선 가능 (현재는 기본 동작)
75    let output = cmd
76        .output()
77        .map_err(|e| DoumError::CommandExecution(format!("명령 실행 실패: {}", e)))?;
78
79    Ok(output)
80}
81
82/// Unix 계열에서 명령 실행
83fn execute_unix(
84    command: &str,
85    shell: &ShellType,
86    _timeout: Option<Duration>,
87) -> DoumResult<Output> {
88    let shell_path = match shell {
89        ShellType::Bash => "/bin/bash",
90        ShellType::Zsh => "/bin/zsh",
91        ShellType::Fish => "/usr/bin/fish",
92        _ => "/bin/sh", // 기본값
93    };
94
95    let mut cmd = Command::new(shell_path);
96    cmd.arg("-c");
97    cmd.arg(command);
98
99    let output = cmd
100        .output()
101        .map_err(|e| DoumError::CommandExecution(format!("명령 실행 실패: {}", e)))?;
102
103    Ok(output)
104}
105
106impl CommandOutput {
107    /// stdout를 UTF-8 문자열로 변환
108    pub fn stdout_string(&self) -> String {
109        String::from_utf8_lossy(&self.stdout).to_string()
110    }
111
112    /// stderr를 UTF-8 문자열로 변환
113    pub fn stderr_string(&self) -> String {
114        String::from_utf8_lossy(&self.stderr).to_string()
115    }
116
117    /// 명령 실행 결과를 사람이 읽기 쉬운 형태로 출력
118    pub fn display(&self) -> String {
119        let mut result = String::new();
120
121        if !self.stdout.is_empty() {
122            result.push_str(&self.stdout_string());
123        }
124
125        if !self.stderr.is_empty() {
126            if !result.is_empty() {
127                result.push('\n');
128            }
129            result.push_str("=== stderr ===\n");
130            result.push_str(&self.stderr_string());
131        }
132
133        if !self.success {
134            if !result.is_empty() {
135                result.push('\n');
136            }
137            result.push_str(&format!("Exit code: {}", self.exit_code));
138        }
139
140        result
141    }
142}