1mod extra;
5
6use crate::commands::{cd::resolve_cd, VirtualCommandRegistry};
7use crate::{
8 quote, CommandContext, CommandResult, ProcessRunner, Result, RunOptions, StreamingRunner,
9};
10use std::collections::HashMap;
11use std::io::Write;
12use std::path::PathBuf;
13
14#[derive(Debug, Clone)]
16pub struct Config {
17 pub silent: bool,
19 pub fatal: bool,
21 pub verbose: bool,
23 pub glob: bool,
25}
26impl Default for Config {
27 fn default() -> Self {
28 Self {
29 silent: false,
30 fatal: false,
31 verbose: false,
32 glob: true,
33 }
34 }
35}
36
37#[derive(Debug)]
41pub struct ShellJs {
42 pub config: Config,
44 pub env: HashMap<String, String>,
46 cwd: PathBuf,
47 stack: Vec<PathBuf>,
48 last_error: Option<String>,
49 last_code: i32,
50}
51impl Default for ShellJs {
52 fn default() -> Self {
53 Self::new()
54 }
55}
56
57macro_rules! methods {
58 ($($name:ident),* $(,)?) => { $(
59 #[doc = concat!("Run `", stringify!($name), "` with separate argument values.")]
60 pub async fn $name(&mut self, args: &[&str]) -> Result<CommandResult> {
61 self.call(stringify!($name), args).await
62 }
63 )* };
64}
65
66impl ShellJs {
67 pub fn new() -> Self {
69 let cwd = std::env::current_dir().unwrap_or_else(|_| std::env::temp_dir());
70 let mut env: HashMap<String, String> = std::env::vars().collect();
71 env.insert("PWD".into(), cwd.display().to_string());
72 Self {
73 config: Config::default(),
74 env,
75 cwd,
76 stack: Vec::new(),
77 last_error: None,
78 last_code: 0,
79 }
80 }
81 pub fn cwd(&self) -> &std::path::Path {
83 &self.cwd
84 }
85 pub fn error(&self) -> Option<&str> {
87 self.last_error.as_deref()
88 }
89 pub fn error_code(&self) -> i32 {
91 self.last_code
92 }
93
94 fn finish(&mut self, result: CommandResult) -> Result<CommandResult> {
95 self.last_code = result.code;
96 self.last_error = if result.code != 0 {
97 Some(result.stderr.to_string())
98 } else {
99 None
100 };
101 if !self.config.silent {
102 let _ = std::io::stdout().write_all(result.stdout.as_bytes());
103 let _ = std::io::stderr().write_all(result.stderr.as_bytes());
104 }
105 if self.config.fatal {
106 result.error_for_status()
107 } else {
108 Ok(result)
109 }
110 }
111
112 fn context(&self, args: &[&str]) -> CommandContext {
113 let mut context = CommandContext::new(args.iter().map(|arg| arg.to_string()).collect());
114 context.cwd = Some(self.cwd.clone());
115 context.env = Some(self.env.clone());
116 context
117 }
118
119 fn expand(&self, command: &str, args: &[&str]) -> Vec<String> {
120 args.iter()
121 .flat_map(|arg| {
122 if !self.config.glob
123 || matches!(
124 command,
125 "echo" | "sed" | "grep" | "test" | "basename" | "dirname"
126 )
127 || arg.starts_with('-')
128 || !arg.contains(['*', '?', '['])
129 {
130 return vec![arg.to_string()];
131 }
132 let pattern = self.cwd.join(arg).display().to_string();
133 let matches = glob::glob(&pattern)
134 .map(|paths| {
135 paths
136 .flatten()
137 .map(|path| path.display().to_string())
138 .collect::<Vec<_>>()
139 })
140 .unwrap_or_default();
141 if matches.is_empty() {
142 vec![arg.to_string()]
143 } else {
144 matches
145 }
146 })
147 .collect()
148 }
149
150 pub async fn call(&mut self, command: &str, args: &[&str]) -> Result<CommandResult> {
153 if command == "cd" {
154 return self.cd(args).await;
155 }
156 let expanded = self.expand(command, args);
157 let args: Vec<&str> = expanded.iter().map(String::as_str).collect();
158 if self.config.verbose {
159 eprintln!("{command} {args:?}");
160 }
161 let context = self.context(&args);
162 let registry = VirtualCommandRegistry::with_builtins();
163 let result = match command {
164 "find" | "grep" | "sed" | "ln" | "chmod" => extra::run(command, context).await,
165 _ => match registry.get(command) {
166 Some(handler) => handler(context).await,
167 None => CommandResult::error_with_code(
168 format!("shelljs: unsupported command: {command}"),
169 127,
170 ),
171 },
172 };
173 self.finish(result)
174 }
175
176 methods!(
177 cat, chmod, cp, echo, find, grep, head, ln, ls, mkdir, mv, pwd, rm, sed, sort, tail, touch,
178 uniq, which, basename, dirname, sleep, env, seq, tee
179 );
180
181 pub async fn cd(&mut self, args: &[&str]) -> Result<CommandResult> {
183 let (result, context) = resolve_cd(self.context(args)).await;
184 if let Some(context) = context {
185 self.env
186 .insert("PWD".into(), context.cwd.display().to_string());
187 self.env
188 .insert("OLDPWD".into(), context.oldpwd.display().to_string());
189 self.cwd = context.cwd;
190 }
191 self.finish(result)
192 }
193 pub async fn test(&mut self, args: &[&str]) -> Result<bool> {
195 Ok(self.call("test", args).await?.code == 0)
196 }
197 pub async fn cmd(&mut self, program: &str, args: &[&str]) -> Result<CommandResult> {
199 let result = StreamingRunner::from_argv(program, args)
200 .cwd(self.cwd.clone())
201 .env(self.env.clone())
202 .collect()
203 .await?;
204 self.finish(result)
205 }
206 pub async fn exec(&mut self, command: &str) -> Result<CommandResult> {
208 let options = RunOptions {
209 mirror: false,
210 cwd: Some(self.cwd.clone()),
211 env: Some(self.env.clone()),
212 ..RunOptions::default()
213 };
214 let result = ProcessRunner::new(command, options).run().await?;
215 self.finish(result)
216 }
217 pub fn dirs(&self) -> CommandResult {
219 CommandResult::success(
220 std::iter::once(&self.cwd)
221 .chain(self.stack.iter().rev())
222 .map(|path| path.display().to_string())
223 .collect::<Vec<_>>()
224 .join("\n"),
225 )
226 }
227 pub async fn pushd(&mut self, args: &[&str]) -> Result<CommandResult> {
229 let previous = self.cwd.clone();
230 let result = self.cd(args).await?;
231 if result.code == 0 {
232 self.stack.push(previous);
233 }
234 Ok(result)
235 }
236 pub async fn popd(&mut self) -> Result<CommandResult> {
238 let Some(path) = self.stack.last().cloned() else {
239 return self.finish(CommandResult::error("popd: directory stack empty"));
240 };
241 let result = self.cd(&[&path.display().to_string()]).await?;
242 if result.code == 0 {
243 self.stack.pop();
244 }
245 Ok(result)
246 }
247 pub fn tempdir(&self) -> PathBuf {
249 std::env::temp_dir()
250 }
251 pub fn set(&mut self, option: &str) -> Result<CommandResult> {
253 match option {
254 "-e" => self.config.fatal = true,
255 "+e" => self.config.fatal = false,
256 "-v" => self.config.verbose = true,
257 "+v" => self.config.verbose = false,
258 "-f" => self.config.glob = false,
259 "+f" => self.config.glob = true,
260 _ => return self.finish(CommandResult::error("set: unsupported option")),
261 }
262 self.finish(CommandResult::success_empty())
263 }
264 pub async fn to(
266 &mut self,
267 result: &CommandResult,
268 path: &str,
269 append: bool,
270 ) -> Result<CommandResult> {
271 use tokio::io::AsyncWriteExt;
272 let write = async {
273 let mut file = tokio::fs::OpenOptions::new()
274 .write(true)
275 .create(true)
276 .append(append)
277 .truncate(!append)
278 .open(self.cwd.join(path))
279 .await?;
280 file.write_all(result.stdout.as_bytes()).await?;
281 file.flush().await
282 }
283 .await;
284 self.finish(match write {
285 Ok(()) => CommandResult::success_empty(),
286 Err(error) => CommandResult::error(format!("to: {path}: {error}")),
287 })
288 }
289 pub fn command(&self, name: &str, args: &[&str]) -> String {
291 std::iter::once(name)
292 .chain(args.iter().copied())
293 .map(quote)
294 .collect::<Vec<_>>()
295 .join(" ")
296 }
297}