use super::*;
use crate::ast::{ParameterExpansion, Range};
use crate::embed::{DiagnosticCategory, DiagnosticKind};
use crate::parser::Parser;
use crate::parser::word::here_document_line;
use std::thread::{self, JoinHandle};
use super::frontend::{emit_diagnostic, range_for_line};
fn emit_expand_error(state: &ShellState, code: &'static str, message: &str, range: Option<Range>) {
emit_diagnostic(
state,
DiagnosticKind::Error,
DiagnosticCategory::Expansion,
code,
message,
None,
range,
);
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) enum ExpansionMode {
CommandName,
Argument,
AssignmentValue { assignment_name: String },
RedirectionTarget,
ForWordList,
CaseWord,
CasePattern,
ParameterRemovalPattern,
ArithmeticText,
HereDocument,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct ExpandedField {
atoms: Vec<ExpandedAtom>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct ExpansionResult {
fields: Vec<ExpandedField>,
last_command_substitution_status: Option<i32>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct ExpandedAtom {
value: char,
glob_pattern: String,
split_candidate: bool,
}
struct PresplitField {
field: ExpandedField,
preserve_empty: bool,
}
impl PresplitField {
fn new(field: ExpandedField, preserve_empty: bool) -> Self {
Self {
field,
preserve_empty,
}
}
fn append(&mut self, other: Self) {
self.field.append(other.field);
self.preserve_empty |= other.preserve_empty;
}
}
impl ExpandedField {
fn active(value: &str) -> Self {
let mut field = Self::default();
for ch in value.chars() {
field.push_active_char(ch);
}
field
}
fn literal(value: &str) -> Self {
let mut field = Self::default();
for ch in value.chars() {
field.push_literal_char(ch);
}
field
}
fn from_unquoted_source(
source: &str,
expected_value: &str,
split_unquoted_literals: bool,
) -> Self {
let mut field = Self::default();
let mut parsed_value = String::new();
let mut chars = source.chars();
while let Some(ch) = chars.next() {
if ch != '\\' {
field.push_unquoted_literal_char(ch, split_unquoted_literals);
parsed_value.push(ch);
continue;
}
match chars.next() {
Some('\n') => {}
Some(escaped) => {
field.push_literal_char(escaped);
parsed_value.push(escaped);
}
None => {
field.push_literal_char('\\');
parsed_value.push('\\');
}
}
}
if parsed_value == expected_value {
field
} else {
Self::unquoted_literal(expected_value, split_unquoted_literals)
}
}
fn unquoted_literal(value: &str, split_unquoted_literals: bool) -> Self {
let mut field = Self::default();
for ch in value.chars() {
field.push_unquoted_literal_char(ch, split_unquoted_literals);
}
field
}
fn append(&mut self, other: Self) {
self.atoms.extend(other.atoms);
}
fn value(&self) -> String {
self.atoms.iter().map(|atom| atom.value).collect()
}
fn glob_pattern(&self) -> String {
self.atoms
.iter()
.map(|atom| atom.glob_pattern.as_str())
.collect()
}
fn contains_unescaped_glob(&self) -> bool {
source_contains_unescaped_glob(&self.glob_pattern())
}
fn is_empty(&self) -> bool {
self.atoms.is_empty()
}
fn apply_tilde_expansion(self, state: &ShellState) -> Self {
let value = self.value();
let Some((replacement, consumed_bytes)) = tilde_expansion_prefix(state, &value) else {
return self;
};
let mut expanded = Self::literal(&replacement);
expanded.append_suffix_after_bytes(self, consumed_bytes);
expanded
}
fn append_suffix_after_bytes(&mut self, field: Self, mut consumed_bytes: usize) {
for atom in field.atoms {
let atom_len = atom.value.len_utf8();
if consumed_bytes >= atom_len {
consumed_bytes -= atom_len;
} else {
self.atoms.push(atom);
}
}
}
fn push_split_candidate_char(&mut self, ch: char, split_candidate: bool) {
self.atoms.push(ExpandedAtom {
value: ch,
glob_pattern: ch.to_string(),
split_candidate,
});
}
fn push_active_char(&mut self, ch: char) {
self.push_split_candidate_char(ch, true);
}
fn push_unquoted_literal_char(&mut self, ch: char, split_candidate: bool) {
self.push_split_candidate_char(ch, split_candidate);
}
fn push_literal_char(&mut self, ch: char) {
let mut glob_pattern = String::new();
push_pattern_literal_char(&mut glob_pattern, ch);
self.atoms.push(ExpandedAtom {
value: ch,
glob_pattern,
split_candidate: false,
});
}
}
impl ExpansionResult {
fn into_values(self) -> Vec<String> {
self.fields.into_iter().map(|field| field.value()).collect()
}
fn into_first_value(self) -> String {
self.into_values().into_iter().next().unwrap_or_default()
}
}
fn expand_word_fields<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
word: &Word,
split_unquoted_literals: bool,
) -> Vec<ExpandedField> {
let fields = expand_word_presplit_fields(state, runtime, word, split_unquoted_literals);
split_presplit_fields(state, fields)
}
fn expand_word_presplit_fields<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
word: &Word,
split_unquoted_literals: bool,
) -> Vec<PresplitField> {
match word {
Word::String(string) if string.single_quoted() => {
vec![PresplitField::new(
ExpandedField::literal(string.value()),
string.value().is_empty(),
)]
}
Word::String(string) if string.value().is_empty() => Vec::new(),
Word::String(string) => {
let split_candidate = split_unquoted_literals && string.split_fields();
vec![PresplitField::new(
string
.source()
.map(|source| {
ExpandedField::from_unquoted_source(source, string.value(), split_candidate)
})
.unwrap_or_else(|| {
ExpandedField::unquoted_literal(string.value(), split_candidate)
}),
false,
)]
}
Word::Parameter(parameter) => expand_parameter_presplit_fields(state, runtime, parameter),
Word::Command(_) | Word::Arithmetic(_) => {
active_value_presplit_fields(expand_word_scalar(state, runtime, word))
}
Word::List(list) if list.double_quoted() => {
expand_double_quoted_list_presplit_fields(state, runtime, list.children())
}
Word::List(list) => expand_unquoted_list_presplit_fields(
state,
runtime,
list.children(),
split_unquoted_literals,
),
}
}
fn expand_parameter_presplit_fields<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
parameter: &ParameterExpansion,
) -> Vec<PresplitField> {
let value = get_parameter_value(state, parameter.name());
let is_set = value.is_some();
let is_nonempty = value.as_ref().is_some_and(|v| !v.is_empty());
let treat_as_unset = if parameter.colon() {
!is_nonempty
} else {
!is_set
};
match parameter.op() {
ParameterOp::Minus if treat_as_unset => {
expand_parameter_arg_presplit_fields(state, runtime, parameter.arg())
}
ParameterOp::Plus => {
if treat_as_unset {
Vec::new()
} else {
expand_parameter_arg_presplit_fields(state, runtime, parameter.arg())
}
}
ParameterOp::Equal if treat_as_unset => {
let Some(default) = assign_parameter_expansion_default(
state,
runtime,
parameter.name(),
parameter.arg(),
) else {
return Vec::new();
};
if parameter
.arg()
.is_some_and(word_can_be_replayed_for_parameter_fields)
{
expand_parameter_arg_presplit_fields(state, runtime, parameter.arg())
} else {
active_value_presplit_fields(default)
}
}
_ => active_value_presplit_fields(expand_parameter(
state,
runtime,
parameter.name(),
parameter.op(),
parameter.colon(),
parameter.arg(),
)),
}
}
fn expand_parameter_arg_presplit_fields<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
arg: Option<&Word>,
) -> Vec<PresplitField> {
let Some(word) = arg else {
return Vec::new();
};
let mut fields = expand_word_presplit_fields(state, runtime, word, true);
if tilde_allowed_in_word(word) {
for field in &mut fields {
field.field = std::mem::take(&mut field.field).apply_tilde_expansion(state);
}
}
fields
}
fn active_value_presplit_fields(value: String) -> Vec<PresplitField> {
if value.is_empty() {
Vec::new()
} else {
vec![PresplitField::new(ExpandedField::active(&value), false)]
}
}
fn expand_double_quoted_list_presplit_fields<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
children: &[Word],
) -> Vec<PresplitField> {
expand_double_quoted_list_fields(state, runtime, children)
.into_iter()
.map(|field| {
let preserve_empty = field.is_empty();
PresplitField::new(field, preserve_empty)
})
.collect()
}
fn expand_double_quoted_list_fields<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
children: &[Word],
) -> Vec<ExpandedField> {
if children.len() == 1 && is_quoted_at_expansion(&children[0]) {
return positional_parameter_fields(state);
}
let mut fields = vec![ExpandedField::default()];
for child in children {
let parts = expand_double_quoted_child_fields(state, runtime, child);
if parts.is_empty() {
continue;
}
append_expanded_field_parts(&mut fields, parts);
}
fields
}
fn expand_unquoted_list_presplit_fields<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
children: &[Word],
split_unquoted_literals: bool,
) -> Vec<PresplitField> {
let mut fields = vec![PresplitField::new(ExpandedField::default(), false)];
let mut produced_any = false;
for child in children {
let parts = expand_word_presplit_fields(state, runtime, child, split_unquoted_literals);
if parts.is_empty() {
continue;
}
produced_any = true;
append_presplit_field_parts(&mut fields, parts);
}
if produced_any { fields } else { Vec::new() }
}
fn split_presplit_fields(state: &ShellState, fields: Vec<PresplitField>) -> Vec<ExpandedField> {
let mut split = Vec::new();
for field in fields {
if field.field.is_empty() {
if field.preserve_empty {
split.push(ExpandedField::default());
}
continue;
}
let parts = split_expanded_field(state, field.field);
if parts.is_empty() {
if field.preserve_empty {
split.push(ExpandedField::default());
}
} else {
split.extend(parts);
}
}
split
}
fn append_expanded_field_parts(fields: &mut Vec<ExpandedField>, parts: Vec<ExpandedField>) {
let mut parts = parts.into_iter();
let first = parts.next().unwrap_or_default();
if let Some(last) = fields.last_mut() {
last.append(first);
} else {
fields.push(first);
}
fields.extend(parts);
}
fn append_presplit_field_parts(fields: &mut Vec<PresplitField>, parts: Vec<PresplitField>) {
let mut parts = parts.into_iter();
let first = parts
.next()
.unwrap_or_else(|| PresplitField::new(ExpandedField::default(), false));
if let Some(last) = fields.last_mut() {
last.append(first);
} else {
fields.push(first);
}
fields.extend(parts);
}
fn assign_parameter_expansion_default<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
name: &str,
arg: Option<&Word>,
) -> Option<String> {
if !is_assignable_parameter_name(name) {
let message = format!("{name}: cannot assign using parameter expansion");
let _ = state.stderr_fd.write_line(&message);
emit_expand_error(state, "expand.parameter_assign", &message, None);
state.record_expansion_error(1);
if !state.interactive {
state.set_exit_code(1);
}
return None;
}
let default = arg
.map(|word| expand_word_scalar(state, runtime, word))
.unwrap_or_default();
let attrib = if state.has_option(OPT_ALLEXPORT) {
VAR_EXPORT
} else {
0
};
if state.env_set(name, default.clone(), attrib) {
Some(default)
} else {
readonly_assignment_status(state, name, "", true);
None
}
}
fn word_can_be_replayed_for_parameter_fields(word: &Word) -> bool {
match word {
Word::String(_) => true,
Word::List(list) => list
.children()
.iter()
.all(word_can_be_replayed_for_parameter_fields),
Word::Parameter(parameter) => {
!matches!(parameter.op(), ParameterOp::Equal | ParameterOp::QMark)
&& parameter
.arg()
.is_none_or(word_can_be_replayed_for_parameter_fields)
}
Word::Command(_) | Word::Arithmetic(_) => false,
}
}
fn is_quoted_at_expansion(word: &Word) -> bool {
matches!(
word,
Word::Parameter(parameter)
if parameter.name() == "@" && parameter.op() == ParameterOp::None
)
}
fn is_quoted_dollar_at(word: &Word) -> bool {
matches!(
word,
Word::List(list)
if list.double_quoted()
&& list.children().len() == 1
&& is_quoted_at_expansion(&list.children()[0])
)
}
fn split_expanded_field(state: &ShellState, field: ExpandedField) -> Vec<ExpandedField> {
let ifs = state.env_get("IFS").unwrap_or(" \t\n");
split_expanded_field_with_ifs(ifs, field)
}
fn split_expanded_field_with_ifs(ifs: &str, field: ExpandedField) -> Vec<ExpandedField> {
if ifs.is_empty() {
return vec![field];
}
fn is_ifs_whitespace(ifs: &str, ch: char) -> bool {
ifs.contains(ch) && ch.is_whitespace()
}
fn is_ifs_non_whitespace(ifs: &str, ch: char) -> bool {
ifs.contains(ch) && !ch.is_whitespace()
}
let mut fields = Vec::new();
let mut current = ExpandedField::default();
let mut at_beginning = true;
let mut non_whitespace_delimiter_run = 0usize;
for atom in field.atoms {
if atom.split_candidate && is_ifs_whitespace(ifs, atom.value) {
if !current.is_empty() {
fields.push(std::mem::take(&mut current));
at_beginning = false;
non_whitespace_delimiter_run = 0;
}
continue;
}
if atom.split_candidate && is_ifs_non_whitespace(ifs, atom.value) {
if !current.is_empty() {
fields.push(std::mem::take(&mut current));
at_beginning = false;
non_whitespace_delimiter_run = 1;
} else if at_beginning || non_whitespace_delimiter_run > 0 {
fields.push(ExpandedField::default());
at_beginning = false;
non_whitespace_delimiter_run += 1;
} else {
non_whitespace_delimiter_run = 1;
}
} else {
current.atoms.push(atom);
at_beginning = false;
non_whitespace_delimiter_run = 0;
}
}
if !current.is_empty() {
fields.push(current);
}
fields
}
fn positional_parameter_fields(state: &ShellState) -> Vec<ExpandedField> {
state
.variable_store
.frame
.iter()
.skip(1)
.map(|field| ExpandedField::literal(field))
.collect()
}
fn expand_parameter_special_fields<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
name: &str,
op: ParameterOp,
colon: bool,
arg: Option<&Word>,
) -> Option<Vec<ExpandedField>> {
let arg_word = arg?;
if !is_quoted_dollar_at(arg_word) {
return None;
}
let value = get_parameter_value(state, name);
let is_set = value.is_some();
let is_nonempty = value.as_ref().is_some_and(|v| !v.is_empty());
let treat_as_unset = if colon { !is_nonempty } else { !is_set };
match op {
ParameterOp::Minus if treat_as_unset => Some(positional_parameter_fields(state)),
ParameterOp::Plus if !treat_as_unset => Some(positional_parameter_fields(state)),
ParameterOp::Equal if treat_as_unset => {
if assign_parameter_expansion_default(state, runtime, name, arg).is_some() {
Some(positional_parameter_fields(state))
} else {
Some(Vec::new())
}
}
_ => None,
}
}
fn expand_double_quoted_child_fields<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
word: &Word,
) -> Vec<ExpandedField> {
match word {
Word::Parameter(parameter) => {
if let Some(fields) = expand_parameter_special_fields(
state,
runtime,
parameter.name(),
parameter.op(),
parameter.colon(),
parameter.arg(),
) {
fields
} else if parameter.name() == "@" && parameter.op() == ParameterOp::None {
positional_parameter_fields(state)
} else if parameter.name() == "*" && parameter.op() == ParameterOp::None {
vec![ExpandedField::literal("ed_star_value(state))]
} else {
vec![ExpandedField::literal(&expand_word_scalar(
state, runtime, word,
))]
}
}
_ => vec![ExpandedField::literal(&expand_word_scalar(
state, runtime, word,
))],
}
}
pub(super) fn quoted_star_value(state: &ShellState) -> String {
join_positional_parameters(state)
}
fn expand_word_scalar<R: Runtime>(state: &mut ShellState, runtime: &mut R, word: &Word) -> String {
match word {
Word::String(string) => string.value().to_string(),
Word::Parameter(parameter) => expand_parameter(
state,
runtime,
parameter.name(),
parameter.op(),
parameter.colon(),
parameter.arg(),
),
Word::Command(command) => {
let output = run_command_substitution(
state,
runtime,
command.program(),
command.source(),
command.range.begin.line,
);
output.trim_end_matches('\n').to_string()
}
Word::Arithmetic(arithm) => {
let _mode = ExpansionMode::ArithmeticText;
match eval_arithmetic_expansion(state, runtime, arithm.body()) {
Ok(val) => val.to_string(),
Err(message) => {
let rendered = format!("arithmetic expansion: {message}");
let _ = state.stderr_fd.write_line(&rendered);
emit_expand_error(state, "expand.arithmetic", &rendered, Some(arithm.range));
state.record_expansion_error(2);
if !state.interactive {
state.set_exit_code(2);
}
String::new()
}
}
}
Word::List(list) => {
let mut result = String::new();
for child in list.children() {
result.push_str(&expand_word_scalar(state, runtime, child));
}
result
}
}
}
fn expand_parameter<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
name: &str,
op: ParameterOp,
colon: bool,
arg: Option<&Word>,
) -> String {
let value = get_parameter_value(state, name);
if op == ParameterOp::None
&& value.is_none()
&& state.has_option(OPT_NOUNSET)
&& !is_nounset_exempt_parameter_name(name)
{
let message = format!("{name}: parameter not set");
let _ = state.stderr_fd.write_line(&message);
emit_expand_error(state, "expand.parameter_unset", &message, None);
state.record_expansion_error(1);
if !state.interactive {
state.set_exit_code(1);
}
return String::new();
}
let is_set = value.is_some();
let is_nonempty = value.as_ref().is_some_and(|v| !v.is_empty());
let val = value.unwrap_or_default();
let treat_as_unset = if colon { !is_nonempty } else { !is_set };
match op {
ParameterOp::None => val,
ParameterOp::LeadingHash => {
if matches!(name, "@" | "*") {
state
.variable_store
.frame
.len()
.saturating_sub(1)
.to_string()
} else {
val.chars().count().to_string()
}
}
ParameterOp::Minus => {
if treat_as_unset {
arg.map(|w| expand_word_scalar(state, runtime, w))
.unwrap_or_default()
} else {
val
}
}
ParameterOp::Equal => {
if treat_as_unset {
assign_parameter_expansion_default(state, runtime, name, arg).unwrap_or_default()
} else {
val
}
}
ParameterOp::QMark => {
if treat_as_unset {
let msg = arg
.map(|w| expand_word_scalar(state, runtime, w))
.unwrap_or_else(|| "parameter null or not set".to_string());
let message = format!("{name}: {msg}");
let _ = state.stderr_fd.write_line(&message);
emit_expand_error(state, "expand.parameter_error", &message, None);
state.record_expansion_error(1);
if !state.interactive {
state.set_exit_code(1);
}
String::new()
} else {
val
}
}
ParameterOp::Plus => {
if treat_as_unset {
String::new()
} else {
arg.map(|w| expand_word_scalar(state, runtime, w))
.unwrap_or_default()
}
}
ParameterOp::Percent => {
let pat = arg
.map(|w| expand_parameter_removal_pattern(state, runtime, w))
.unwrap_or_default();
strip_suffix(&val, &pat, false)
}
ParameterOp::DoublePercent => {
let pat = arg
.map(|w| expand_parameter_removal_pattern(state, runtime, w))
.unwrap_or_default();
strip_suffix(&val, &pat, true)
}
ParameterOp::Hash => {
let pat = arg
.map(|w| expand_parameter_removal_pattern(state, runtime, w))
.unwrap_or_default();
strip_prefix(&val, &pat, false)
}
ParameterOp::DoubleHash => {
let pat = arg
.map(|w| expand_parameter_removal_pattern(state, runtime, w))
.unwrap_or_default();
strip_prefix(&val, &pat, true)
}
}
}
fn parse_arithmetic_text(expr: &str) -> Result<ArithmExpr, String> {
crate::parser::arithm::parse_arithm_expr_strict(expr).map_err(|err| {
format!(
"{}: \"{}\"",
err.message,
expr[err.position.min(expr.len())..].trim()
)
})
}
fn eval_arithmetic_expansion<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
expr: &ArithmExpr,
) -> Result<i64, String> {
let ArithmExpr::Raw(raw) = expr else {
return eval_arithm(state, expr);
};
let text_word = here_document_line(raw.expr());
let expanded = expand_word_with_mode(state, runtime, &text_word, ExpansionMode::ArithmeticText)
.into_first_value();
let parsed = parse_arithmetic_text(&expanded)?;
eval_arithm(state, &parsed)
}
pub(super) fn is_special_parameter_name(name: &str) -> bool {
matches!(name, "@" | "*" | "#" | "?" | "-" | "$" | "!")
|| name == "0"
|| name.parse::<usize>().is_ok()
}
pub(super) fn is_nounset_exempt_parameter_name(name: &str) -> bool {
matches!(name, "@" | "*" | "#" | "?" | "-" | "$") || name == "0"
}
fn is_assignable_parameter_name(name: &str) -> bool {
if is_special_parameter_name(name) || name == "!" || name.parse::<usize>().is_ok() {
return false;
}
true
}
pub(super) fn word_begin_line(word: &Word) -> Option<u32> {
match word {
Word::String(string) => Some(string.range.begin.line),
Word::Command(command) => Some(command.range.begin.line),
Word::Arithmetic(arithm) => Some(arithm.range.begin.line),
Word::Parameter(parameter) => Some(parameter.dollar_pos().line),
Word::List(list) => list.children().first().and_then(word_begin_line),
}
}
pub(super) fn word_contains_command_substitution(word: &Word) -> bool {
match word {
Word::Command(_) => true,
Word::Arithmetic(arithm) => arithmetic_contains_command_substitution(arithm.body()),
Word::List(list) => list
.children()
.iter()
.any(word_contains_command_substitution),
Word::Parameter(parameter) => parameter
.arg()
.is_some_and(word_contains_command_substitution),
Word::String(_) => false,
}
}
fn arithmetic_contains_command_substitution(expr: &ArithmExpr) -> bool {
match expr {
ArithmExpr::Raw(raw) => {
let text_word = here_document_line(raw.expr());
word_contains_command_substitution(&text_word)
}
ArithmExpr::BinOp(binary) => {
arithmetic_contains_command_substitution(binary.left())
|| arithmetic_contains_command_substitution(binary.right())
}
ArithmExpr::UnOp(unary) => arithmetic_contains_command_substitution(unary.operand()),
ArithmExpr::Cond(cond) => {
arithmetic_contains_command_substitution(cond.cond())
|| arithmetic_contains_command_substitution(cond.then_branch())
|| arithmetic_contains_command_substitution(cond.else_branch())
}
ArithmExpr::Assign(assign) => arithmetic_contains_command_substitution(assign.value()),
ArithmExpr::Literal(_) | ArithmExpr::Variable(_) => false,
}
}
pub(super) fn get_parameter_value(state: &ShellState, name: &str) -> Option<String> {
match name {
"@" | "*" => Some(join_positional_parameters(state)),
"#" => Some((state.variable_store.frame.len().saturating_sub(1)).to_string()),
"?" => Some(state.last_status.to_string()),
"-" => Some(format_options_with_schema(
state.options,
&state.definition.option_schema,
)),
"$" => Some(state.logical_shell_pid().to_string()),
"!" => state.job_table.last_bg_pid.map(|pid| pid.to_string()),
"0" => state.variable_store.frame.first().cloned(),
_ => {
if let Ok(n) = name.parse::<usize>() {
state.variable_store.frame.get(n).cloned()
} else {
state.env_get(name).map(String::from)
}
}
}
}
fn join_positional_parameters(state: &ShellState) -> String {
let params: Vec<&str> = state
.variable_store
.frame
.iter()
.skip(1)
.map(|s| s.as_str())
.collect();
let sep = match state.env_get("IFS").unwrap_or(" \t\n").chars().next() {
Some(sep) => sep.to_string(),
None => String::new(),
};
params.join(&sep)
}
#[cfg(all(test, feature = "test-support", feature = "unix-runtime"))]
pub(super) fn split_fields(state: &ShellState, s: &str) -> Vec<String> {
let ifs = state.env_get("IFS").unwrap_or(" \t\n");
split_expanded_field_with_ifs(ifs, ExpandedField::active(s))
.into_iter()
.map(|field| field.value())
.collect()
}
pub(super) fn expand_case_pattern<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
word: &Word,
) -> String {
expand_word_with_mode(state, runtime, word, ExpansionMode::CasePattern).into_first_value()
}
fn expand_parameter_removal_pattern<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
word: &Word,
) -> String {
expand_word_with_mode(state, runtime, word, ExpansionMode::ParameterRemovalPattern)
.into_first_value()
}
fn expand_case_pattern_inner<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
word: &Word,
in_double_quotes: bool,
) -> String {
match word {
Word::String(string) if in_double_quotes || string.single_quoted() => {
escape_pattern_literal(string.value())
}
Word::String(string) => string
.source()
.map(pattern_from_unquoted_string_source)
.unwrap_or_else(|| string.value().to_string()),
Word::List(list) => {
let quoted = in_double_quotes || list.double_quoted();
list.children()
.iter()
.map(|child| expand_case_pattern_inner(state, runtime, child, quoted))
.collect::<String>()
}
Word::Parameter(_) | Word::Command(_) | Word::Arithmetic(_) => {
let expanded = expand_word_scalar(state, runtime, word);
if in_double_quotes {
escape_pattern_literal(&expanded)
} else {
expanded
}
}
}
}
fn pattern_from_unquoted_string_source(source: &str) -> String {
let mut pattern = String::new();
let mut chars = source.chars();
while let Some(ch) = chars.next() {
if ch != '\\' {
pattern.push(ch);
continue;
}
match chars.next() {
Some('\n') => {}
Some(escaped) => push_pattern_literal_char(&mut pattern, escaped),
None => push_pattern_literal_char(&mut pattern, '\\'),
}
}
pattern
}
fn escape_pattern_literal(value: &str) -> String {
let mut escaped = String::new();
for ch in value.chars() {
push_pattern_literal_char(&mut escaped, ch);
}
escaped
}
fn push_pattern_literal_char(out: &mut String, ch: char) {
if matches!(ch, '*' | '?' | '[' | '\\') {
out.push('\\');
}
out.push(ch);
}
fn readonly_assignment_status(
state: &mut ShellState,
name: &str,
context: &str,
expansion_context: bool,
) -> i32 {
let message = if context.is_empty() {
format!("{name}: readonly variable")
} else {
format!("{context}: {name}: readonly variable")
};
shell_errln(state, &message);
if expansion_context {
state.record_expansion_error(1);
if !state.interactive {
state.exit_code = 1;
}
}
1
}
pub(super) fn pattern_match(pattern: &str, text: &str) -> bool {
let pattern: Vec<char> = pattern.chars().collect();
let text: Vec<char> = text.chars().collect();
pattern_match_inner(&pattern, &text)
}
fn pattern_match_inner(pat: &[char], txt: &[char]) -> bool {
let mut pat_idx = 0usize;
let mut text_idx = 0usize;
let mut last_star: Option<(usize, usize)> = None;
while text_idx < txt.len() {
if pat_idx < pat.len() && pat[pat_idx] == '*' {
while pat_idx < pat.len() && pat[pat_idx] == '*' {
pat_idx += 1;
}
if pat_idx == pat.len() {
return true;
}
last_star = Some((pat_idx, text_idx));
continue;
}
if let Some(atom) = match_pattern_atom(pat, pat_idx, txt[text_idx])
&& atom.matched
{
pat_idx += atom.consumed;
text_idx += 1;
continue;
}
if let Some((star_pat_idx, star_text_idx)) = last_star.as_mut()
&& *star_text_idx < txt.len()
{
*star_text_idx += 1;
pat_idx = *star_pat_idx;
text_idx = *star_text_idx;
continue;
}
return false;
}
while pat_idx < pat.len() && pat[pat_idx] == '*' {
pat_idx += 1;
}
pat_idx == pat.len()
}
struct PatternAtomMatch {
matched: bool,
consumed: usize,
}
fn match_pattern_atom(pat: &[char], pat_idx: usize, ch: char) -> Option<PatternAtomMatch> {
let atom = *pat.get(pat_idx)?;
let atom = match atom {
'*' => return None,
'?' => PatternAtomMatch {
matched: true,
consumed: 1,
},
'[' => match match_char_bracket_class(&pat[pat_idx..], ch) {
Some((matched, consumed)) => PatternAtomMatch { matched, consumed },
None => PatternAtomMatch {
matched: ch == '[',
consumed: 1,
},
},
'\\' if pat_idx + 1 < pat.len() => PatternAtomMatch {
matched: ch == pat[pat_idx + 1],
consumed: 2,
},
literal => PatternAtomMatch {
matched: ch == literal,
consumed: 1,
},
};
Some(atom)
}
fn literal_pattern(pattern: &str) -> Option<String> {
let chars: Vec<char> = pattern.chars().collect();
let mut literal = String::new();
let mut idx = 0usize;
while idx < chars.len() {
match chars[idx] {
'*' | '?' => return None,
'[' => {
if valid_bracket_class(&chars[idx..]) {
return None;
}
literal.push('[');
idx += 1;
}
'\\' if idx + 1 < chars.len() => {
literal.push(chars[idx + 1]);
idx += 2;
}
ch => {
literal.push(ch);
idx += 1;
}
}
}
Some(literal)
}
fn valid_bracket_class(pat: &[char]) -> bool {
match_char_bracket_class(pat, '\0').is_some()
}
fn match_char_bracket_class(pat: &[char], ch: char) -> Option<(bool, usize)> {
if pat.first().copied() != Some('[') {
return None;
}
let mut i = 1usize;
if i >= pat.len() {
return None;
}
let mut negate = false;
if pat[i] == '!' || pat[i] == '^' {
negate = true;
i += 1;
}
let mut matched = false;
while i < pat.len() {
if pat[i] == ']' && i > 1 {
return Some(((if negate { !matched } else { matched }), i + 1));
}
if i + 2 < pat.len() && pat[i + 1] == '-' && pat[i + 2] != ']' {
let start = pat[i];
let end = pat[i + 2];
if start <= ch && ch <= end {
matched = true;
}
i += 3;
continue;
}
if pat[i] == ch {
matched = true;
}
i += 1;
}
None
}
#[cfg(all(test, feature = "test-support", feature = "unix-runtime"))]
pub(super) fn match_bracket_class(pat: &[u8], byte: u8) -> Option<(bool, usize)> {
if pat.first().copied() != Some(b'[') {
return None;
}
let mut i = 1usize;
if i >= pat.len() {
return None;
}
let mut negate = false;
if pat[i] == b'!' || pat[i] == b'^' {
negate = true;
i += 1;
}
let mut matched = false;
while i < pat.len() {
if pat[i] == b']' && i > 1 {
return Some(((if negate { !matched } else { matched }), i + 1));
}
if i + 2 < pat.len() && pat[i + 1] == b'-' && pat[i + 2] != b']' {
let start = pat[i];
let end = pat[i + 2];
if start <= byte && byte <= end {
matched = true;
}
i += 3;
continue;
}
if pat[i] == byte {
matched = true;
}
i += 1;
}
None
}
pub(super) fn strip_suffix(value: &str, pattern: &str, longest: bool) -> String {
if let Some(literal) = literal_pattern(pattern) {
return value.strip_suffix(&literal).unwrap_or(value).to_string();
}
let indices = value
.char_indices()
.map(|(i, _)| i)
.chain(std::iter::once(value.len()));
if longest {
for i in indices {
if pattern_match(pattern, &value[i..]) {
return value[..i].to_string();
}
}
} else {
let indices: Vec<usize> = indices.collect();
for i in indices.into_iter().rev() {
if pattern_match(pattern, &value[i..]) {
return value[..i].to_string();
}
}
}
value.to_string()
}
pub(super) fn strip_prefix(value: &str, pattern: &str, longest: bool) -> String {
if let Some(literal) = literal_pattern(pattern) {
return value.strip_prefix(&literal).unwrap_or(value).to_string();
}
if longest {
if pattern_match(pattern, value) {
return String::new();
}
for (i, _) in value.char_indices().rev() {
if pattern_match(pattern, &value[..i]) {
return value[i..].to_string();
}
}
} else {
for (i, _) in value.char_indices() {
if pattern_match(pattern, &value[..i]) {
return value[i..].to_string();
}
}
if pattern_match(pattern, value) {
return String::new();
}
}
value.to_string()
}
pub(super) fn expand_tilde(state: &ShellState, s: &str) -> String {
if let Some((replacement, consumed_bytes)) = tilde_expansion_prefix(state, s) {
return format!("{replacement}{}", &s[consumed_bytes..]);
}
s.to_string()
}
fn tilde_expansion_prefix(state: &ShellState, s: &str) -> Option<(String, usize)> {
let rest = s.strip_prefix('~')?;
if (rest.is_empty() || rest.starts_with('/'))
&& let Some(home) = state.env_get("HOME")
{
return Some((home.to_string(), 1));
}
let (user, consumed_bytes) = match rest.find('/') {
Some(slash_pos) => (&rest[..slash_pos], 1 + slash_pos),
None => (rest, s.len()),
};
if !user.is_empty()
&& let Some(home) = user_home_dir(user)
{
return Some((home, consumed_bytes));
}
None
}
fn user_home_dir(user: &str) -> Option<String> {
let c_user = CString::new(user).ok()?;
let passwd = unsafe { libc::getpwnam(c_user.as_ptr()) };
if passwd.is_null() {
return None;
}
let dir = unsafe { (*passwd).pw_dir };
if dir.is_null() {
return None;
}
Some(
unsafe { CStr::from_ptr(dir) }
.to_string_lossy()
.into_owned(),
)
}
fn read_command_substitution_output(
read_fd: sys::FileDescriptor,
max_bytes: usize,
) -> JoinHandle<io::Result<String>> {
thread::spawn(move || {
let result = read_fd.read_to_string_with_limit(max_bytes);
read_fd.close();
result
})
}
fn collect_command_substitution_output(
reader: JoinHandle<io::Result<String>>,
) -> io::Result<String> {
reader.join().unwrap_or_else(|_| {
Err(io::Error::other(
"command substitution reader thread panicked",
))
})
}
fn run_command_substitution<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
program: &Program,
source_text: Option<&str>,
source_line: u32,
) -> String {
const MAX_COMMAND_SUBSTITUTION_BYTES: usize = 1024 * 1024;
let pipe = match sys::OsPipe::new() {
Ok(p) => p,
Err(_) => return String::new(),
};
let text = source_text
.map(str::to_owned)
.unwrap_or_else(|| program.to_canonical());
let aliases = state.aliases_snapshot();
let aliases_for_error = state.aliases_snapshot();
let reparse_text = format!("{text})");
let mut parser = Parser::from_string(&reparse_text);
parser.set_alias_inserted_rparen_is_data(true);
crate::parser::configure_parser_for_language(&mut parser, &state.definition.language);
parser.set_alias_func(crate::parser::AliasFn::new(move |name| {
aliases.get(name).cloned()
}));
let reparsed = match parser.parse_command_substitution_reparse() {
Ok(program) => program,
Err(err) => {
let source = state
.current_source
.as_deref()
.unwrap_or(state.shell_name());
let message = format!(
"{source}: command substitution: line {source_line}: {}",
err.message
);
let _ = state.stderr_fd.write_line(&message);
emit_expand_error(
state,
"expand.command_substitution_parse",
&err.message,
Some(range_for_line(source_line)),
);
let excerpt = text
.split_whitespace()
.next()
.and_then(|name| aliases_for_error.get(name))
.map(|expanded| format!("{expanded} "))
.unwrap_or_else(|| {
let mut excerpt = text[..err.pos.offset.min(text.len())].to_string();
if let Some(ch) = text[err.pos.offset.min(text.len())..].chars().next() {
excerpt.push(ch);
excerpt.push(' ');
}
excerpt
});
let _ = state.stderr_fd.write_line(&format!(
"{source}: command substitution: line {source_line}: `{excerpt}'"
));
state.set_last_status(1);
state.record_expansion_error(1);
if !state.interactive {
state.exit_code = 1;
}
pipe.read_fd.close();
pipe.write_fd.close();
return String::new();
}
};
let mut sub_runtime = match runtime.fork() {
Ok(runtime) => runtime,
Err(err) => {
let source = state
.current_source
.as_deref()
.unwrap_or(state.shell_name());
let message = format!("{source}: command substitution: failed to fork runtime: {err}");
let _ = state.stderr_fd.write_line(&message);
emit_expand_error(
state,
"expand.command_substitution_fork",
&message,
Some(range_for_line(source_line)),
);
state.set_last_status(1);
state.record_expansion_error(1);
if !state.interactive {
state.exit_code = 1;
}
pipe.read_fd.close();
pipe.write_fd.close();
return String::new();
}
};
let mut child_state = state.fork_for(ExecutionContextKind::CommandSubstitution);
child_state.stdout_fd = pipe.write_fd;
let reader = read_command_substitution_output(pipe.read_fd, MAX_COMMAND_SUBSTITUTION_BYTES);
let _process_globals =
super::exec::program_may_mutate_current_process_globals(state, &reparsed)
.then(|| ProcessGlobalGuard::capture_for(&child_state));
let parent_last_status = state.last_status;
let body_status = run_program(&mut child_state, &mut sub_runtime, &reparsed);
shell_traps::run_exit_trap(&mut child_state, &mut sub_runtime);
let status = if child_state.exit_code >= 0 {
child_state.exit_code
} else {
body_status
};
state.record_command_substitution_status(status);
state.set_last_status(parent_last_status);
pipe.write_fd.close();
let mut output = match collect_command_substitution_output(reader) {
Ok(output) => output,
Err(err) => {
let message = format!(
"command substitution: output exceeds {MAX_COMMAND_SUBSTITUTION_BYTES} bytes"
);
let _ = state.stderr_fd.write_line(&message);
emit_expand_error(
state,
"expand.command_substitution_output_too_large",
&format!("{message}: {err}"),
Some(range_for_line(source_line)),
);
state.record_expansion_error(1);
if !state.interactive {
state.exit_code = 1;
}
return String::new();
}
};
while output.ends_with('\n') {
output.pop();
}
output
}
pub(super) fn expand_tilde_assignment(state: &ShellState, value: &str) -> String {
value
.split(':')
.map(|segment| {
if segment.starts_with('~') {
expand_tilde(state, segment)
} else {
segment.to_string()
}
})
.collect::<Vec<_>>()
.join(":")
}
fn expand_word_with_mode<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
word: &Word,
mode: ExpansionMode,
) -> ExpansionResult {
let fields = match &mode {
ExpansionMode::CommandName | ExpansionMode::Argument | ExpansionMode::ForWordList => {
let mut fields = Vec::new();
append_expanded_word_fields(state, runtime, &mut fields, word);
fields
}
ExpansionMode::AssignmentValue { assignment_name } => {
let _ = assignment_name;
let value = expand_word_scalar(state, runtime, word);
let value = if assignment_tilde_allowed(word) {
expand_tilde_assignment(state, &value)
} else {
value
};
vec![ExpandedField::literal(&value)]
}
ExpansionMode::RedirectionTarget => {
let value = expand_word_scalar(state, runtime, word);
let value = if tilde_allowed_in_word(word) {
expand_tilde(state, &value)
} else {
value
};
vec![ExpandedField::literal(&value)]
}
ExpansionMode::CaseWord | ExpansionMode::ArithmeticText | ExpansionMode::HereDocument => {
vec![ExpandedField::literal(&expand_word_scalar(
state, runtime, word,
))]
}
ExpansionMode::CasePattern | ExpansionMode::ParameterRemovalPattern => {
vec![ExpandedField::literal(&expand_case_pattern_inner(
state, runtime, word, false,
))]
}
};
ExpansionResult {
fields,
last_command_substitution_status: state.command_substitution_status,
}
}
pub(super) fn expand_command_name<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
name_word: &Word,
) -> Vec<String> {
let result = expand_word_with_mode(state, runtime, name_word, ExpansionMode::CommandName);
let _ = result.last_command_substitution_status;
result.into_values()
}
fn append_expanded_word_fields<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
fields: &mut Vec<ExpandedField>,
word: &Word,
) {
let expanded = expand_word_fields(state, runtime, word, false);
let allow_tilde = tilde_allowed_in_word(word);
for field in expanded {
let field = if allow_tilde {
field.apply_tilde_expansion(state)
} else {
field
};
if !state.has_option(OPT_NOGLOB) && field.contains_unescaped_glob() {
let value = field.value();
let matches = sys::glob_expand(&field.glob_pattern(), &state.path_state.cwd);
if matches.is_empty() {
fields.push(ExpandedField::literal(&value));
} else {
fields.extend(
matches
.into_iter()
.map(|value| ExpandedField::literal(&value)),
);
}
} else {
fields.push(field);
}
}
}
fn tilde_allowed_in_word(word: &Word) -> bool {
match word {
Word::String(string) => {
!string.single_quoted()
&& string.split_fields()
&& string.value().starts_with('~')
&& string
.source()
.map(source_allows_tilde_expansion)
.unwrap_or(true)
}
Word::List(list) if !list.double_quoted() => match list.children().first() {
Some(Word::String(string)) if !string.single_quoted() && string.split_fields() => {
string.value().starts_with('~')
&& string
.source()
.map(source_allows_tilde_expansion)
.unwrap_or(true)
&& !(string.value() == "~" && list.children().len() > 1)
}
_ => false,
},
_ => false,
}
}
fn source_allows_tilde_expansion(source: &str) -> bool {
let mut chars = source.chars();
if chars.next() != Some('~') {
return false;
}
for ch in chars {
match ch {
'/' => return true,
'\\' | '\'' | '"' | '$' | '`' => return false,
_ if ch.is_whitespace() => return false,
_ => {}
}
}
true
}
fn source_contains_unescaped_glob(source: &str) -> bool {
let mut escaped = false;
for ch in source.chars() {
if escaped {
escaped = false;
continue;
}
match ch {
'\\' => escaped = true,
'*' | '?' | '[' => return true,
_ => {}
}
}
false
}
pub(super) trait IntoCommandArgv {
fn into_command_argv(self) -> Vec<String>;
}
impl IntoCommandArgv for String {
fn into_command_argv(self) -> Vec<String> {
vec![self]
}
}
impl IntoCommandArgv for Vec<String> {
fn into_command_argv(self) -> Vec<String> {
self
}
}
pub(super) fn build_argv<R: Runtime, A: IntoCommandArgv>(
state: &mut ShellState,
runtime: &mut R,
argv: A,
arguments: &[Word],
) -> Vec<String> {
let mut argv = argv.into_command_argv();
for arg in arguments {
argv.extend(expand_argument_word(state, runtime, arg));
}
argv
}
fn literal_assignment_name_prefix(word: &Word) -> Option<String> {
match word {
Word::String(string) => string
.value()
.split_once('=')
.map(|(name, _)| name.to_string()),
Word::List(list) if !list.double_quoted() => {
let mut prefix = String::new();
for child in list.children() {
match child {
Word::String(string) => {
if let Some((before_eq, _)) = string.value().split_once('=') {
prefix.push_str(before_eq);
return Some(prefix);
}
prefix.push_str(string.value());
}
_ => return None,
}
}
None
}
_ => None,
}
}
fn declaration_assignment_tilde_allowed(word: &Word) -> bool {
match word {
Word::String(string) => {
let Some((_, value)) = string.value().split_once('=') else {
return false;
};
!string.single_quoted()
&& value.starts_with('~')
&& string
.source()
.and_then(|source| source.split_once('=').map(|(_, value)| value))
.map(source_allows_tilde_expansion)
.unwrap_or(true)
}
Word::List(list) if !list.double_quoted() => {
let mut saw_eq = false;
for child in list.children() {
if !saw_eq {
match child {
Word::String(string) => {
if let Some((_, suffix)) = string.value().split_once('=') {
saw_eq = true;
if !suffix.is_empty() {
return !string.single_quoted()
&& suffix.starts_with('~')
&& string
.source()
.and_then(|source| {
source.split_once('=').map(|(_, value)| value)
})
.map(source_allows_tilde_expansion)
.unwrap_or(true);
}
}
}
_ => return false,
}
continue;
}
return assignment_tilde_allowed(child);
}
false
}
_ => false,
}
}
pub(super) fn build_declaration_argv<R: Runtime, A: IntoCommandArgv>(
state: &mut ShellState,
runtime: &mut R,
argv: A,
arguments: &[Word],
) -> Vec<String> {
let mut argv = argv.into_command_argv();
for arg in arguments {
let Some(name) = literal_assignment_name_prefix(arg) else {
let mut fields = Vec::new();
append_expanded_word_fields(state, runtime, &mut fields, arg);
argv.extend(fields.into_iter().map(|field| field.value()));
continue;
};
if !shell_builtins::is_valid_identifier(&name) {
let mut fields = Vec::new();
append_expanded_word_fields(state, runtime, &mut fields, arg);
argv.extend(fields.into_iter().map(|field| field.value()));
continue;
}
let expanded = expand_word_scalar(state, runtime, arg);
let Some((expanded_name, value)) = expanded.split_once('=') else {
argv.push(expanded);
continue;
};
let value = if declaration_assignment_tilde_allowed(arg) {
expand_tilde_assignment(state, value)
} else {
value.to_string()
};
argv.push(format!("{expanded_name}={value}"));
}
argv
}
pub(super) fn expand_argument_word<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
word: &Word,
) -> Vec<String> {
expand_word_with_mode(state, runtime, word, ExpansionMode::Argument).into_values()
}
pub(super) fn expand_for_word<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
word: &Word,
) -> Vec<String> {
expand_word_with_mode(state, runtime, word, ExpansionMode::ForWordList).into_values()
}
pub(super) fn expand_redirection_target<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
word: &Word,
) -> String {
expand_word_with_mode(state, runtime, word, ExpansionMode::RedirectionTarget).into_first_value()
}
pub(super) fn expand_assignment_word<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
name: &str,
word: &Word,
) -> String {
expand_word_with_mode(
state,
runtime,
word,
ExpansionMode::AssignmentValue {
assignment_name: name.to_string(),
},
)
.into_first_value()
}
pub(super) fn expand_case_word<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
word: &Word,
) -> String {
expand_word_with_mode(state, runtime, word, ExpansionMode::CaseWord).into_first_value()
}
pub(super) fn expand_here_document_word<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
word: &Word,
) -> String {
expand_word_with_mode(state, runtime, word, ExpansionMode::HereDocument).into_first_value()
}
fn assignment_tilde_allowed(word: &Word) -> bool {
match word {
Word::String(string) => {
!string.single_quoted()
&& string
.source()
.map(source_allows_tilde_expansion)
.unwrap_or_else(|| string.value().starts_with('~'))
}
Word::List(list) if !list.double_quoted() => list
.children()
.first()
.is_some_and(assignment_tilde_allowed),
_ => false,
}
}
#[cfg(all(test, feature = "test-support", feature = "unix-runtime"))]
pub(super) fn contains_glob_chars(s: &str) -> bool {
s.contains('*') || s.contains('?') || s.contains('[')
}