Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 55 additions & 16 deletions brush-core/src/expansion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::borrow::Cow;
use std::cmp::min;

use brush_parser::word::{ParameterTransformOp, SubstringMatchKind};
use itertools::Either;
use itertools::Itertools;

use crate::ExecutionParameters;
Expand Down Expand Up @@ -492,6 +493,41 @@ pub async fn assign_to_named_parameter(
expander.assign_to_parameter(&parameter, value).await
}

fn resolve_nameref_inner<'a, SE: extensions::ShellExtensions>(
shell: &'a Shell<SE>,
name: &str,
) -> Option<Either<(env::EnvironmentScope, &'a ShellVariable), Cow<'a, str>>> {
let (scope, var) = shell.env().get(name)?;
if !var.is_treated_as_nameref() {
Some(Either::Left((scope, var)))
} else {
var.value().try_get_cow_str(shell).map(Either::Right)
}
}

/// If `name` refers to a nameref variable, returns the target variable name.
/// Returns `None` if it's not a nameref or if the target is empty/unset.
pub(crate) fn resolve_nameref_target<'a, SE: extensions::ShellExtensions>(
shell: &Shell<SE>,
name: &'a str,
) -> Cow<'a, str> {
match resolve_nameref_inner(shell, name) {
Some(Either::Left(_)) | None => Cow::Borrowed(name),
Some(Either::Right(n)) => Cow::Owned(n.to_string()),
}
}

/// Resolves a nameref and returns the target variable from the environment.
/// If `name` is not a nameref, returns the variable for `name` itself.
/// Returns `None` if the variable (or nameref target) doesn't exist.
/// This version avoids allocation by directly looking up the target variable.
pub(crate) fn resolve_nameref_var<'a, SE: extensions::ShellExtensions>(
shell: &'a Shell<SE>,
name: &str,
) -> Option<(env::EnvironmentScope, &'a ShellVariable)> {
resolve_nameref_inner(shell, name)?.either(Some, |n| shell.env().get(&n))
}

