use super::*;
use crate::limits::BudgetedVec;
pub(super) const COMPOUND_MARK: char = '\u{F8FF}';
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(super) enum DeclKind {
Declare,
Local,
Export,
Readonly,
}
impl DeclKind {
fn allowed_options(self) -> &'static str {
match self {
DeclKind::Declare | DeclKind::Local => "aAfFgiIlnprtux",
DeclKind::Export => "fnp",
DeclKind::Readonly => "aAfp",
}
}
fn usage(self, cmd: &str) -> String {
match self {
DeclKind::Declare => format!(
"{cmd}: usage: {cmd} [-aAfFgiIlnrtux] [name[=value] ...] or {cmd} -p [-aAfFilnrtux] [name ...]\n"
),
DeclKind::Local => "local: usage: local [option] name[=value] ...\n".to_string(),
DeclKind::Export => {
"export: usage: export [-fn] [name[=value] ...] or export -p\n".to_string()
}
DeclKind::Readonly => {
"readonly: usage: readonly [-aAf] [name[=value] ...] or readonly -p\n".to_string()
}
}
}
}
#[derive(Default, Clone, Copy)]
struct AttrFlags {
indexed: bool,
assoc: bool,
integer: bool,
nameref: bool,
readonly: bool,
export: bool,
lower: bool,
upper: bool,
}
impl AttrFlags {
fn any(&self) -> bool {
self.indexed
|| self.assoc
|| self.integer
|| self.nameref
|| self.readonly
|| self.export
|| self.lower
|| self.upper
}
}
#[derive(Default)]
struct DeclOpts {
on: AttrFlags,
off: AttrFlags,
print: bool,
functions: bool,
function_names: bool,
global: bool,
}
enum DeclValue {
Str(String),
Compound(Vec<Word>),
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(super) enum VarKind {
Scalar,
Indexed,
Assoc,
Unset,
}
pub(super) fn valid_nameref_target(s: &str) -> bool {
match s.find('[') {
Some(b) => is_valid_var_name(&s[..b]) && s.ends_with(']') && s.len() > b + 2,
None => is_valid_var_name(s),
}
}
fn operand_eq_pos(arg: &str) -> Option<usize> {
let mut depth = 0usize;
for (i, c) in arg.char_indices() {
match c {
'[' => depth += 1,
']' => depth = depth.saturating_sub(1),
'=' if depth == 0 => return Some(i),
_ => {}
}
}
None
}
pub(super) fn ansic_should_quote(s: &str) -> bool {
s.chars().any(|c| c.is_control())
}
pub(super) fn ansic_quote(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 3);
out.push_str("$'");
for c in s.chars() {
match c {
'\x07' => out.push_str("\\a"),
'\x08' => out.push_str("\\b"),
'\x1b' => out.push_str("\\E"),
'\x0c' => out.push_str("\\f"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\x0b' => out.push_str("\\v"),
'\\' => out.push_str("\\\\"),
'\'' => out.push_str("\\'"),
c if c.is_control() => {
let mut buf = [0u8; 4];
for b in c.encode_utf8(&mut buf).bytes() {
out.push_str(&format!("\\{b:03o}"));
}
}
c => out.push(c),
}
}
out.push('\'');
out
}
pub(super) fn double_quote(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
if matches!(c, '"' | '\\' | '$' | '`') {
out.push('\\');
}
out.push(c);
}
out.push('"');
out
}
pub(super) fn declare_quote(s: &str) -> String {
if ansic_should_quote(s) {
ansic_quote(s)
} else {
double_quote(s)
}
}
fn contains_shell_metas(s: &str) -> bool {
let chars: Vec<char> = s.chars().collect();
for (i, &c) in chars.iter().enumerate() {
match c {
' ' | '\t' | '\n' | '\'' | '"' | '\\' | '|' | '&' | ';' | '(' | ')' | '<' | '>'
| '!' | '{' | '}' | '*' | '[' | '?' | ']' | '^' | '$' | '`' => return true,
'~' if i == 0 || matches!(chars[i - 1], '=' | ':') => return true,
'#' if i == 0 => return true,
_ => {}
}
}
false
}
fn set_quote(s: &str) -> String {
if ansic_should_quote(s) {
ansic_quote(s)
} else if contains_shell_metas(s) {
format!("'{}'", s.replace('\'', "'\\''"))
} else {
s.to_string()
}
}
fn bash_hash(s: &str) -> u32 {
let mut h: u32 = 2_166_136_261;
for b in s.bytes() {
h = h.wrapping_mul(16_777_619);
h ^= u32::from(b);
}
h
}
pub(super) fn assoc_keys_bash_order(arr: &HashMap<String, String>) -> Vec<&String> {
let mut keys: Vec<&String> = arr.keys().collect();
keys.sort_by(|a, b| {
(bash_hash(a) & 1023)
.cmp(&(bash_hash(b) & 1023))
.then_with(|| b.cmp(a))
});
keys
}
fn format_indexed_body(arr: &HashMap<usize, String>) -> String {
let mut idx: Vec<&usize> = arr.keys().collect();
idx.sort();
let items: Vec<String> = idx
.iter()
.map(|i| format!("[{}]={}", i, declare_quote(&arr[*i])))
.collect();
format!("({})", items.join(" "))
}
fn format_assoc_body(arr: &HashMap<String, String>) -> String {
if arr.is_empty() {
return "()".to_string();
}
let mut out = String::from("(");
for k in assoc_keys_bash_order(arr) {
let key = if contains_shell_metas(k) || k.is_empty() {
double_quote(k)
} else {
k.clone()
};
out.push_str(&format!("[{}]={} ", key, declare_quote(&arr[k])));
}
out.push(')');
out
}
pub(super) fn split_keyed_word(word: &Word) -> Option<(Word, Word, bool)> {
let quoted_at = |i: usize| word.part_quoted.get(i).copied().unwrap_or(word.quoted);
match word.parts.first() {
Some(WordPart::Literal(s)) if s.starts_with('[') && !quoted_at(0) => {}
_ => return None,
}
let mut depth = 0usize;
for (pi, part) in word.parts.iter().enumerate() {
let WordPart::Literal(s) = part else {
continue;
};
if quoted_at(pi) {
continue;
}
let start = if pi == 0 { 1 } else { 0 };
if pi == 0 {
depth = 1;
}
for (ci, c) in s.char_indices().skip(start) {
match c {
'[' => depth += 1,
']' => {
depth -= 1;
if depth == 0 {
let rest = &s[ci + 1..];
let (append, value_head) = if let Some(v) = rest.strip_prefix("+=") {
(true, v)
} else if let Some(v) = rest.strip_prefix('=') {
(false, v)
} else {
return None;
};
let key_head = if pi == 0 { &s[1..ci] } else { &s[..ci] };
let mut key_parts = Vec::new();
let mut key_quoted = Vec::new();
for (i, p) in word.parts.iter().enumerate().take(pi) {
if i == 0 {
if let WordPart::Literal(l) = p {
key_parts.push(WordPart::Literal(l[1..].to_string()));
}
} else {
key_parts.push(p.clone());
}
key_quoted.push(quoted_at(i));
}
if pi == 0 || !key_head.is_empty() {
key_parts.push(WordPart::Literal(key_head.to_string()));
key_quoted.push(false);
}
let mut value_parts = Vec::new();
let mut value_quoted = Vec::new();
if !value_head.is_empty() {
value_parts.push(WordPart::Literal(value_head.to_string()));
value_quoted.push(false);
}
for (i, p) in word.parts.iter().enumerate().skip(pi + 1) {
value_parts.push(p.clone());
value_quoted.push(quoted_at(i));
}
let make = |parts: Vec<WordPart>, quoted: Vec<bool>| Word {
quoted: !quoted.is_empty() && quoted.iter().all(|q| *q),
parts,
has_unquoted_glob: false,
part_quoted: quoted,
raw: None,
};
return Some((
make(key_parts, key_quoted),
make(value_parts, value_quoted),
append,
));
}
}
_ => {}
}
}
}
None
}
impl Interpreter {
pub(super) fn var_kind(&self, name: &str) -> VarKind {
if self.scoped.assoc_arrays.contains_key(name) {
VarKind::Assoc
} else if self.scoped.arrays.contains_key(name) {
VarKind::Indexed
} else if self.scoped.variables.contains_key(name) || self.env.contains_key(name) {
VarKind::Scalar
} else {
VarKind::Unset
}
}
pub(super) fn is_local_anywhere(&self, name: &str) -> bool {
self.call_stack
.iter()
.any(|f| f.saved_vars.contains_key(name))
}
fn declared(&self, name: &str) -> bool {
self.var_kind(name) != VarKind::Unset
|| self.scoped.var_attrs.contains_key(name)
|| self.scoped.namerefs.contains_key(name)
|| self.is_local_anywhere(name)
}
pub(super) fn attr_letters(&self, name: &str) -> String {
let attrs = self.var_attrs_get(name);
let mut s = String::new();
match self.var_kind(name) {
VarKind::Indexed => s.push('a'),
VarKind::Assoc => s.push('A'),
_ => {}
}
if attrs.contains(VarAttrs::INTEGER) {
s.push('i');
}
if self.scoped.namerefs.contains_key(name) {
s.push('n');
}
if attrs.contains(VarAttrs::READONLY) {
s.push('r');
}
if attrs.contains(VarAttrs::EXPORT) || self.env.contains_key(name) {
s.push('x');
}
if attrs.contains(VarAttrs::LOWER) {
s.push('l');
}
if attrs.contains(VarAttrs::UPPER) {
s.push('u');
}
if s.is_empty() {
s.push('-');
}
s
}
pub(super) fn format_declare_line(&self, name: &str) -> Option<String> {
if !self.declared(name) {
return None;
}
let flags = self.attr_letters(name);
let novalue = self.var_attrs_get(name).contains(VarAttrs::NOVALUE);
let body = if let Some(target) = self.scoped.namerefs.get(name) {
(!target.is_empty()).then(|| double_quote(target))
} else {
match self.var_kind(name) {
VarKind::Scalar | VarKind::Unset if name == "SHELLOPTS" => Some(declare_quote(
&crate::builtins::shellopts_value(&self.scoped.variables),
)),
VarKind::Scalar | VarKind::Unset if name == "BASHOPTS" => Some(declare_quote(
&crate::builtins::bashopts_value(&self.scoped.variables),
)),
VarKind::Indexed => {
let arr = &self.scoped.arrays[name];
(!(arr.is_empty() && novalue)).then(|| format_indexed_body(arr))
}
VarKind::Assoc => {
let arr = &self.scoped.assoc_arrays[name];
(!(arr.is_empty() && novalue)).then(|| format_assoc_body(arr))
}
VarKind::Scalar => self
.scoped
.variables
.get(name)
.or_else(|| self.env.get(name))
.map(|v| declare_quote(v)),
VarKind::Unset => None,
}
};
Some(match body {
Some(b) => format!("declare -{flags} {name}={b}\n"),
None => format!("declare -{flags} {name}\n"),
})
}
pub(super) fn listable_names(&self) -> Vec<String> {
let mut names: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
names.extend(self.scoped.variables.keys().cloned());
names.extend(self.env.keys().cloned());
names.extend(self.scoped.arrays.keys().cloned());
names.extend(self.scoped.assoc_arrays.keys().cloned());
names.extend(self.scoped.var_attrs.keys().cloned());
names.extend(self.scoped.namerefs.keys().cloned());
names
.into_iter()
.filter(|n| !is_hidden_variable(n) && !is_internal_variable(n))
.collect()
}
fn has_attrs(&self, name: &str, want: &AttrFlags) -> bool {
let attrs = self.var_attrs_get(name);
let kind = self.var_kind(name);
(!want.indexed || kind == VarKind::Indexed)
&& (!want.assoc || kind == VarKind::Assoc)
&& (!want.integer || attrs.contains(VarAttrs::INTEGER))
&& (!want.nameref || self.scoped.namerefs.contains_key(name))
&& (!want.readonly || attrs.contains(VarAttrs::READONLY))
&& (!want.export || attrs.contains(VarAttrs::EXPORT) || self.env.contains_key(name))
&& (!want.lower || attrs.contains(VarAttrs::LOWER))
&& (!want.upper || attrs.contains(VarAttrs::UPPER))
}
pub(super) fn format_set_listing(&self) -> String {
let mut out = String::new();
for name in self.listable_names() {
if self.scoped.namerefs.contains_key(&name) {
if let Some(t) = self.scoped.namerefs.get(&name)
&& !t.is_empty()
{
out.push_str(&format!("{name}={}\n", set_quote(t)));
}
continue;
}
match self.var_kind(&name) {
VarKind::Indexed => {
out.push_str(&format!(
"{name}={}\n",
format_indexed_body(&self.scoped.arrays[&name])
));
}
VarKind::Assoc => {
out.push_str(&format!(
"{name}={}\n",
format_assoc_body(&self.scoped.assoc_arrays[&name])
));
}
VarKind::Scalar => {
let v = self
.scoped
.variables
.get(&name)
.or_else(|| self.env.get(&name))
.cloned()
.unwrap_or_default();
out.push_str(&format!("{name}={}\n", set_quote(&v)));
}
VarKind::Unset => {}
}
}
out
}
pub(super) fn resolve_nameref_strict(&self, name: &str) -> std::result::Result<String, ()> {
if self.scoped.namerefs.is_empty() {
return Ok(name.to_string());
}
let mut current = name.to_string();
let mut seen = std::collections::HashSet::new();
seen.insert(current.clone());
loop {
let (base, sub) = match current.find('[') {
Some(b) => (current[..b].to_string(), Some(current[b..].to_string())),
None => (current.clone(), None),
};
match self.scoped.namerefs.get(&base) {
Some(target) if !target.is_empty() => {
let next = match (&sub, target.find('[')) {
(Some(s), None) => format!("{target}{s}"),
_ => target.clone(),
};
if !seen.insert(next.clone()) {
return Err(());
}
current = next;
}
_ => return Ok(current),
}
}
}
pub(super) async fn execute_declaration_builtin(
&mut self,
cmd: &str,
kind: DeclKind,
args: &[String],
redirects: &[Redirect],
) -> Result<ExecResult> {
let compounds = std::mem::take(&mut self.pending_compound_args);
let result = self.run_declaration(cmd, kind, args, compounds).await?;
self.apply_redirections(result, redirects).await
}
async fn run_declaration(
&mut self,
cmd: &str,
kind: DeclKind,
args: &[String],
compounds: Vec<Vec<Word>>,
) -> Result<ExecResult> {
let mut opts = DeclOpts::default();
let mut i = 0;
while i < args.len() {
let a = &args[i];
if a == "--" {
i += 1;
break;
}
let (set, rest) = if let Some(r) = a.strip_prefix('-') {
(true, r)
} else if let Some(r) = a.strip_prefix('+') {
(false, r)
} else {
break;
};
if rest.is_empty() || rest.contains('=') {
break;
}
for c in rest.chars() {
if !kind.allowed_options().contains(c) {
let sign = if set { '-' } else { '+' };
return Ok(ExecResult::err(
self.diag(format!(
"{cmd}: {sign}{c}: invalid option\n{}",
kind.usage(cmd)
)),
2,
));
}
let flags = if set { &mut opts.on } else { &mut opts.off };
match c {
'a' => flags.indexed = true,
'A' => flags.assoc = true,
'i' => flags.integer = true,
'n' if kind == DeclKind::Export => opts.off.export = true,
'n' => flags.nameref = true,
'r' => flags.readonly = true,
'x' => flags.export = true,
'l' => flags.lower = true,
'u' => flags.upper = true,
'p' => opts.print = true,
'f' => opts.functions = true,
'F' => opts.function_names = true,
'g' => opts.global = true,
_ => {} }
}
i += 1;
}
let names = &args[i..];
if kind == DeclKind::Local && self.local_frame_index().is_none() {
for arg in names {
if let Some(eq) = operand_eq_pos(arg)
&& let Some(words) = arg[eq + 1..]
.strip_prefix(COMPOUND_MARK)
.and_then(|r| r.strip_suffix(COMPOUND_MARK))
.and_then(|n| n.parse::<usize>().ok())
.and_then(|n| compounds.get(n))
{
let (lhs, append) = match arg[..eq].strip_suffix('+') {
Some(l) => (l, true),
None => (&arg[..eq], false),
};
if is_valid_var_name(lhs)
&& !is_internal_variable(lhs)
&& !self.is_var_readonly(lhs)
{
let assoc = self.var_kind(lhs) == VarKind::Assoc;
let words = words.clone();
self.assign_array_words(lhs, &words, append, assoc).await?;
}
}
}
return Ok(ExecResult::err(
self.diag("local: can only be used in a function\n"),
1,
));
}
if opts.functions || opts.function_names {
return Ok(self.declaration_functions(cmd, kind, &opts, names));
}
if names.is_empty() || opts.print {
return Ok(self.declaration_listing(cmd, kind, &opts, names));
}
let mut stderr = String::new();
let mut status = 0;
for arg in names {
if let Some(msg) = self
.declare_operand(cmd, kind, &opts, arg, &compounds)
.await?
{
stderr.push_str(&msg);
status = 1;
}
}
Ok(ExecResult {
stderr: stderr.into(),
exit_code: status,
..Default::default()
})
}
fn declaration_functions(
&self,
cmd: &str,
kind: DeclKind,
opts: &DeclOpts,
names: &[String],
) -> ExecResult {
let show = |name: &str, listing_all: bool| match self.scoped.functions.get(name) {
Some(_) if opts.function_names && listing_all => format!("declare -f {name}\n"),
Some(_) if opts.function_names => format!("{name}\n"),
Some(f) => format!("{}\n", crate::parser::function_string(name, &f.body)),
None => String::new(),
};
let mut out = String::new();
let mut err = String::new();
let mut status = 0;
if names.is_empty() {
if matches!(kind, DeclKind::Declare | DeclKind::Local) {
let mut fnames: Vec<&String> = self.scoped.functions.keys().collect();
fnames.sort();
for f in fnames {
out.push_str(&show(f, true));
}
}
} else {
for name in names {
if !self.scoped.functions.contains_key(name.as_str()) {
status = 1;
if matches!(kind, DeclKind::Export | DeclKind::Readonly) {
err.push_str(&self.diag(format!("{cmd}: {name}: not a function\n")));
}
} else if matches!(kind, DeclKind::Declare | DeclKind::Local) {
out.push_str(&show(name, false));
}
}
}
ExecResult {
stdout: out.into(),
stderr: err.into(),
exit_code: status,
..Default::default()
}
}
fn declaration_listing(
&self,
cmd: &str,
kind: DeclKind,
opts: &DeclOpts,
names: &[String],
) -> ExecResult {
let mut out = String::new();
if !names.is_empty() {
let mut err = String::new();
let mut status = 0;
for name in names {
match self.format_declare_line(name) {
Some(line) => out.push_str(&line),
None => {
err.push_str(&self.diag(format!("{cmd}: {name}: not found\n")));
status = 1;
}
}
}
return ExecResult {
stdout: out.into(),
stderr: err.into(),
exit_code: status,
..Default::default()
};
}
let mut want = opts.on;
match kind {
DeclKind::Local => {
if let Some(idx) = self.local_frame_index() {
let mut locals: Vec<&String> = self.call_stack[idx].saved_vars.keys().collect();
locals.sort();
for name in locals {
if let Some(line) = self.format_declare_line(name) {
out.push_str(&line);
}
}
}
return ExecResult::ok(out);
}
DeclKind::Export => {
if opts.off.export {
return ExecResult::ok(String::new());
}
want.export = true;
}
DeclKind::Readonly => want.readonly = true,
DeclKind::Declare => {
if !opts.print && !want.any() {
return ExecResult::ok(self.format_set_listing());
}
}
}
for name in self.listable_names() {
if self.has_attrs(&name, &want)
&& let Some(line) = self.format_declare_line(&name)
{
out.push_str(&line);
}
}
ExecResult::ok(out)
}
async fn declare_operand(
&mut self,
cmd: &str,
kind: DeclKind,
opts: &DeclOpts,
arg: &str,
compounds: &[Vec<Word>],
) -> Result<Option<String>> {
let (lhs, value, append) = match operand_eq_pos(arg) {
Some(eq) => {
let lhs = &arg[..eq];
let (lhs, append) = match lhs.strip_suffix('+') {
Some(l) => (l, true),
None => (lhs, false),
};
let raw = &arg[eq + 1..];
let value = match raw
.strip_prefix(COMPOUND_MARK)
.and_then(|r| r.strip_suffix(COMPOUND_MARK))
.and_then(|n| n.parse::<usize>().ok())
.and_then(|n| compounds.get(n))
{
Some(words) => DeclValue::Compound(words.clone()),
None => DeclValue::Str(raw.to_string()),
};
(lhs, Some(value), append)
}
None => (arg, None, false),
};
let display_arg = || -> String {
match operand_eq_pos(arg) {
Some(eq) if arg[eq + 1..].starts_with(COMPOUND_MARK) => {
format!("{}(...)", &arg[..=eq])
}
_ => arg.to_string(),
}
};
let (base, subscript) = match lhs.find('[') {
Some(b)
if lhs.ends_with(']') && matches!(kind, DeclKind::Declare | DeclKind::Local) =>
{
(&lhs[..b], Some(lhs[b + 1..lhs.len() - 1].to_string()))
}
_ => (lhs, None),
};
if !is_valid_var_name(base) {
return Ok(Some(self.diag(format!(
"{cmd}: `{}': not a valid identifier\n",
display_arg()
))));
}
if is_internal_variable(base) {
return Ok(None);
}
let in_function = self.local_frame_index().is_some();
let make_local = !opts.global
&& match kind {
DeclKind::Local => true,
DeclKind::Declare => in_function,
_ => false,
};
if opts.on.nameref && kind != DeclKind::Export {
return Ok(self.declare_nameref(cmd, base, value, make_local, opts));
}
if opts.off.nameref
&& let Some(target) = self.scoped.namerefs.get(base).cloned()
{
self.remove_nameref(base);
self.insert_variable_checked(base.to_string(), target);
return Ok(None);
}
let target = if make_local && !self.is_local_in_current_frame(base) {
lhs.to_string()
} else {
match self.resolve_nameref_strict(lhs) {
Ok(t) => t,
Err(()) => {
return Ok(Some(self.diag(format!(
"{cmd}: warning: {base}: circular name reference\n"
))));
}
}
};
let (name, subscript) = match target.find('[') {
Some(b) if target.ends_with(']') => (
target[..b].to_string(),
Some(target[b + 1..target.len() - 1].to_string()),
),
_ => (target.clone(), subscript),
};
let name = name.as_str();
if opts.global
&& subscript.is_none()
&& let Some(DeclValue::Str(v)) = &value
&& let Some(idx) = self
.call_stack
.iter()
.position(|f| f.saved_vars.contains_key(name))
{
let saved = self.call_stack[idx]
.saved_vars
.get(name)
.cloned()
.unwrap_or_default();
if saved.attrs.is_some_and(|a| a.contains(VarAttrs::READONLY)) {
return Ok(Some(
self.diag(format!("{cmd}: {name}: readonly variable\n")),
));
}
let new_value = if append {
saved.value.clone().unwrap_or_default() + v
} else {
v.clone()
};
let old_len = saved.value.as_ref().map_or(0, String::len);
let is_new = saved.value.is_none();
let old_key_len = if is_new { 0 } else { name.len() };
if let Err(error) = self.memory_budget.check_variable_insert(
name.len(),
new_value.len(),
is_new,
old_key_len,
old_len,
&self.memory_limits,
) {
self.memory_limit_error.get_or_insert(error);
return Ok(None);
}
self.memory_budget.record_variable_insert(
name.len(),
new_value.len(),
is_new,
old_key_len,
old_len,
);
if let Some(slot) = self.call_stack[idx].saved_vars.get_mut(name) {
slot.value = Some(new_value);
}
return Ok(None);
}
if make_local && !self.is_local_in_current_frame(name) {
if self.is_var_readonly(name) {
return Ok(Some(
self.diag(format!("{cmd}: {name}: readonly variable\n")),
));
}
self.make_local(name);
}
if self.is_var_readonly(name)
&& (value.is_some() || opts.off.readonly || opts.off.export && kind == DeclKind::Export)
{
return Ok(Some(
self.diag(format!("{cmd}: {name}: readonly variable\n")),
));
}
let is_compound = matches!(value, Some(DeclValue::Compound(_)));
let (mut want_indexed, mut want_assoc) = (opts.on.indexed, opts.on.assoc);
if kind == DeclKind::Readonly && !is_compound {
want_indexed = false;
want_assoc = false;
}
if subscript.is_some() && !want_assoc && self.var_kind(name) != VarKind::Assoc {
want_indexed = true;
}
let kind_now = self.var_kind(name);
let conversion = if want_assoc && kind_now == VarKind::Indexed {
Some("indexed to associative")
} else if want_indexed && kind_now == VarKind::Assoc {
Some("associative to indexed")
} else {
None
};
if let Some(what) = conversion {
let msg = self.diag(format!("{cmd}: {name}: cannot convert {what} array\n"));
if is_compound {
return Err(crate::error::Error::LineAbort(msg));
}
return Ok(Some(msg));
}
if want_assoc && kind_now != VarKind::Assoc {
let mut arr = HashMap::new();
if let Some(v) = self.remove_scalar_value(name) {
arr.insert("0".to_string(), v);
}
let empty = arr.is_empty();
self.insert_assoc_array_checked(name.to_string(), arr);
if empty && value.is_none() {
self.add_var_attr(name, VarAttrs::NOVALUE);
}
} else if want_indexed && !matches!(kind_now, VarKind::Indexed | VarKind::Assoc) {
let mut arr = HashMap::new();
if let Some(v) = self.remove_scalar_value(name) {
arr.insert(0, v);
}
let empty = arr.is_empty();
self.insert_array_checked(name.to_string(), arr);
if empty && value.is_none() {
self.add_var_attr(name, VarAttrs::NOVALUE);
}
} else if kind_now == VarKind::Unset
&& value.is_none()
&& !self.scoped.var_attrs.contains_key(name)
&& !self.is_local_in_current_frame(name)
{
self.add_var_attr(name, VarAttrs::NOVALUE);
}
self.apply_declared_attrs(name, kind, opts);
match value {
None => {}
Some(DeclValue::Compound(words)) => {
if subscript.is_some() {
return Ok(Some(self.diag(format!(
"{cmd}: {name}: cannot assign list to array member\n"
))));
}
let assoc = self.var_kind(name) == VarKind::Assoc;
self.assign_array_words(name, &words, append, assoc).await?;
}
Some(DeclValue::Str(v)) => {
if let Some(sub) = subscript {
self.assign_element(name, &sub, v, append).await?;
} else if (opts.on.indexed || opts.on.assoc)
&& v.starts_with('(')
&& v.ends_with(')')
&& kind != DeclKind::Readonly
{
let words = self.parse_compound_text(name, &v)?;
let assoc = self.var_kind(name) == VarKind::Assoc;
self.assign_array_words(name, &words, append, assoc).await?;
} else if append {
self.append_scalar(name, v);
} else {
self.set_variable(name.to_string(), v);
}
}
}
if opts.on.readonly || kind == DeclKind::Readonly {
self.add_var_attr(name, VarAttrs::READONLY);
}
if self.var_attrs_get(name).contains(VarAttrs::EXPORT)
&& let Some(v) = self.scoped.variables.get(name).cloned()
{
self.insert_env_checked(name.to_string(), v);
}
Ok(None)
}
fn apply_declared_attrs(&mut self, name: &str, kind: DeclKind, opts: &DeclOpts) {
if opts.on.integer {
self.add_var_attr(name, VarAttrs::INTEGER);
}
if opts.off.integer {
self.remove_var_attr(name, VarAttrs::INTEGER);
}
if opts.on.lower && opts.on.upper {
self.remove_var_attr(name, VarAttrs::LOWER);
self.remove_var_attr(name, VarAttrs::UPPER);
} else if opts.on.lower {
self.add_var_attr(name, VarAttrs::LOWER);
self.remove_var_attr(name, VarAttrs::UPPER);
} else if opts.on.upper {
self.add_var_attr(name, VarAttrs::UPPER);
self.remove_var_attr(name, VarAttrs::LOWER);
}
if opts.off.lower {
self.remove_var_attr(name, VarAttrs::LOWER);
}
if opts.off.upper {
self.remove_var_attr(name, VarAttrs::UPPER);
}
if (opts.on.export || kind == DeclKind::Export) && !opts.off.export {
self.add_var_attr(name, VarAttrs::EXPORT);
}
if opts.off.export {
if !self.scoped.variables.contains_key(name)
&& let Some(v) = self.env.get(name).cloned()
{
self.insert_variable_checked(name.to_string(), v);
}
self.remove_var_attr(name, VarAttrs::EXPORT);
self.env_mut().remove(name);
}
}
fn declare_nameref(
&mut self,
cmd: &str,
name: &str,
value: Option<DeclValue>,
make_local: bool,
opts: &DeclOpts,
) -> Option<String> {
let target = match value {
Some(DeclValue::Str(t)) => Some(t),
Some(DeclValue::Compound(_)) => {
return Some(self.diag(format!(
"{cmd}: {name}: reference variable cannot be an array\n"
)));
}
None => None,
};
if let Some(t) = &target {
if !valid_nameref_target(t) {
return Some(self.diag(format!(
"{cmd}: `{t}': invalid variable name for name reference\n"
)));
}
if t == name && !make_local {
return Some(self.diag(format!(
"{cmd}: {name}: nameref variable self references not allowed\n"
)));
}
}
if make_local && !self.is_local_in_current_frame(name) {
if self.is_var_readonly(name) {
return Some(self.diag(format!("{cmd}: {name}: readonly variable\n")));
}
self.make_local(name);
}
if target.is_some() && self.is_var_readonly(name) {
return Some(self.diag(format!("{cmd}: {name}: readonly variable\n")));
}
let target = match target {
Some(t) => t,
None => match self.scoped.namerefs.get(name) {
Some(existing) => existing.clone(),
None => match self.scoped.variables.get(name).cloned() {
Some(v) if valid_nameref_target(&v) && v != name => {
self.remove_scalar_value(name);
v
}
Some(v) => {
return Some(self.diag(format!(
"{cmd}: `{v}': invalid variable name for name reference\n"
)));
}
None => String::new(),
},
},
};
self.remove_scalar_value(name);
self.remove_var_attr(name, VarAttrs::NOVALUE);
if opts.on.export && !target.is_empty() {
self.add_var_attr(name, VarAttrs::EXPORT);
self.insert_env_checked(name.to_string(), target.clone());
}
self.set_nameref(name, target);
if opts.on.readonly {
self.add_var_attr(name, VarAttrs::READONLY);
}
None
}
pub(super) fn append_scalar(&mut self, name: &str, value: String) {
let existing = self.expand_variable(name);
if self.var_attrs_get(name).contains(VarAttrs::INTEGER) {
let base = if existing.is_empty() {
"0".to_string()
} else {
existing
};
self.set_variable(name.to_string(), format!("{base}+({value})"));
} else {
self.set_variable(name.to_string(), existing + &value);
}
}
fn transform_element(&mut self, attrs: VarAttrs, old: Option<&str>, value: String) -> String {
if attrs.contains(VarAttrs::INTEGER) {
let expr = match old {
Some(o) if !o.is_empty() => format!("{o}+({value})"),
_ => value,
};
return self.evaluate_arithmetic_with_assign(&expr).to_string();
}
let value = match old {
Some(o) => format!("{o}{value}"),
None => value,
};
if attrs.contains(VarAttrs::LOWER) {
value.to_lowercase()
} else if attrs.contains(VarAttrs::UPPER) {
value.to_uppercase()
} else {
value
}
}
pub(super) async fn assign_element(
&mut self,
name: &str,
sub: &str,
value: String,
append: bool,
) -> Result<()> {
let attrs = self.var_attrs_get(name);
if self.var_kind(name) == VarKind::Assoc {
let key = self.expand_raw_assoc_key(sub).await?;
let old = if append {
self.scoped
.assoc_arrays
.get(name)
.and_then(|a| a.get(&key))
.cloned()
} else {
None
};
let v = self.transform_element(attrs, old.as_deref(), value);
self.set_assoc_element_checked(name.to_string(), key, v);
} else {
self.promote_scalar_to_indexed(name);
let expanded;
let sub = if sub.contains("$(") || sub.contains('`') {
expanded = Box::pin(self.expand_command_subs_in_arithmetic(sub)).await?;
expanded.as_str()
} else {
sub
};
let idx = self
.indexed_write_subscript(name, sub)
.map_err(crate::error::Error::LineAbort)?;
let old = if append {
Some(
self.scoped
.arrays
.get(name)
.and_then(|a| a.get(&idx))
.cloned()
.unwrap_or_default(),
)
} else {
None
};
let v = self.transform_element(attrs, old.as_deref(), value);
self.set_indexed_element_checked(name, idx, v);
}
self.remove_var_attr(name, VarAttrs::NOVALUE);
Ok(())
}
pub(super) fn set_indexed_element_checked(&mut self, name: &str, idx: usize, value: String) {
let old_len = self
.scoped
.arrays
.get(name)
.and_then(|a| a.get(&idx))
.map(String::len);
let is_new = old_len.is_none();
if !self.admit_array_write(usize::from(is_new), value.len(), old_len.unwrap_or(0)) {
return;
}
self.arrays_mut()
.entry(name.to_string())
.or_default()
.insert(idx, value);
}
pub(super) fn promote_scalar_to_indexed(&mut self, name: &str) {
if self.scoped.arrays.contains_key(name) || self.scoped.assoc_arrays.contains_key(name) {
return;
}
let Some(v) = self.remove_scalar_value(name) else {
return;
};
let mut arr = HashMap::new();
arr.insert(0, v);
self.insert_array_checked(name.to_string(), arr);
}
fn parse_compound_text(&self, name: &str, text: &str) -> Result<Vec<Word>> {
let src = format!("{name}={text}");
let parser = Parser::with_limits(
&src,
self.limits.max_ast_depth,
self.limits.max_parser_operations,
)
.with_execution_budget(self.execution_budget.clone());
let script = parser.parse()?;
for cmd in &script.commands {
if let Command::Simple(simple) = cmd
&& let Some(a) = simple.assignments.first()
&& let AssignmentValue::Array(words) = &a.value
{
return Ok(words.clone());
}
}
Ok(Vec::new())
}
pub(super) async fn assign_array_words(
&mut self,
name: &str,
words: &[Word],
append: bool,
assoc: bool,
) -> Result<()> {
let attrs = self.var_attrs_get(name);
if assoc {
let mut map: HashMap<String, String> = if append {
self.scoped
.assoc_arrays
.get(name)
.cloned()
.unwrap_or_default()
} else {
HashMap::new()
};
let old_entries = self.scoped.assoc_arrays.get(name).map_or(0, |a| a.len());
let max_entries = old_entries.saturating_add(
self.memory_limits
.max_array_entries
.saturating_sub(self.memory_budget.array_entries),
);
let mut pending_key: Option<String> = None;
let keyed_list = words.first().is_some_and(|w| split_keyed_word(w).is_some());
for word in words {
if map.len() >= max_entries {
break;
}
if let Some((kw, vw, kappend)) = split_keyed_word(word) {
let key = self.expand_word(&kw).await?;
let val = self.expand_word(&no_tilde_word(&vw)).await?;
let old = if kappend && append {
Some(map.get(&key).cloned().unwrap_or_default())
} else {
None
};
let v = self.transform_element(attrs, old.as_deref(), val);
map.insert(key, v);
continue;
}
if keyed_list {
let text = self.expand_word(word).await?;
let msg = self.diag(format!(
"{name}: {text}: must use subscript when assigning associative array\n"
));
self.queue_subst_stderr(&crate::StreamData::from(msg));
continue;
}
let remaining = max_entries.saturating_sub(map.len()).saturating_mul(2);
let fields = self.expand_element_fields(word, remaining).await?;
let (fields, _fields_lease) = fields.into_parts();
for field in fields {
let field = field.into_inner();
match pending_key.take() {
Some(k) => {
let v = self.transform_element(attrs, None, field);
map.insert(k, v);
}
None => pending_key = Some(field),
}
}
}
if let Some(k) = pending_key {
map.insert(k, String::new());
}
self.remove_scalar_value(name);
self.arrays_mut().remove(name);
self.insert_assoc_array_checked(name.to_string(), map);
} else {
let mut map: HashMap<usize, String> = if append {
self.promote_scalar_to_indexed(name);
self.scoped.arrays.get(name).cloned().unwrap_or_default()
} else {
HashMap::new()
};
let old_entries = self.scoped.arrays.get(name).map_or(0, |a| a.len());
let max_entries = old_entries.saturating_add(
self.memory_limits
.max_array_entries
.saturating_sub(self.memory_budget.array_entries),
);
let mut next = map.keys().max().map_or(0, |k| k + 1);
enum Item {
Keyed(BudgetedString, BudgetedString, bool),
Fields(BudgetedVec<BudgetedString>),
}
let mut items = BudgetedVec::new(Some(&self.execution_budget))?;
let mut budget = max_entries.saturating_sub(map.len());
for word in words {
self.execution_budget.consume_work(1)?;
let braces = self.brace_expand_word(word).is_some();
if let Some((kw, vw, kappend)) = split_keyed_word(word).filter(|_| !braces) {
let key_text = self.expand_array_word(&kw).await?;
let (vw, _tilde_leases) = self.tilde_assignment_value(&vw)?;
let val = self.expand_array_word(&vw).await?;
items.try_push(Item::Keyed(key_text, val, kappend))?;
continue;
}
if budget == 0 {
break;
}
let fields = self.expand_element_fields(word, budget).await?;
budget = budget.saturating_sub(fields.len());
items.try_push(Item::Fields(fields))?;
}
let (items, _items_lease) = items.into_parts();
let mentions_self = items
.iter()
.any(|i| matches!(i, Item::Keyed(k, ..) if k.contains(name)));
if mentions_self && !append {
self.insert_array_checked(name.to_string(), HashMap::new());
}
'items: for item in items {
match item {
Item::Keyed(key_text, val, kappend) => {
let raw = match self.try_evaluate_arithmetic_with_assign(&key_text) {
Ok(v) => v,
Err(msg) => {
return Err(crate::error::Error::LineAbort(
self.arith_diag("", &msg),
));
}
};
let idx = if raw < 0 {
let len = map.keys().max().map_or(0, |m| m + 1) as i64;
let i = len + raw;
if i < 0 {
return Err(crate::error::Error::LineAbort(self.diag(format!(
"{name}[{}]: bad array subscript\n",
&*key_text
))));
}
i as usize
} else {
raw as usize
};
let old = if kappend {
Some(map.get(&idx).cloned().unwrap_or_default())
} else {
None
};
let v = self.transform_element(attrs, old.as_deref(), val.into_inner());
if map.len() >= max_entries && !map.contains_key(&idx) {
break 'items;
}
if mentions_self {
self.set_indexed_element_checked(name, idx, v.clone());
}
map.insert(idx, v);
next = idx + 1;
}
Item::Fields(fields) => {
let (fields, _fields_lease) = fields.into_parts();
for field in fields {
let field = field.into_inner();
if map.len() >= max_entries {
break 'items;
}
let v = self.transform_element(attrs, None, field);
if mentions_self {
self.set_indexed_element_checked(name, next, v.clone());
}
map.insert(next, v);
next += 1;
}
}
}
}
self.remove_scalar_value(name);
self.assoc_arrays_mut().remove(name);
self.insert_array_checked(name.to_string(), map);
}
self.remove_var_attr(name, VarAttrs::NOVALUE);
Ok(())
}
async fn expand_element_fields(
&mut self,
word: &Word,
limit: usize,
) -> Result<BudgetedVec<BudgetedString>> {
let Some(braced) = self.brace_expand_word(word) else {
let fields = self.expand_element_word(word, limit).await?;
return self.lease_array_fields(fields);
};
let mut fields = BudgetedVec::new(Some(&self.execution_budget))?;
for w in &braced {
let left = limit.saturating_sub(fields.len());
if left == 0 {
break;
}
let expanded = self.expand_element_word(w, left).await?;
let leased = self.lease_array_fields(expanded)?;
let (expanded, _lease) = leased.into_parts();
for field in expanded {
fields.try_push(field)?;
}
}
Ok(fields)
}
fn lease_array_fields(&self, fields: Vec<String>) -> Result<BudgetedVec<BudgetedString>> {
let mut leased = BudgetedVec::new(Some(&self.execution_budget))?;
for field in fields {
leased.try_push(BudgetedString::try_from_string(
field,
Some(&self.execution_budget),
)?)?;
}
Ok(leased)
}
async fn expand_element_word(&mut self, word: &Word, limit: usize) -> Result<Vec<String>> {
let is_unquoted_expansion = !word.quoted
&& word.parts.iter().any(|p| {
matches!(
p,
WordPart::Variable(_)
| WordPart::CommandSubstitution(_)
| WordPart::ArithmeticExpansion(_)
| WordPart::ParameterExpansion { .. }
| WordPart::ArrayAccess { .. }
)
});
let is_splat = word.quoted
&& word.parts.len() == 1
&& matches!(
&word.parts[0],
WordPart::ArrayAccess { index, .. } if index == "@"
)
|| word.quoted
&& word.parts.len() == 1
&& matches!(&word.parts[0], WordPart::Variable(n) if n == "@");
if is_unquoted_expansion {
let expanded = self.expand_array_word(word).await?;
let mut fields = Vec::new();
for field in self.ifs_split_limited(&expanded, limit)? {
match self.expand_glob_item(&field, false).await {
Ok(items) => fields.extend(items),
Err(pat) => return Err(self.failglob_abort(&pat)),
}
if fields.len() >= limit {
break;
}
}
fields.truncate(limit);
return Ok(fields);
}
if is_splat || word.quoted && !word.has_unquoted_glob && word_has_at_splat(word) {
let mut fields = self.expand_word_to_fields(word).await?;
fields.truncate(limit);
return Ok(fields);
}
let value = self.expand_array_word(word).await?;
let literal_only = word.parts.iter().all(|p| matches!(p, WordPart::Literal(_)));
let globbable = if word.quoted {
word.has_unquoted_glob
} else {
literal_only
};
if !globbable {
return Ok(vec![value.into_inner()]);
}
let mut fields = match self
.expand_glob_item(&value, word.quoted && word.has_unquoted_glob)
.await
{
Ok(items) => items,
Err(pat) => return Err(self.failglob_abort(&pat)),
};
fields.truncate(limit);
Ok(fields)
}
fn failglob_abort(&mut self, pattern: &str) -> crate::error::Error {
self.last_exit_code = 1;
crate::error::Error::LineAbort(self.diag(format!("no match: {pattern}\n")))
}
}
fn no_tilde_word(word: &Word) -> std::borrow::Cow<'_, Word> {
let has_tilde = word
.parts
.iter()
.any(|p| matches!(p, WordPart::Literal(s) if s.contains('~')));
if !has_tilde {
return std::borrow::Cow::Borrowed(word);
}
let mut out = word.clone();
out.part_quoted = word
.parts
.iter()
.enumerate()
.map(|(i, p)| {
matches!(p, WordPart::Literal(s) if s.contains('~'))
|| word.part_quoted.get(i).copied().unwrap_or(word.quoted)
})
.collect();
std::borrow::Cow::Owned(out)
}
fn word_has_at_splat(word: &Word) -> bool {
word.parts.iter().any(|p| match p {
WordPart::ArrayAccess { index, .. } => index == "@",
WordPart::Variable(n) => n == "@",
WordPart::Substring { name, .. } => name == "@" || name.ends_with("[@]"),
_ => false,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quoting_matches_bash() {
assert_eq!(declare_quote("a\"b$c`d\\e"), "\"a\\\"b\\$c\\`d\\\\e\"");
assert_eq!(declare_quote("x\ny"), "$'x\\ny'");
assert_eq!(ansic_quote("a\x01\x7f'"), "$'a\\001\\177\\''");
assert_eq!(set_quote("abc"), "abc");
assert_eq!(set_quote("a b"), "'a b'");
assert_eq!(set_quote("it's"), "'it'\\''s'");
assert_eq!(set_quote("a\tb c"), "$'a\\tb c'");
}
#[test]
fn assoc_order_follows_bash_hash_buckets() {
let m: HashMap<String, String> = ["k", "j", "a", "b", "zz", "x1"]
.iter()
.map(|k| (k.to_string(), String::new()))
.collect();
let keys: Vec<&str> = assoc_keys_bash_order(&m)
.into_iter()
.map(String::as_str)
.collect();
assert_eq!(keys, ["x1", "k", "j", "b", "a", "zz"]);
}
#[test]
fn keyed_elements_split_into_key_and_value() {
let script = Parser::new(r#"a=([k]="hello world" [i+1]+=x plain)"#)
.parse()
.unwrap();
let Command::Simple(cmd) = &script.commands[0] else {
panic!("expected a simple command");
};
let AssignmentValue::Array(words) = &cmd.assignments[0].value else {
panic!("expected an array assignment");
};
assert_eq!(words.len(), 3);
let (k, v, append) = split_keyed_word(&words[0]).expect("keyed");
assert_eq!(
(k.to_string(), v.to_string(), append),
("k".into(), "hello world".into(), false)
);
let (k, _, append) = split_keyed_word(&words[1]).expect("keyed");
assert_eq!((k.to_string(), append), ("i+1".into(), true));
assert!(split_keyed_word(&words[2]).is_none());
}
#[test]
fn nameref_targets() {
assert!(valid_nameref_target("a"));
assert!(valid_nameref_target("a[1]"));
assert!(!valid_nameref_target("1x"));
assert!(!valid_nameref_target("@"));
assert!(!valid_nameref_target("a b"));
}
}