Skip to main content

pretty_please/
exec.rs

1use std::borrow::Cow;
2use std::path::PathBuf;
3use std::process::{Command, ExitStatus};
4
5use anyhow::{Context, Result};
6use owo_colors::OwoColorize;
7
8use crate::cli::CommandInput;
9use crate::error::PleaseError;
10
11#[derive(Clone, Debug, Eq, PartialEq)]
12pub struct Runtime {
13    pub is_root: bool,
14    pub sudo_path: Option<PathBuf>,
15}
16
17impl Runtime {
18    pub fn detect() -> Self {
19        Self {
20            is_root: is_root(),
21            sudo_path: which::which("sudo").ok(),
22        }
23    }
24}
25
26#[derive(Clone, Debug, Eq, PartialEq)]
27pub struct ExecutionPlan {
28    pub program: PathBuf,
29    pub args: Vec<String>,
30    pub note: Option<String>,
31}
32
33impl ExecutionPlan {
34    pub fn argv(&self) -> Vec<String> {
35        let mut argv = vec![self.program.display().to_string()];
36        argv.extend(self.args.clone());
37        argv
38    }
39}
40
41pub fn plan(input: CommandInput, runtime: &Runtime) -> Result<ExecutionPlan, PleaseError> {
42    let args = resolve_args(input)?;
43    let first = args[0].clone();
44
45    if is_shell_builtin(&first) {
46        return Err(PleaseError::Builtin(first));
47    }
48
49    if first == "sudo" {
50        return Ok(ExecutionPlan {
51            program: PathBuf::from("sudo"),
52            args: args.into_iter().skip(1).collect(),
53            note: Some("note: command already starts with sudo; leaving it alone.".to_string()),
54        });
55    }
56
57    if runtime.is_root {
58        return Ok(ExecutionPlan {
59            program: PathBuf::from(&first),
60            args: args.into_iter().skip(1).collect(),
61            note: Some("note: already running as root; skipping sudo.".to_string()),
62        });
63    }
64
65    let sudo_path = runtime.sudo_path.clone().ok_or(PleaseError::MissingSudo)?;
66
67    Ok(ExecutionPlan {
68        program: sudo_path,
69        args,
70        note: None,
71    })
72}
73
74pub fn run(input: CommandInput) -> Result<()> {
75    let runtime = Runtime::detect();
76    let plan = plan(input, &runtime)?;
77
78    if let Some(note) = &plan.note {
79        eprintln!("{}", note.yellow());
80    }
81
82    let status = spawn(&plan)?;
83    std::process::exit(exit_code(status));
84}
85
86pub fn spawn(plan: &ExecutionPlan) -> Result<ExitStatus> {
87    Command::new(&plan.program)
88        .args(&plan.args)
89        .status()
90        .with_context(|| {
91            format!(
92                "failed to launch `{}`",
93                shell_words::join(plan.argv().iter().map(String::as_str))
94            )
95        })
96}
97
98fn resolve_args(input: CommandInput) -> Result<Vec<String>, PleaseError> {
99    match input {
100        CommandInput::Explicit(args) => normalize_args(args),
101        CommandInput::History(command) => normalize_history(command),
102    }
103}
104
105fn normalize_args(args: Vec<String>) -> Result<Vec<String>, PleaseError> {
106    if args.is_empty() || args.iter().all(|arg| arg.trim().is_empty()) {
107        return Err(PleaseError::MissingCommand);
108    }
109
110    Ok(args)
111}
112
113fn normalize_history(command: String) -> Result<Vec<String>, PleaseError> {
114    if command.trim().is_empty() {
115        return Err(PleaseError::EmptyHistory);
116    }
117
118    let parsed = shell_words::split(&command).map_err(PleaseError::ParseHistory)?;
119    if parsed.is_empty() {
120        return Err(PleaseError::EmptyHistory);
121    }
122
123    Ok(parsed)
124}
125
126fn is_shell_builtin(command: &str) -> bool {
127    let normalized = normalize_builtin_name(command);
128
129    matches!(
130        normalized.as_ref(),
131        "." | "alias"
132            | "bg"
133            | "bind"
134            | "builtin"
135            | "cd"
136            | "command"
137            | "complete"
138            | "compgen"
139            | "declare"
140            | "dirs"
141            | "disown"
142            | "enable"
143            | "eval"
144            | "exec"
145            | "exit"
146            | "export"
147            | "fc"
148            | "fg"
149            | "get-history"
150            | "getopts"
151            | "hash"
152            | "help"
153            | "history"
154            | "import-module"
155            | "jobs"
156            | "let"
157            | "local"
158            | "logout"
159            | "popd"
160            | "pushd"
161            | "read"
162            | "readonly"
163            | "return"
164            | "set"
165            | "set-alias"
166            | "set-item"
167            | "set-location"
168            | "set-variable"
169            | "shift"
170            | "shopt"
171            | "source"
172            | "trap"
173            | "type"
174            | "typeset"
175            | "ulimit"
176            | "umask"
177            | "unalias"
178            | "unset"
179            | "wait"
180    )
181}
182
183fn normalize_builtin_name(command: &str) -> Cow<'_, str> {
184    if command == "." {
185        Cow::Borrowed(".")
186    } else {
187        Cow::Owned(command.trim().to_ascii_lowercase())
188    }
189}
190
191fn exit_code(status: ExitStatus) -> i32 {
192    status.code().unwrap_or({
193        #[cfg(unix)]
194        {
195            use std::os::unix::process::ExitStatusExt;
196
197            status.signal().map_or(1, |signal| 128 + signal)
198        }
199
200        #[cfg(not(unix))]
201        {
202            1
203        }
204    })
205}
206
207#[cfg(unix)]
208fn is_root() -> bool {
209    unsafe { libc::geteuid() == 0 }
210}
211
212#[cfg(not(unix))]
213fn is_root() -> bool {
214    false
215}