pub mod arith;
pub mod command_sub;
pub mod field_split;
pub mod param;
pub mod pathname;
pub mod pattern;
mod heredoc;
mod pipeline;
mod scan;
mod tilde;
pub use heredoc::expand_body as expand_heredoc_body;
use pipeline::expand_word_to_fields;
pub(crate) use scan::skip_balanced_parens;
pub(crate) use tilde::expand_tilde_prefix;
use crate::env::ShellEnv;
use crate::parser::ast::Word;
#[derive(Debug, Clone, PartialEq)]
pub struct ExpandedField {
pub value: String,
quoted_mask: Vec<u64>,
pub was_quoted: bool,
}
impl ExpandedField {
pub fn new() -> Self {
Self {
value: String::new(),
quoted_mask: Vec::new(),
was_quoted: false,
}
}
pub fn is_quoted(&self, byte_index: usize) -> bool {
let word = byte_index / 64;
let bit = byte_index % 64;
self.quoted_mask
.get(word)
.is_some_and(|w| w & (1u64 << bit) != 0)
}
pub fn push_quoted(&mut self, s: &str) {
let start = self.value.len();
self.value.push_str(s);
self.set_range(start, s.len(), true);
self.was_quoted = true;
}
pub fn push_unquoted(&mut self, s: &str) {
let start = self.value.len();
self.value.push_str(s);
self.set_range(start, s.len(), false);
}
pub fn is_empty(&self) -> bool {
self.value.is_empty()
}
pub fn all_quoted(value: String) -> Self {
let len = value.len();
let needed_words = len.div_ceil(64);
let mask = vec![u64::MAX; needed_words];
Self {
value,
quoted_mask: mask,
was_quoted: false,
}
}
fn set_range(&mut self, start: usize, len: usize, quoted: bool) {
if len == 0 {
return;
}
let end = start + len;
let needed_words = end.div_ceil(64);
self.quoted_mask.resize(needed_words, 0);
if quoted {
for i in start..end {
self.quoted_mask[i / 64] |= 1u64 << (i % 64);
}
}
}
}
impl Default for ExpandedField {
fn default() -> Self {
Self::new()
}
}
pub fn expand_word(env: &mut ShellEnv, word: &Word) -> crate::error::Result<Vec<String>> {
let fields = expand_word_to_fields(env, word)?;
let fields = field_split::split(env, fields);
let fields = if env.mode.options.noglob {
fields
} else {
pathname::expand(env, fields)
};
Ok(fields
.into_iter()
.filter(|f| !f.is_empty() || f.was_quoted)
.map(|f| f.value)
.collect())
}
pub fn expand_words(env: &mut ShellEnv, words: &[Word]) -> crate::error::Result<Vec<String>> {
let mut result = Vec::new();
for word in words {
result.extend(expand_word(env, word)?);
}
Ok(result)
}
pub fn expand_word_to_string(env: &mut ShellEnv, word: &Word) -> crate::error::Result<String> {
let fields = expand_word_to_fields(env, word)?;
Ok(fields
.into_iter()
.map(|f| f.value)
.collect::<Vec<_>>()
.join(" "))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::env::ShellEnv;
use crate::parser::ast::{ParamExpr, SpecialParam, Word, WordPart};
fn make_env() -> ShellEnv {
ShellEnv::new("yosh", vec![])
}
#[test]
fn test_expand_word_basic() {
let mut env = make_env();
let word = Word::literal("hello");
assert_eq!(expand_word(&mut env, &word).unwrap(), vec!["hello"]);
}
#[test]
fn test_expand_words_basic() {
let mut env = make_env();
env.vars.set("A", "foo").unwrap();
let words = vec![
Word::literal("hello"),
Word {
parts: vec![WordPart::Parameter(ParamExpr::Simple("A".to_string()))],
},
];
assert_eq!(
expand_words(&mut env, &words).unwrap(),
vec!["hello", "foo"]
);
}
#[test]
fn test_dollar_at_in_double_quotes_splits() {
let mut env = ShellEnv::new(
"yosh",
vec!["a".to_string(), "b".to_string(), "c".to_string()],
);
let word = Word {
parts: vec![WordPart::DoubleQuoted(vec![WordPart::Parameter(
ParamExpr::Special(SpecialParam::At),
)])],
};
assert_eq!(expand_word(&mut env, &word).unwrap(), vec!["a", "b", "c"]);
}
#[test]
fn test_dollar_at_empty_params_produces_nothing() {
let mut env = ShellEnv::new("yosh", vec![]);
let word = Word {
parts: vec![WordPart::DoubleQuoted(vec![WordPart::Parameter(
ParamExpr::Special(SpecialParam::At),
)])],
};
let result = expand_word(&mut env, &word).unwrap();
assert!(result.is_empty(), "expected empty, got {:?}", result);
}
#[test]
fn test_dollar_star_in_double_quotes_joins() {
let mut env = ShellEnv::new(
"yosh",
vec!["a".to_string(), "b".to_string(), "c".to_string()],
);
let word = Word {
parts: vec![WordPart::DoubleQuoted(vec![WordPart::Parameter(
ParamExpr::Special(SpecialParam::Star),
)])],
};
assert_eq!(expand_word(&mut env, &word).unwrap(), vec!["a b c"]);
}
#[test]
fn test_tilde_root_starts_with_slash() {
let mut env = make_env();
let word = Word {
parts: vec![WordPart::Tilde(Some("root".to_string()))],
};
let result = expand_word_to_string(&mut env, &word).unwrap();
assert!(
result.starts_with('/') || result == "~root",
"unexpected tilde-root result: {}",
result
);
}
#[test]
fn test_literal() {
let mut env = make_env();
let word = Word::literal("hello");
assert_eq!(expand_word_to_string(&mut env, &word).unwrap(), "hello");
}
#[test]
fn test_single_quoted() {
let mut env = make_env();
let word = Word {
parts: vec![WordPart::SingleQuoted("hello world".to_string())],
};
assert_eq!(
expand_word_to_string(&mut env, &word).unwrap(),
"hello world"
);
}
#[test]
fn test_dollar_single_quoted() {
let mut env = make_env();
let word = Word {
parts: vec![WordPart::DollarSingleQuoted("hello\\nworld".to_string())],
};
assert_eq!(
expand_word_to_string(&mut env, &word).unwrap(),
"hello\\nworld"
);
}
#[test]
fn test_double_quoted_literal() {
let mut env = make_env();
let word = Word {
parts: vec![WordPart::DoubleQuoted(vec![WordPart::Literal(
"hello".to_string(),
)])],
};
assert_eq!(expand_word_to_string(&mut env, &word).unwrap(), "hello");
}
#[test]
fn test_simple_param() {
let mut env = make_env();
env.vars.set("FOO", "bar").unwrap();
let word = Word {
parts: vec![WordPart::Parameter(ParamExpr::Simple("FOO".to_string()))],
};
assert_eq!(expand_word_to_string(&mut env, &word).unwrap(), "bar");
}
#[test]
fn test_unset_param() {
let mut env = make_env();
let word = Word {
parts: vec![WordPart::Parameter(ParamExpr::Simple(
"UNSET_VAR_XYZ".to_string(),
))],
};
assert_eq!(expand_word_to_string(&mut env, &word).unwrap(), "");
}
#[test]
fn test_special_question() {
let mut env = make_env();
env.exec.last_exit_status = 42;
let word = Word {
parts: vec![WordPart::Parameter(ParamExpr::Special(
SpecialParam::Question,
))],
};
assert_eq!(expand_word_to_string(&mut env, &word).unwrap(), "42");
}
#[test]
fn test_special_dollar() {
let mut env = make_env();
let word = Word {
parts: vec![WordPart::Parameter(ParamExpr::Special(
SpecialParam::Dollar,
))],
};
let result = expand_word_to_string(&mut env, &word).unwrap();
let pid: i32 = result.parse().expect("PID should be an integer");
assert!(pid > 0);
}
#[test]
fn test_special_zero() {
let mut env = ShellEnv::new("myyosh", vec![]);
let word = Word {
parts: vec![WordPart::Parameter(ParamExpr::Special(SpecialParam::Zero))],
};
assert_eq!(expand_word_to_string(&mut env, &word).unwrap(), "myyosh");
}
#[test]
fn test_positional_param() {
let mut env = ShellEnv::new("yosh", vec!["first".to_string(), "second".to_string()]);
let word = Word {
parts: vec![WordPart::Parameter(ParamExpr::Positional(1))],
};
assert_eq!(expand_word_to_string(&mut env, &word).unwrap(), "first");
let word2 = Word {
parts: vec![WordPart::Parameter(ParamExpr::Positional(2))],
};
assert_eq!(expand_word_to_string(&mut env, &word2).unwrap(), "second");
}
#[test]
fn test_positional_out_of_range() {
let mut env = ShellEnv::new("yosh", vec!["only".to_string()]);
let word = Word {
parts: vec![WordPart::Parameter(ParamExpr::Positional(5))],
};
assert_eq!(expand_word_to_string(&mut env, &word).unwrap(), "");
}
#[test]
fn test_special_hash() {
let mut env = ShellEnv::new(
"yosh",
vec!["a".to_string(), "b".to_string(), "c".to_string()],
);
let word = Word {
parts: vec![WordPart::Parameter(ParamExpr::Special(SpecialParam::Hash))],
};
assert_eq!(expand_word_to_string(&mut env, &word).unwrap(), "3");
}
#[test]
fn test_tilde_none() {
let mut env = make_env();
env.vars.set("HOME", "/home/user").unwrap();
let word = Word {
parts: vec![WordPart::Tilde(None)],
};
assert_eq!(
expand_word_to_string(&mut env, &word).unwrap(),
"/home/user"
);
}
#[test]
fn test_tilde_none_no_home() {
let mut env = make_env();
let _ = env.vars.unset("HOME");
let word = Word {
parts: vec![WordPart::Tilde(None)],
};
assert_eq!(expand_word_to_string(&mut env, &word).unwrap(), "~");
}
#[test]
fn test_mixed_parts() {
let mut env = make_env();
env.vars.set("NAME", "world").unwrap();
let word = Word {
parts: vec![
WordPart::Literal("hello ".to_string()),
WordPart::Parameter(ParamExpr::Simple("NAME".to_string())),
WordPart::Literal("!".to_string()),
],
};
assert_eq!(
expand_word_to_string(&mut env, &word).unwrap(),
"hello world!"
);
}
#[test]
fn test_dollar_in_double_quote() {
let mut env = make_env();
env.vars.set("X", "42").unwrap();
let word = Word {
parts: vec![WordPart::DoubleQuoted(vec![
WordPart::Literal("value=".to_string()),
WordPart::Parameter(ParamExpr::Simple("X".to_string())),
])],
};
assert_eq!(expand_word_to_string(&mut env, &word).unwrap(), "value=42");
}
#[test]
fn test_param_default() {
let mut env = make_env();
let word = Word {
parts: vec![WordPart::Parameter(ParamExpr::Default {
name: "UNSET_VAR".to_string(),
word: Some(Word::literal("default")),
null_check: false,
})],
};
assert_eq!(expand_word_to_string(&mut env, &word).unwrap(), "default");
env.vars.set("UNSET_VAR", "actual").unwrap();
assert_eq!(expand_word_to_string(&mut env, &word).unwrap(), "actual");
}
#[test]
fn test_param_default_null_check() {
let mut env = make_env();
env.vars.set("EMPTY_VAR", "").unwrap();
let word = Word {
parts: vec![WordPart::Parameter(ParamExpr::Default {
name: "EMPTY_VAR".to_string(),
word: Some(Word::literal("fallback")),
null_check: true,
})],
};
assert_eq!(expand_word_to_string(&mut env, &word).unwrap(), "fallback");
let word2 = Word {
parts: vec![WordPart::Parameter(ParamExpr::Default {
name: "EMPTY_VAR".to_string(),
word: Some(Word::literal("fallback")),
null_check: false,
})],
};
assert_eq!(expand_word_to_string(&mut env, &word2).unwrap(), "");
}
}