struct WordExpander<'a, SE: extensions::ShellExtensions> {
/// The shell in which to perform expansion.
shell: &'a mut Shell<SE>,
Expand Down Expand Up @@ -1515,7 +1551,7 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> {
variable_name,
concatenate,
} => {
let keys = if let Some((_, var)) = self.shell.env().get(variable_name) {
let keys = if let Some((_, var)) = resolve_nameref_var(self.shell, &variable_name) {
var.value().element_keys(self.shell)
} else {
vec![]
Expand Down Expand Up @@ -1567,6 +1603,8 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> {
}
};

// Resolve nameref: if the variable is a nameref, redirect the write to the target.
let variable_name = resolve_nameref_target(self.shell, variable_name.as_str());
if let Some(index) = index {
self.shell.env_mut().update_or_add_array_element(
variable_name,
Expand Down Expand Up @@ -1620,9 +1658,9 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> {
} => (Some(name.to_owned()), None),
};

let var = name
.as_ref()
.and_then(|name| self.shell.env().get(name).map(|(_, var)| var.clone()));
let var = name.as_ref().and_then(|name| {
resolve_nameref_var(self.shell, name.as_str()).map(|(_, var)| var.clone())
});

(name, index, var)
}
Expand Down Expand Up @@ -1702,7 +1740,7 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> {
brush_parser::word::Parameter::Named(n) => {
if !env::valid_variable_name(n.as_str()) {
Err(error::ErrorKind::BadSubstitution(n.clone()).into())
} else if let Some((_, var)) = self.shell.env().get(n) {
} else if let Some((_, var)) = resolve_nameref_var(self.shell, n.as_str()) {
if matches!(var.value(), ShellValue::Unset(_)) {
self.undefined_expansion(parameter, allow_unset_vars)
} else {
Expand All @@ -1719,23 +1757,24 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> {
}
brush_parser::word::Parameter::NamedWithIndex { name, index } => {
// First check to see if it's an associative array.
let is_set_assoc_array = if let Some((_, var)) = self.shell.env().get(name) {
matches!(
var.value(),
ShellValue::AssociativeArray(_)
| ShellValue::Unset(ShellValueUnsetType::AssociativeArray)
)
} else {
false
};
let is_set_assoc_array =
if let Some((_, var)) = resolve_nameref_var(self.shell, name.as_str()) {
matches!(
var.value(),
ShellValue::AssociativeArray(_)
| ShellValue::Unset(ShellValueUnsetType::AssociativeArray)
)
} else {
false
};

// Figure out which index to use.
let index_to_use = self
.expand_array_index(index.as_str(), is_set_assoc_array)
.await?;

// Index into the array.
if let Some((_, var)) = self.shell.env().get(name)
if let Some((_, var)) = resolve_nameref_var(self.shell, name.as_str())
&& let Ok(Some(value)) = var.value().get_at(index_to_use.as_str(), self.shell)
{
Ok(Expansion::from(value.to_string()))
Expand All @@ -1744,7 +1783,7 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> {
}
}
brush_parser::word::Parameter::NamedWithAllIndices { name, concatenate } => {
if let Some((_, var)) = self.shell.env().get(name) {
if let Some((_, var)) = resolve_nameref_var(self.shell, name.as_str()) {
let values = var.value().element_values(self.shell);

Ok(Expansion {
Expand Down
9 changes: 5 additions & 4 deletions brush-core/src/interp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1389,6 +1389,9 @@ async fn apply_assignment(
}
};

// Resolve nameref: if the variable is a nameref, redirect writes to the target.
let variable_name = expansion::resolve_nameref_target(shell, variable_name.as_str());

// Expand the values.
let new_value = match &assignment.value {
ast::AssignmentValue::Scalar(unexpanded_value) => {
Expand Down Expand Up @@ -1436,7 +1439,7 @@ async fn apply_assignment(
// See if we need to eval an array index.
if let Some(idx) = &array_index {
let will_be_indexed_array = if let Some((_, existing_value)) =
shell.env().get(variable_name)
expansion::resolve_nameref_var(shell, &variable_name)
{
matches!(
existing_value.value(),
Expand All @@ -1459,9 +1462,7 @@ async fn apply_assignment(
let export_variables_on_modification = shell.options().export_variables_on_modification;

// See if we can find an existing value associated with the variable.
if let Some((existing_value_scope, existing_value)) =
shell.env_mut().get_mut(variable_name.as_str())
{
if let Some((existing_value_scope, existing_value)) = shell.env_mut().get_mut(&variable_name) {
if required_scope.is_none() || Some(existing_value_scope) == required_scope {
if let Some(array_index) = array_index {
match new_value {
Expand Down
8 changes: 3 additions & 5 deletions brush-shell/tests/cases/compat/arrays.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ common_test_files:
- path: "helpers.sh"
contents: |
stable_print_assoc_array() {
# TODO(nameref): enable use of nameref when implemented; for now
# we assume the name of the array is assoc_array
# local -n assoc_array=$1
local -n _arr=$1
local key

for key in $(printf "%s\n" "${!assoc_array[@]}" | sort -n); do
echo "\"${key}\" => ${assoc_array[${key}]}"
for key in $(printf "%s\n" "${!_arr[@]}" | sort -n); do
echo "\"${key}\" => ${_arr[${key}]}"
done
}
cases:
Expand Down
88 changes: 83 additions & 5 deletions brush-shell/tests/cases/compat/builtins/declare.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ common_test_files:
- path: "helpers.sh"
contents: |
stable_print_assoc_array() {
# TODO(nameref): enable use of nameref when implemented; for now
# we assume the name of the array is assoc_array
# local -n assoc_array=$1
local -n _arr=$1
local key

for key in $(printf "%s\n" "${!assoc_array[@]}" | sort -n); do
echo "\"${key}\" => ${assoc_array[${key}]}"
for key in $(printf "%s\n" "${!_arr[@]}" | sort -n); do
echo "\"${key}\" => ${_arr[${key}]}"
done
}

Expand Down Expand Up @@ -359,3 +357,83 @@ cases:
stdin: |
declare 1=x && echo "Set 1"
declare @=10 && echo "Set @"

- name: "Nameref basic read"
stdin: |
target="hello world"
declare -n ref=target
echo "ref: $ref"
echo "target: $target"

- name: "Nameref write through reference"
stdin: |
target="original"
declare -n ref=target
echo "before: $target"
ref="modified"
echo "after: $target"
echo "ref: $ref"

- name: "Nameref to array element"
stdin: |
arr=(one two three)
declare -n ref=arr
echo "ref[0]: ${ref[0]}"
echo "ref[1]: ${ref[1]}"
echo "ref[@]: ${ref[@]}"
echo "ref[*]: ${ref[*]}"

- name: "Nameref array modification"
stdin: |
arr=(a b c)
declare -n ref=arr
ref[1]="modified"
echo "arr[1]: ${arr[1]}"
echo "ref[1]: ${ref[1]}"

- name: "Nameref to associative array"
stdin: |
declare -A assoc=(["key1"]="val1" ["key2"]="val2")
declare -n ref=assoc
echo "ref[key1]: ${ref[key1]}"
echo "ref[key2]: ${ref[key2]}"

- name: "Nameref in function - pass by reference"
stdin: |
modify_var() {
local -n ref=$1
ref="modified by function"
}

myvar="original"
echo "before: $myvar"
modify_var myvar
echo "after: $myvar"

- name: "Nameref array iteration in function"
stdin: |
print_array() {
local -n arr=$1
echo "count: ${#arr[@]}"
for elem in "${arr[@]}"; do
echo "elem: $elem"
done
}

myarr=(first second third)
print_array myarr

- name: "Nameref get keys"
stdin: |
declare -A assoc=(["a"]=1 ["b"]=2 ["c"]=3)
declare -n ref=assoc
echo "keys (sorted): $(printf '%s\n' ${!ref[@]} | sort | tr '\n' ' ')"
echo "count: ${#ref[@]}"

- name: "Nameref unset target"
stdin: |
target="exists"
declare -n ref=target
echo "ref: $ref"
unset target
echo "ref after unset: '${ref}'"
Loading