mod extra;
use crate::commands::{cd::resolve_cd, VirtualCommandRegistry};
use crate::{
quote, CommandContext, CommandResult, ProcessRunner, Result, RunOptions, StreamingRunner,
};
use std::collections::HashMap;
use std::io::Write;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct Config {
pub silent: bool,
pub fatal: bool,
pub verbose: bool,
pub glob: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
silent: false,
fatal: false,
verbose: false,
glob: true,
}
}
}
#[derive(Debug)]
pub struct ShellJs {
pub config: Config,
pub env: HashMap<String, String>,
cwd: PathBuf,
stack: Vec<PathBuf>,
last_error: Option<String>,
last_code: i32,
}
impl Default for ShellJs {
fn default() -> Self {
Self::new()
}
}
macro_rules! methods {
($($name:ident),* $(,)?) => { $(
#[doc = concat!("Run `", stringify!($name), "` with separate argument values.")]
pub async fn $name(&mut self, args: &[&str]) -> Result<CommandResult> {
self.call(stringify!($name), args).await
}
)* };
}
impl ShellJs {
pub fn new() -> Self {
let cwd = std::env::current_dir().unwrap_or_else(|_| std::env::temp_dir());
let mut env: HashMap<String, String> = std::env::vars().collect();
env.insert("PWD".into(), cwd.display().to_string());
Self {
config: Config::default(),
env,
cwd,
stack: Vec::new(),
last_error: None,
last_code: 0,
}
}
pub fn cwd(&self) -> &std::path::Path {
&self.cwd
}
pub fn error(&self) -> Option<&str> {
self.last_error.as_deref()
}
pub fn error_code(&self) -> i32 {
self.last_code
}
fn finish(&mut self, result: CommandResult) -> Result<CommandResult> {
self.last_code = result.code;
self.last_error = if result.code != 0 {
Some(result.stderr.to_string())
} else {
None
};
if !self.config.silent {
let _ = std::io::stdout().write_all(result.stdout.as_bytes());
let _ = std::io::stderr().write_all(result.stderr.as_bytes());
}
if self.config.fatal {
result.error_for_status()
} else {
Ok(result)
}
}
fn context(&self, args: &[&str]) -> CommandContext {
let mut context = CommandContext::new(args.iter().map(|arg| arg.to_string()).collect());
context.cwd = Some(self.cwd.clone());
context.env = Some(self.env.clone());
context
}
fn expand(&self, command: &str, args: &[&str]) -> Vec<String> {
args.iter()
.flat_map(|arg| {
if !self.config.glob
|| matches!(
command,
"echo" | "sed" | "grep" | "test" | "basename" | "dirname"
)
|| arg.starts_with('-')
|| !arg.contains(['*', '?', '['])
{
return vec![arg.to_string()];
}
let pattern = self.cwd.join(arg).display().to_string();
let matches = glob::glob(&pattern)
.map(|paths| {
paths
.flatten()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
})
.unwrap_or_default();
if matches.is_empty() {
vec![arg.to_string()]
} else {
matches
}
})
.collect()
}
pub async fn call(&mut self, command: &str, args: &[&str]) -> Result<CommandResult> {
if command == "cd" {
return self.cd(args).await;
}
let expanded = self.expand(command, args);
let args: Vec<&str> = expanded.iter().map(String::as_str).collect();
if self.config.verbose {
eprintln!("{command} {args:?}");
}
let context = self.context(&args);
let registry = VirtualCommandRegistry::with_builtins();
let result = match command {
"find" | "grep" | "sed" | "ln" | "chmod" => extra::run(command, context).await,
_ => match registry.get(command) {
Some(handler) => handler(context).await,
None => CommandResult::error_with_code(
format!("shelljs: unsupported command: {command}"),
127,
),
},
};
self.finish(result)
}
methods!(
cat, chmod, cp, echo, find, grep, head, ln, ls, mkdir, mv, pwd, rm, sed, sort, tail, touch,
uniq, which, basename, dirname, sleep, env, seq, tee
);
pub async fn cd(&mut self, args: &[&str]) -> Result<CommandResult> {
let (result, context) = resolve_cd(self.context(args)).await;
if let Some(context) = context {
self.env
.insert("PWD".into(), context.cwd.display().to_string());
self.env
.insert("OLDPWD".into(), context.oldpwd.display().to_string());
self.cwd = context.cwd;
}
self.finish(result)
}
pub async fn test(&mut self, args: &[&str]) -> Result<bool> {
Ok(self.call("test", args).await?.code == 0)
}
pub async fn cmd(&mut self, program: &str, args: &[&str]) -> Result<CommandResult> {
let result = StreamingRunner::from_argv(program, args)
.cwd(self.cwd.clone())
.env(self.env.clone())
.collect()
.await?;
self.finish(result)
}
pub async fn exec(&mut self, command: &str) -> Result<CommandResult> {
let options = RunOptions {
mirror: false,
cwd: Some(self.cwd.clone()),
env: Some(self.env.clone()),
..RunOptions::default()
};
let result = ProcessRunner::new(command, options).run().await?;
self.finish(result)
}
pub fn dirs(&self) -> CommandResult {
CommandResult::success(
std::iter::once(&self.cwd)
.chain(self.stack.iter().rev())
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
.join("\n"),
)
}
pub async fn pushd(&mut self, args: &[&str]) -> Result<CommandResult> {
let previous = self.cwd.clone();
let result = self.cd(args).await?;
if result.code == 0 {
self.stack.push(previous);
}
Ok(result)
}
pub async fn popd(&mut self) -> Result<CommandResult> {
let Some(path) = self.stack.last().cloned() else {
return self.finish(CommandResult::error("popd: directory stack empty"));
};
let result = self.cd(&[&path.display().to_string()]).await?;
if result.code == 0 {
self.stack.pop();
}
Ok(result)
}
pub fn tempdir(&self) -> PathBuf {
std::env::temp_dir()
}
pub fn set(&mut self, option: &str) -> Result<CommandResult> {
match option {
"-e" => self.config.fatal = true,
"+e" => self.config.fatal = false,
"-v" => self.config.verbose = true,
"+v" => self.config.verbose = false,
"-f" => self.config.glob = false,
"+f" => self.config.glob = true,
_ => return self.finish(CommandResult::error("set: unsupported option")),
}
self.finish(CommandResult::success_empty())
}
pub async fn to(
&mut self,
result: &CommandResult,
path: &str,
append: bool,
) -> Result<CommandResult> {
use tokio::io::AsyncWriteExt;
let write = async {
let mut file = tokio::fs::OpenOptions::new()
.write(true)
.create(true)
.append(append)
.truncate(!append)
.open(self.cwd.join(path))
.await?;
file.write_all(result.stdout.as_bytes()).await?;
file.flush().await
}
.await;
self.finish(match write {
Ok(()) => CommandResult::success_empty(),
Err(error) => CommandResult::error(format!("to: {path}: {error}")),
})
}
pub fn command(&self, name: &str, args: &[&str]) -> String {
std::iter::once(name)
.chain(args.iter().copied())
.map(quote)
.collect::<Vec<_>>()
.join(" ")
}
}