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}