use super::braces::{self, BraceError, MAX_BRACE_EXPANSIONS};
use super::env::ShellExecEnv;
use super::glob::{self, WalkOptions};
use super::interpreter::Interpreter;
use super::io::ShellSysError;
use super::parser::{Atom, CmdSubst, SimpleAtom};
use super::ShellError;
const TRIM_CHARS: &[char] = &[' ', '\n', '\r', '\t'];
#[derive(Clone, Debug)]
pub(crate) enum ExpandError {
Sys(ShellSysError),
Custom(String),
Fatal(ShellError),
}
impl ExpandError {
pub(crate) fn display(self) -> Result<String, ShellError> {
match self {
ExpandError::Sys(e) => Ok(e.display()),
ExpandError::Custom(msg) => Ok(format!("bun: {msg}")),
ExpandError::Fatal(e) => Err(e),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct Expanded {
pub(crate) buf: String,
pub(crate) bounds: Vec<usize>,
pub(crate) out_exit_code: Option<i32>,
pub(crate) has_quoted_empty: bool,
}
impl Expanded {
pub(crate) fn words(&self) -> Vec<String> {
let mut words = Vec::with_capacity(self.bounds.len() + 1);
let mut start = 0;
for &end in &self.bounds {
words.push(self.buf[start..end].to_string());
start = end;
}
words.push(self.buf[start..].to_string());
words
}
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct ExpandOpts {
pub(crate) is_assign: bool,
pub(crate) assign_ctx: bool,
}
fn is_sep(ch: char) -> bool {
ch == '/' || (cfg!(windows) && ch == '\\')
}
fn neutralize_glob_metachars(current_out: &str, meta_offsets: &[usize]) -> String {
let mut pattern = String::with_capacity(current_out.len());
let mut next_meta = 0;
for (i, ch) in current_out.char_indices() {
if meta_offsets.get(next_meta) == Some(&i) {
next_meta += 1;
pattern.push(ch);
continue;
}
match ch {
'*' | '?' | '[' | ']' | '{' | '}' | ',' => {
pattern.push('[');
pattern.push(ch);
pattern.push(']');
}
'!' => {
if pattern.chars().last().is_none_or(is_sep) {
pattern.push_str("{!}");
} else {
pattern.push('!');
}
}
'\\' if !cfg!(windows) => pattern.push_str("[\\\\]"),
_ => pattern.push(ch),
}
}
pattern
}
struct Expansion<'a> {
interp: &'a Interpreter,
shell: &'a ShellExecEnv,
atom: &'a Atom,
opts: ExpandOpts,
buf: String,
bounds: Vec<usize>,
current_out: String,
meta_offsets: Vec<usize>,
has_quoted_empty: bool,
out_exit_code: Option<i32>,
}
impl<'a> Expansion<'a> {
fn push_current_out(&mut self) {
if !self.buf.is_empty() {
self.bounds.push(self.buf.len());
}
self.buf.push_str(&self.current_out);
self.current_out.clear();
self.meta_offsets.clear();
}
fn push_word(&mut self, word: &str) {
if !self.buf.is_empty() {
self.bounds.push(self.buf.len());
}
self.buf.push_str(word);
}
fn push_meta(&mut self, s: &str) {
for i in 0..s.len() {
self.meta_offsets.push(self.current_out.len() + i);
}
self.current_out.push_str(s);
}
fn expand_simple(&mut self, simple: &SimpleAtom) {
match simple {
SimpleAtom::Text(text) => self.current_out.push_str(text),
SimpleAtom::QuotedEmpty => self.has_quoted_empty = true,
SimpleAtom::Var(name) => {
if let Some(v) = self.shell.get_var(name) {
self.current_out.push_str(v);
}
}
SimpleAtom::VarArgv(n) => {
if let Some(v) = self.interp.argv.get(usize::from(*n)) {
self.current_out.push_str(v);
}
}
SimpleAtom::Asterisk => self.push_meta("*"),
SimpleAtom::DoubleAsterisk => self.push_meta("**"),
SimpleAtom::BraceBegin => self.push_meta("{"),
SimpleAtom::BraceEnd => self.push_meta("}"),
SimpleAtom::Comma => self.push_meta(","),
SimpleAtom::Tilde => {
let home = self.shell.get_homedir();
self.current_out.push_str(&home);
}
SimpleAtom::CmdSubst(_) => {}
}
}
async fn cmd_subst(&mut self, simple: &CmdSubst) -> Result<(), ExpandError> {
let (exit_code, stdout) = self
.interp
.cmd_subst(&simple.script, self.shell)
.await
.map_err(ExpandError::Fatal)?;
if exit_code != 0 && matches!(self.atom, Atom::Simple(_)) {
self.out_exit_code = Some(exit_code);
}
if simple.quoted || self.opts.assign_ctx {
self.current_out
.push_str(stdout.trim_end_matches(TRIM_CHARS));
} else {
self.post_subshell_expansion(&stdout);
}
Ok(())
}
fn post_subshell_expansion(&mut self, stdout: &str) {
let out = stdout.strip_suffix('\n').unwrap_or(stdout);
let replaced = out.replace('\n', " ");
let out = replaced.trim_matches(TRIM_CHARS);
if out.is_empty() {
return;
}
let mut prev_ws = false;
let mut a = 0;
for (i, c) in out.char_indices() {
if prev_ws {
if c != ' ' {
a = i;
prev_ws = false;
}
continue;
}
if c == ' ' {
prev_ws = true;
self.current_out.push_str(&out[a..i]);
self.push_current_out();
}
}
self.current_out.push_str(&out[a..]);
}
fn expand_leading_tilde(&mut self) {
let home = self.shell.get_homedir();
let before = self.current_out.len();
match self.current_out.chars().next() {
Some('/') | Some('\\') => self.current_out.insert_str(0, &home),
Some(_) => self.current_out.insert(0, '~'),
None if self.has_quoted_empty => self.current_out = home,
None => {}
}
let prepended = self.current_out.len() - before;
if prepended != 0 {
for o in &mut self.meta_offsets {
*o += prepended;
}
}
}
fn brace_expand(&mut self) -> Result<(), ExpandError> {
let mut escaped = String::with_capacity(self.current_out.len());
let mut next_meta = 0;
for (i, ch) in self.current_out.char_indices() {
if self.meta_offsets.get(next_meta) == Some(&i) {
next_meta += 1;
} else if matches!(ch, '{' | '}' | ',' | '\\') {
escaped.push('\\');
}
escaped.push(ch);
}
let tokens = braces::tokenize(&escaped);
let count = braces::calculate_expanded_amount(&tokens.tokens);
if count > MAX_BRACE_EXPANSIONS {
return Err(ExpandError::Custom(format!(
"too many brace expansions ({count} > {MAX_BRACE_EXPANSIONS})"
)));
}
let expanded = if count == 0 {
vec![self.current_out.clone()]
} else {
match braces::expand(tokens.tokens, count, tokens.contains_nested) {
Ok(words) => words,
Err(BraceError::TooManyBraces) => {
return Err(ExpandError::Custom(
"too many braces in brace expansion".to_string(),
))
}
Err(e) => return Err(ExpandError::Custom(e.to_string())),
}
};
for word in &expanded {
self.push_word(word);
}
Ok(())
}
fn glob(&mut self) -> Result<(), ExpandError> {
let pattern = neutralize_glob_metachars(&self.current_out, &self.meta_offsets);
let options = WalkOptions {
cwd: self.shell.cwd.clone(),
dot: false,
absolute: false,
follow_symlinks: false,
error_on_broken_symlinks: false,
only_files: false,
};
let (entries, walk_err) = match glob::walk(&pattern, &options) {
Ok(entries) => (entries, None),
Err(e) if e.is("ENOENT") || e.is("ENOTDIR") => (Vec::new(), None),
Err(e) => (Vec::new(), Some(e)),
};
if entries.is_empty() || walk_err.is_some() {
if self.opts.is_assign {
self.push_current_out();
return Ok(());
}
if let Some(e) = walk_err {
return Err(match e.code {
Some(code) if code.starts_with('E') => ExpandError::Sys(
ShellSysError::new(code)
.with_path(e.path)
.with_syscall(e.syscall),
),
_ => ExpandError::Custom(e.message),
});
}
return Err(ExpandError::Custom(format!(
"no matches found: {}",
self.current_out
)));
}
for entry in &entries {
self.push_word(entry);
}
Ok(())
}
async fn run(&mut self) -> Result<(), ExpandError> {
let atom = self.atom;
let atoms = atom.atoms();
let leading_tilde =
matches!(atom, Atom::Compound(_)) && matches!(atoms.first(), Some(SimpleAtom::Tilde));
for simple in &atoms[usize::from(leading_tilde)..] {
match simple {
SimpleAtom::CmdSubst(cs) => self.cmd_subst(cs).await?,
_ => self.expand_simple(simple),
}
}
if leading_tilde {
self.expand_leading_tilde();
}
if atom.has_brace_expansion() {
self.brace_expand()?;
if atom.has_glob_expansion() {
self.glob()?;
}
return Ok(());
}
if atom.has_glob_expansion() {
return self.glob();
}
self.push_current_out();
Ok(())
}
}
pub(crate) async fn expand_atom(
interp: &Interpreter,
shell: &ShellExecEnv,
atom: &Atom,
opts: ExpandOpts,
) -> Result<Expanded, ExpandError> {
let mut e = Expansion {
interp,
shell,
atom,
opts,
buf: String::new(),
bounds: Vec::new(),
current_out: String::new(),
meta_offsets: Vec::new(),
has_quoted_empty: false,
out_exit_code: None,
};
e.run().await?;
Ok(Expanded {
buf: e.buf,
bounds: e.bounds,
out_exit_code: e.out_exit_code,
has_quoted_empty: e.has_quoted_empty,
})
}
#[cfg(test)]
mod tests;