1091 lines
38 KiB
Rust
1091 lines
38 KiB
Rust
//! Implements variables for a shell environment.
|
|||
|
|
|
||
|
|
use std::borrow::Cow;
|
||
|
|
use std::collections::BTreeMap;
|
||
|
|
use std::ffi::OsString;
|
||
|
|
use std::fmt::{Display, Write};
|
||
|
|
|
||
|
|
use crate::shell::{Shell, ShellState};
|
||
|
|
use crate::{error, escape, extensions};
|
||
|
|
|
||
|
|
/// A shell variable.
|
||
|
|
#[derive(Clone, Debug)]
|
||
|
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||
|
|
pub struct ShellVariable {
|
||
|
|
/// The value currently associated with the variable.
|
||
|
|
value: ShellValue,
|
||
|
|
/// Whether or not the variable is marked as exported to child processes.
|
||
|
|
exported: bool,
|
||
|
|
/// Whether or not the variable is marked as read-only.
|
||
|
|
readonly: bool,
|
||
|
|
/// Whether or not the variable should be enumerated in the shell's environment.
|
||
|
|
enumerable: bool,
|
||
|
|
/// The transformation to apply to the variable's value when it is updated.
|
||
|
|
transform_on_update: ShellVariableUpdateTransform,
|
||
|
|
/// Whether or not the variable is marked as being traced.
|
||
|
|
trace: bool,
|
||
|
|
/// Whether or not the variable should be treated as an integer.
|
||
|
|
treat_as_integer: bool,
|
||
|
|
/// Whether or not the variable should be treated as a name reference.
|
||
|
|
treat_as_nameref: bool,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Kind of transformation to apply to a variable's value when it is updated.
|
||
|
|
#[derive(Clone, Copy, Debug)]
|
||
|
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||
|
|
pub enum ShellVariableUpdateTransform {
|
||
|
|
/// No transformation.
|
||
|
|
None,
|
||
|
|
/// Convert the value to lowercase.
|
||
|
|
Lowercase,
|
||
|
|
/// Convert the value to uppercase.
|
||
|
|
Uppercase,
|
||
|
|
/// Convert the value to lowercase, with the first character capitalized.
|
||
|
|
Capitalize,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Default for ShellVariable {
|
||
|
|
fn default() -> Self {
|
||
|
|
Self {
|
||
|
|
value: ShellValue::String(String::new()),
|
||
|
|
exported: false,
|
||
|
|
readonly: false,
|
||
|
|
enumerable: true,
|
||
|
|
transform_on_update: ShellVariableUpdateTransform::None,
|
||
|
|
trace: false,
|
||
|
|
treat_as_integer: false,
|
||
|
|
treat_as_nameref: false,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl ShellVariable {
|
||
|
|
/// Returns a new shell variable, initialized with the given value.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `value` - The value to associate with the variable.
|
||
|
|
pub fn new<I: Into<ShellValue>>(value: I) -> Self {
|
||
|
|
Self {
|
||
|
|
value: value.into(),
|
||
|
|
..Self::default()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns the value associated with the variable.
|
||
|
|
pub const fn value(&self) -> &ShellValue {
|
||
|
|
&self.value
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns whether or not the variable is exported to child processes.
|
||
|
|
pub const fn is_exported(&self) -> bool {
|
||
|
|
self.exported
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Marks the variable as exported to child processes.
|
||
|
|
pub const fn export(&mut self) -> &mut Self {
|
||
|
|
self.exported = true;
|
||
|
|
self
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Marks the variable as not exported to child processes.
|
||
|
|
pub const fn unexport(&mut self) -> &mut Self {
|
||
|
|
self.exported = false;
|
||
|
|
self
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns whether or not the variable is read-only.
|
||
|
|
pub const fn is_readonly(&self) -> bool {
|
||
|
|
self.readonly
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Marks the variable as read-only.
|
||
|
|
pub const fn set_readonly(&mut self) -> &mut Self {
|
||
|
|
self.readonly = true;
|
||
|
|
self
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Marks the variable as not read-only.
|
||
|
|
pub fn unset_readonly(&mut self) -> Result<&mut Self, error::Error> {
|
||
|
|
if self.readonly {
|
||
|
|
return Err(error::ErrorKind::ReadonlyVariable.into());
|
||
|
|
}
|
||
|
|
|
||
|
|
self.readonly = false;
|
||
|
|
Ok(self)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns whether or not the variable is traced.
|
||
|
|
pub const fn is_trace_enabled(&self) -> bool {
|
||
|
|
self.trace
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Marks the variable as traced.
|
||
|
|
pub const fn enable_trace(&mut self) -> &mut Self {
|
||
|
|
self.trace = true;
|
||
|
|
self
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Marks the variable as not traced.
|
||
|
|
pub const fn disable_trace(&mut self) -> &mut Self {
|
||
|
|
self.trace = false;
|
||
|
|
self
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns whether or not the variable should be enumerated in the shell's environment.
|
||
|
|
pub const fn is_enumerable(&self) -> bool {
|
||
|
|
self.enumerable
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Marks the variable as not enumerable in the shell's environment.
|
||
|
|
pub const fn hide_from_enumeration(&mut self) -> &mut Self {
|
||
|
|
self.enumerable = false;
|
||
|
|
self
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Return the update transform associated with the variable.
|
||
|
|
pub const fn get_update_transform(&self) -> ShellVariableUpdateTransform {
|
||
|
|
self.transform_on_update
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Set the update transform associated with the variable.
|
||
|
|
pub const fn set_update_transform(&mut self, transform: ShellVariableUpdateTransform) {
|
||
|
|
self.transform_on_update = transform;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns whether or not the variable should be treated as an integer.
|
||
|
|
pub const fn is_treated_as_integer(&self) -> bool {
|
||
|
|
self.treat_as_integer
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Marks the variable as being treated as an integer.
|
||
|
|
pub const fn treat_as_integer(&mut self) -> &mut Self {
|
||
|
|
self.treat_as_integer = true;
|
||
|
|
self
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Marks the variable as not being treated as an integer.
|
||
|
|
pub const fn unset_treat_as_integer(&mut self) -> &mut Self {
|
||
|
|
self.treat_as_integer = false;
|
||
|
|
self
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns whether or not the variable should be treated as a name reference.
|
||
|
|
pub const fn is_treated_as_nameref(&self) -> bool {
|
||
|
|
self.treat_as_nameref
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Marks the variable as being treated as a name reference.
|
||
|
|
pub const fn treat_as_nameref(&mut self) -> &mut Self {
|
||
|
|
self.treat_as_nameref = true;
|
||
|
|
self
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Marks the variable as not being treated as a name reference.
|
||
|
|
pub const fn unset_treat_as_nameref(&mut self) -> &mut Self {
|
||
|
|
self.treat_as_nameref = false;
|
||
|
|
self
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Converts the variable to an indexed array.
|
||
|
|
pub fn convert_to_indexed_array(&mut self) -> Result<(), error::Error> {
|
||
|
|
match self.value() {
|
||
|
|
ShellValue::IndexedArray(_) => Ok(()),
|
||
|
|
ShellValue::AssociativeArray(_) => {
|
||
|
|
Err(error::ErrorKind::ConvertingAssociativeArrayToIndexedArray.into())
|
||
|
|
}
|
||
|
|
_ => {
|
||
|
|
let mut new_values = BTreeMap::new();
|
||
|
|
new_values.insert(
|
||
|
|
0,
|
||
|
|
self.value.to_cow_str_without_dynamic_support().to_string(),
|
||
|
|
);
|
||
|
|
self.value = ShellValue::IndexedArray(new_values);
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Converts the variable to an associative array.
|
||
|
|
pub fn convert_to_associative_array(&mut self) -> Result<(), error::Error> {
|
||
|
|
match self.value() {
|
||
|
|
ShellValue::AssociativeArray(_) => Ok(()),
|
||
|
|
ShellValue::IndexedArray(_) => {
|
||
|
|
Err(error::ErrorKind::ConvertingIndexedArrayToAssociativeArray.into())
|
||
|
|
}
|
||
|
|
_ => {
|
||
|
|
let mut new_values: BTreeMap<String, String> = BTreeMap::new();
|
||
|
|
new_values.insert(
|
||
|
|
String::from("0"),
|
||
|
|
self.value.to_cow_str_without_dynamic_support().to_string(),
|
||
|
|
);
|
||
|
|
self.value = ShellValue::AssociativeArray(new_values);
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Assign the given value to the variable, conditionally appending to the preexisting value.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `value` - The value to assign to the variable.
|
||
|
|
/// * `append` - Whether or not to append the value to the preexisting value.
|
||
|
|
#[expect(clippy::too_many_lines)]
|
||
|
|
pub fn assign(&mut self, value: ShellValueLiteral, append: bool) -> Result<(), error::Error> {
|
||
|
|
if self.is_readonly() {
|
||
|
|
return Err(error::ErrorKind::ReadonlyVariable.into());
|
||
|
|
}
|
||
|
|
|
||
|
|
let value = self.convert_value_literal_for_assignment(value);
|
||
|
|
|
||
|
|
if append {
|
||
|
|
match (&self.value, &value) {
|
||
|
|
// If we're appending an array to a declared-but-unset variable (or appending
|
||
|
|
// anything to a declared-but-unset array), then fill it out first.
|
||
|
|
(ShellValue::Unset(_), ShellValueLiteral::Array(_))
|
||
|
|
| (
|
||
|
|
ShellValue::Unset(
|
||
|
|
ShellValueUnsetType::IndexedArray | ShellValueUnsetType::AssociativeArray,
|
||
|
|
),
|
||
|
|
_,
|
||
|
|
) => {
|
||
|
|
self.assign(ShellValueLiteral::Array(ArrayLiteral(vec![])), false)?;
|
||
|
|
}
|
||
|
|
// If we're appending a scalar to a declared-but-unset variable, then
|
||
|
|
// start with the empty string. This will result in the right thing happening,
|
||
|
|
// even in treat-as-integer cases.
|
||
|
|
(ShellValue::Unset(_), ShellValueLiteral::Scalar(_)) => {
|
||
|
|
self.assign(ShellValueLiteral::Scalar(String::new()), false)?;
|
||
|
|
}
|
||
|
|
// If we're trying to append an array to a string, we first promote the string to be
|
||
|
|
// an array with the string being present at index 0.
|
||
|
|
(ShellValue::String(_), ShellValueLiteral::Array(_)) => {
|
||
|
|
self.convert_to_indexed_array()?;
|
||
|
|
}
|
||
|
|
_ => (),
|
||
|
|
}
|
||
|
|
|
||
|
|
let treat_as_int = self.is_treated_as_integer();
|
||
|
|
let update_transform = self.get_update_transform();
|
||
|
|
|
||
|
|
match &mut self.value {
|
||
|
|
ShellValue::String(base) => match value {
|
||
|
|
ShellValueLiteral::Scalar(suffix) => {
|
||
|
|
if treat_as_int {
|
||
|
|
let int_value = base.parse::<i64>().unwrap_or(0)
|
||
|
|
+ suffix.parse::<i64>().unwrap_or(0);
|
||
|
|
base.clear();
|
||
|
|
base.push_str(int_value.to_string().as_str());
|
||
|
|
} else {
|
||
|
|
base.push_str(suffix.as_str());
|
||
|
|
Self::apply_value_transforms(base, treat_as_int, update_transform);
|
||
|
|
}
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
ShellValueLiteral::Array(_) => {
|
||
|
|
// This case was already handled (see above).
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
},
|
||
|
|
ShellValue::IndexedArray(existing_values) => match value {
|
||
|
|
ShellValueLiteral::Scalar(new_value) => {
|
||
|
|
self.assign_at_index(String::from("0"), new_value, append)
|
||
|
|
}
|
||
|
|
ShellValueLiteral::Array(new_values) => {
|
||
|
|
ShellValue::update_indexed_array_from_literals(existing_values, new_values);
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
},
|
||
|
|
ShellValue::AssociativeArray(existing_values) => match value {
|
||
|
|
ShellValueLiteral::Scalar(new_value) => {
|
||
|
|
self.assign_at_index(String::from("0"), new_value, append)
|
||
|
|
}
|
||
|
|
ShellValueLiteral::Array(new_values) => {
|
||
|
|
ShellValue::update_associative_array_from_literals(
|
||
|
|
existing_values,
|
||
|
|
new_values,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
},
|
||
|
|
ShellValue::Unset(_) => unreachable!("covered in conversion above"),
|
||
|
|
// TODO(dynamic): implement appending to dynamic vars
|
||
|
|
ShellValue::Dynamic { .. } => Ok(()),
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
match (&self.value, value) {
|
||
|
|
// If we're updating an array value with a string, then treat it as an update to
|
||
|
|
// just the "0"-indexed element of the array.
|
||
|
|
(
|
||
|
|
ShellValue::IndexedArray(_)
|
||
|
|
| ShellValue::AssociativeArray(_)
|
||
|
|
| ShellValue::Unset(
|
||
|
|
ShellValueUnsetType::AssociativeArray | ShellValueUnsetType::IndexedArray,
|
||
|
|
),
|
||
|
|
ShellValueLiteral::Scalar(s),
|
||
|
|
) => self.assign_at_index(String::from("0"), s, false),
|
||
|
|
|
||
|
|
// If we're updating an indexed array value with an array, then preserve the array
|
||
|
|
// type. We also default to using an indexed array if we are
|
||
|
|
// assigning an array to a previously string-holding variable.
|
||
|
|
(
|
||
|
|
ShellValue::IndexedArray(_)
|
||
|
|
| ShellValue::Unset(
|
||
|
|
ShellValueUnsetType::IndexedArray | ShellValueUnsetType::Untyped,
|
||
|
|
)
|
||
|
|
| ShellValue::String(_)
|
||
|
|
| ShellValue::Dynamic { .. },
|
||
|
|
ShellValueLiteral::Array(literal_values),
|
||
|
|
) => {
|
||
|
|
self.value = ShellValue::indexed_array_from_literals(literal_values);
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
// If we're updating an associative array value with an array, then preserve the
|
||
|
|
// array type.
|
||
|
|
(
|
||
|
|
ShellValue::AssociativeArray(_)
|
||
|
|
| ShellValue::Unset(ShellValueUnsetType::AssociativeArray),
|
||
|
|
ShellValueLiteral::Array(literal_values),
|
||
|
|
) => {
|
||
|
|
self.value = ShellValue::associative_array_from_literals(literal_values)?;
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle updates to dynamic values; for now we just drop them.
|
||
|
|
// TODO(dynamic): Allow updates to dynamic values
|
||
|
|
(ShellValue::Dynamic { .. }, _) => Ok(()),
|
||
|
|
|
||
|
|
// Assign a scalar value to a scalar or unset (and untyped) variable.
|
||
|
|
(ShellValue::String(_) | ShellValue::Unset(_), ShellValueLiteral::Scalar(s)) => {
|
||
|
|
self.value = ShellValue::String(s);
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Assign the given value to the variable at the given index, conditionally appending to the
|
||
|
|
/// preexisting value present at that element within the value.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `array_index` - The index at which to assign the value.
|
||
|
|
/// * `value` - The value to assign to the variable at the given index.
|
||
|
|
/// * `append` - Whether or not to append the value to the preexisting value stored at the given
|
||
|
|
/// index.
|
||
|
|
pub fn assign_at_index(
|
||
|
|
&mut self,
|
||
|
|
array_index: String,
|
||
|
|
value: String,
|
||
|
|
append: bool,
|
||
|
|
) -> Result<(), error::Error> {
|
||
|
|
match &self.value {
|
||
|
|
ShellValue::Unset(_) => {
|
||
|
|
self.assign(ShellValueLiteral::Array(ArrayLiteral(vec![])), false)?;
|
||
|
|
}
|
||
|
|
ShellValue::String(_) => {
|
||
|
|
self.convert_to_indexed_array()?;
|
||
|
|
}
|
||
|
|
_ => (),
|
||
|
|
}
|
||
|
|
|
||
|
|
let treat_as_int = self.is_treated_as_integer();
|
||
|
|
let value = self.convert_value_str_for_assignment(value);
|
||
|
|
|
||
|
|
match &mut self.value {
|
||
|
|
ShellValue::IndexedArray(arr) => {
|
||
|
|
let key = get_key_for_indexed_array(arr, array_index.as_str())?;
|
||
|
|
|
||
|
|
if append {
|
||
|
|
let existing_value = arr.get(&key).map_or_else(|| "", |v| v.as_str());
|
||
|
|
|
||
|
|
let mut new_value;
|
||
|
|
if treat_as_int {
|
||
|
|
new_value = (existing_value.parse::<i64>().unwrap_or(0)
|
||
|
|
+ value.parse::<i64>().unwrap_or(0))
|
||
|
|
.to_string();
|
||
|
|
} else {
|
||
|
|
new_value = existing_value.to_owned();
|
||
|
|
new_value.push_str(value.as_str());
|
||
|
|
}
|
||
|
|
|
||
|
|
arr.insert(key, new_value);
|
||
|
|
} else {
|
||
|
|
arr.insert(key, value);
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
ShellValue::AssociativeArray(arr) => {
|
||
|
|
if append {
|
||
|
|
let existing_value = arr
|
||
|
|
.get(array_index.as_str())
|
||
|
|
.map_or_else(|| "", |v| v.as_str());
|
||
|
|
|
||
|
|
let mut new_value;
|
||
|
|
if treat_as_int {
|
||
|
|
new_value = (existing_value.parse::<i64>().unwrap_or(0)
|
||
|
|
+ value.parse::<i64>().unwrap_or(0))
|
||
|
|
.to_string();
|
||
|
|
} else {
|
||
|
|
new_value = existing_value.to_owned();
|
||
|
|
new_value.push_str(value.as_str());
|
||
|
|
}
|
||
|
|
|
||
|
|
arr.insert(array_index, new_value.clone());
|
||
|
|
} else {
|
||
|
|
arr.insert(array_index, value);
|
||
|
|
}
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
_ => {
|
||
|
|
tracing::error!("assigning to index {array_index} of {:?}", self.value);
|
||
|
|
error::unimp("assigning to index of non-array variable")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn convert_value_literal_for_assignment(&self, value: ShellValueLiteral) -> ShellValueLiteral {
|
||
|
|
match value {
|
||
|
|
ShellValueLiteral::Scalar(s) => {
|
||
|
|
ShellValueLiteral::Scalar(self.convert_value_str_for_assignment(s))
|
||
|
|
}
|
||
|
|
ShellValueLiteral::Array(literals) => ShellValueLiteral::Array(ArrayLiteral(
|
||
|
|
literals
|
||
|
|
.0
|
||
|
|
.into_iter()
|
||
|
|
.map(|(k, v)| (k, self.convert_value_str_for_assignment(v)))
|
||
|
|
.collect(),
|
||
|
|
)),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn convert_value_str_for_assignment(&self, mut s: String) -> String {
|
||
|
|
Self::apply_value_transforms(
|
||
|
|
&mut s,
|
||
|
|
self.is_treated_as_integer(),
|
||
|
|
self.get_update_transform(),
|
||
|
|
);
|
||
|
|
|
||
|
|
s
|
||
|
|
}
|
||
|
|
|
||
|
|
fn apply_value_transforms(
|
||
|
|
s: &mut String,
|
||
|
|
treat_as_int: bool,
|
||
|
|
update_transform: ShellVariableUpdateTransform,
|
||
|
|
) {
|
||
|
|
if treat_as_int {
|
||
|
|
*s = (*s).parse::<i64>().unwrap_or(0).to_string();
|
||
|
|
} else {
|
||
|
|
match update_transform {
|
||
|
|
ShellVariableUpdateTransform::None => (),
|
||
|
|
ShellVariableUpdateTransform::Lowercase => *s = (*s).to_lowercase(),
|
||
|
|
ShellVariableUpdateTransform::Uppercase => *s = (*s).to_uppercase(),
|
||
|
|
ShellVariableUpdateTransform::Capitalize => {
|
||
|
|
// This isn't really title-case; only the first character is capitalized.
|
||
|
|
*s = s.to_lowercase();
|
||
|
|
if let Some(c) = s.chars().next() {
|
||
|
|
s.replace_range(0..1, &c.to_uppercase().to_string());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Tries to unset the value stored at the given index in the variable. Returns
|
||
|
|
/// whether or not a value was unset.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `index` - The index at which to unset the value.
|
||
|
|
pub fn unset_index(&mut self, index: &str) -> Result<bool, error::Error> {
|
||
|
|
match &mut self.value {
|
||
|
|
ShellValue::Unset(ty) => match ty {
|
||
|
|
ShellValueUnsetType::Untyped => Err(error::ErrorKind::NotArray.into()),
|
||
|
|
ShellValueUnsetType::AssociativeArray | ShellValueUnsetType::IndexedArray => {
|
||
|
|
Ok(false)
|
||
|
|
}
|
||
|
|
},
|
||
|
|
ShellValue::String(_) => Err(error::ErrorKind::NotArray.into()),
|
||
|
|
ShellValue::AssociativeArray(values) => Ok(values.remove(index).is_some()),
|
||
|
|
ShellValue::IndexedArray(values) => {
|
||
|
|
let key = get_key_for_indexed_array(values, index)?;
|
||
|
|
Ok(values.remove(&key).is_some())
|
||
|
|
}
|
||
|
|
ShellValue::Dynamic { .. } => Ok(false),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns the variable's value; for dynamic values, this will resolve the value.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `shell` - The shell in which the variable is being resolved.
|
||
|
|
pub fn resolve_value(&self, shell: &Shell<impl extensions::ShellExtensions>) -> ShellValue {
|
||
|
|
// N.B. We do *not* specially handle a dynamic value that resolves to a dynamic value.
|
||
|
|
match &self.value {
|
||
|
|
ShellValue::Dynamic { getter, .. } => getter(shell),
|
||
|
|
_ => self.value.clone(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns the canonical attribute flag string for this variable.
|
||
|
|
pub fn attribute_flags(&self, shell: &Shell<impl extensions::ShellExtensions>) -> String {
|
||
|
|
let value = self.resolve_value(shell);
|
||
|
|
|
||
|
|
let mut result = String::new();
|
||
|
|
|
||
|
|
if matches!(
|
||
|
|
value,
|
||
|
|
ShellValue::IndexedArray(_) | ShellValue::Unset(ShellValueUnsetType::IndexedArray)
|
||
|
|
) {
|
||
|
|
result.push('a');
|
||
|
|
}
|
||
|
|
if matches!(
|
||
|
|
value,
|
||
|
|
ShellValue::AssociativeArray(_)
|
||
|
|
| ShellValue::Unset(ShellValueUnsetType::AssociativeArray)
|
||
|
|
) {
|
||
|
|
result.push('A');
|
||
|
|
}
|
||
|
|
if matches!(
|
||
|
|
self.get_update_transform(),
|
||
|
|
ShellVariableUpdateTransform::Capitalize
|
||
|
|
) {
|
||
|
|
result.push('c');
|
||
|
|
}
|
||
|
|
if self.is_treated_as_integer() {
|
||
|
|
result.push('i');
|
||
|
|
}
|
||
|
|
if self.is_treated_as_nameref() {
|
||
|
|
result.push('n');
|
||
|
|
}
|
||
|
|
if self.is_readonly() {
|
||
|
|
result.push('r');
|
||
|
|
}
|
||
|
|
if matches!(
|
||
|
|
self.get_update_transform(),
|
||
|
|
ShellVariableUpdateTransform::Lowercase
|
||
|
|
) {
|
||
|
|
result.push('l');
|
||
|
|
}
|
||
|
|
if self.is_trace_enabled() {
|
||
|
|
result.push('t');
|
||
|
|
}
|
||
|
|
if matches!(
|
||
|
|
self.get_update_transform(),
|
||
|
|
ShellVariableUpdateTransform::Uppercase
|
||
|
|
) {
|
||
|
|
result.push('u');
|
||
|
|
}
|
||
|
|
if self.is_exported() {
|
||
|
|
result.push('x');
|
||
|
|
}
|
||
|
|
|
||
|
|
result
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
type DynamicValueGetter = fn(&dyn ShellState) -> ShellValue;
|
||
|
|
type DynamicValueSetter = fn(&dyn ShellState) -> ();
|
||
|
|
|
||
|
|
/// A shell value.
|
||
|
|
#[derive(Clone, Debug)]
|
||
|
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||
|
|
pub enum ShellValue {
|
||
|
|
/// A value that has been typed but not yet set.
|
||
|
|
Unset(ShellValueUnsetType),
|
||
|
|
/// A string.
|
||
|
|
String(String),
|
||
|
|
/// An associative array.
|
||
|
|
AssociativeArray(BTreeMap<String, String>),
|
||
|
|
/// An indexed array.
|
||
|
|
IndexedArray(BTreeMap<u64, String>),
|
||
|
|
/// A value that is dynamically computed.
|
||
|
|
Dynamic {
|
||
|
|
/// Function that can query the value.
|
||
|
|
/// TODO(serde): figure out how to serialize/deserialize dynamic values.
|
||
|
|
#[cfg_attr(
|
||
|
|
feature = "serde",
|
||
|
|
serde(skip, default = "default_dynamic_value_getter")
|
||
|
|
)]
|
||
|
|
getter: DynamicValueGetter,
|
||
|
|
/// Function that receives value update requests.
|
||
|
|
/// TODO(serde): figure out how to serialize/deserialize dynamic values.
|
||
|
|
#[cfg_attr(
|
||
|
|
feature = "serde",
|
||
|
|
serde(skip, default = "default_dynamic_value_setter")
|
||
|
|
)]
|
||
|
|
setter: DynamicValueSetter,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(feature = "serde")]
|
||
|
|
fn default_dynamic_value_getter() -> DynamicValueGetter {
|
||
|
|
|_shell: &dyn ShellState| ShellValue::String(String::new())
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(feature = "serde")]
|
||
|
|
fn default_dynamic_value_setter() -> DynamicValueSetter {
|
||
|
|
|_shell: &dyn ShellState| {}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The type of an unset shell value.
|
||
|
|
#[derive(Clone, Debug)]
|
||
|
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||
|
|
pub enum ShellValueUnsetType {
|
||
|
|
/// The value is untyped.
|
||
|
|
Untyped,
|
||
|
|
/// The value is an associative array.
|
||
|
|
AssociativeArray,
|
||
|
|
/// The value is an indexed array.
|
||
|
|
IndexedArray,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// A shell value literal; used for assignment.
|
||
|
|
#[derive(Clone, Debug)]
|
||
|
|
pub enum ShellValueLiteral {
|
||
|
|
/// A scalar value.
|
||
|
|
Scalar(String),
|
||
|
|
/// An array value.
|
||
|
|
Array(ArrayLiteral),
|
||
|
|
}
|
||
|
|
|
||
|
|
impl ShellValueLiteral {
|
||
|
|
pub(crate) fn fmt_for_tracing(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
|
|
match self {
|
||
|
|
Self::Scalar(s) => Self::fmt_scalar_for_tracing(s.as_str(), f),
|
||
|
|
Self::Array(elements) => {
|
||
|
|
write!(f, "(")?;
|
||
|
|
for (i, (key, value)) in elements.0.iter().enumerate() {
|
||
|
|
if i > 0 {
|
||
|
|
write!(f, " ")?;
|
||
|
|
}
|
||
|
|
if let Some(key) = key {
|
||
|
|
write!(f, "[")?;
|
||
|
|
Self::fmt_scalar_for_tracing(key.as_str(), f)?;
|
||
|
|
write!(f, "]=")?;
|
||
|
|
}
|
||
|
|
Self::fmt_scalar_for_tracing(value.as_str(), f)?;
|
||
|
|
}
|
||
|
|
write!(f, ")")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn fmt_scalar_for_tracing(s: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
|
|
let processed = escape::quote_if_needed(s, escape::QuoteMode::SingleQuote);
|
||
|
|
write!(f, "{processed}")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Display for ShellValueLiteral {
|
||
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
|
|
self.fmt_for_tracing(f)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<&str> for ShellValueLiteral {
|
||
|
|
fn from(value: &str) -> Self {
|
||
|
|
Self::Scalar(value.to_owned())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<String> for ShellValueLiteral {
|
||
|
|
fn from(value: String) -> Self {
|
||
|
|
Self::Scalar(value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<Vec<&str>> for ShellValueLiteral {
|
||
|
|
fn from(value: Vec<&str>) -> Self {
|
||
|
|
Self::Array(ArrayLiteral(
|
||
|
|
value.into_iter().map(|s| (None, s.to_owned())).collect(),
|
||
|
|
))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// An array literal.
|
||
|
|
#[derive(Clone, Debug)]
|
||
|
|
pub struct ArrayLiteral(pub Vec<(Option<String>, String)>);
|
||
|
|
|
||
|
|
/// Style for formatting a shell variable's value.
|
||
|
|
#[derive(Copy, Clone, Debug)]
|
||
|
|
pub enum FormatStyle {
|
||
|
|
/// Basic formatting.
|
||
|
|
Basic,
|
||
|
|
/// Formatting as appropriate in the `declare` built-in command.
|
||
|
|
DeclarePrint,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl ShellValue {
|
||
|
|
/// Returns whether or not the value is an array.
|
||
|
|
pub const fn is_array(&self) -> bool {
|
||
|
|
matches!(
|
||
|
|
self,
|
||
|
|
Self::IndexedArray(_)
|
||
|
|
| Self::AssociativeArray(_)
|
||
|
|
| Self::Unset(
|
||
|
|
ShellValueUnsetType::IndexedArray | ShellValueUnsetType::AssociativeArray
|
||
|
|
)
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns whether or not the value is set.
|
||
|
|
pub const fn is_set(&self) -> bool {
|
||
|
|
!matches!(self, Self::Unset(_))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns a new indexed array value constructed from the given slice of owned strings.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `values` - The slice of strings to construct the indexed array from.
|
||
|
|
pub fn indexed_array_from_strings<S>(values: S) -> Self
|
||
|
|
where
|
||
|
|
S: IntoIterator<Item = String>,
|
||
|
|
{
|
||
|
|
let mut owned_values = BTreeMap::new();
|
||
|
|
for (i, value) in values.into_iter().enumerate() {
|
||
|
|
owned_values.insert(i as u64, value);
|
||
|
|
}
|
||
|
|
|
||
|
|
Self::IndexedArray(owned_values)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns a new indexed array value constructed from the given slice of unowned strings.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `values` - The slice of strings to construct the indexed array from.
|
||
|
|
pub fn indexed_array_from_strs(values: &[&str]) -> Self {
|
||
|
|
let mut owned_values = BTreeMap::new();
|
||
|
|
for (i, value) in values.iter().enumerate() {
|
||
|
|
owned_values.insert(i as u64, (*value).to_string());
|
||
|
|
}
|
||
|
|
|
||
|
|
Self::IndexedArray(owned_values)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns a new indexed array value constructed from the given literals.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `literals` - The literals to construct the indexed array from.
|
||
|
|
pub fn indexed_array_from_literals(literals: ArrayLiteral) -> Self {
|
||
|
|
let mut values = BTreeMap::new();
|
||
|
|
Self::update_indexed_array_from_literals(&mut values, literals);
|
||
|
|
|
||
|
|
Self::IndexedArray(values)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn update_indexed_array_from_literals(
|
||
|
|
existing_values: &mut BTreeMap<u64, String>,
|
||
|
|
literal_values: ArrayLiteral,
|
||
|
|
) {
|
||
|
|
let mut new_key = if let Some((largest_index, _)) = existing_values.last_key_value() {
|
||
|
|
largest_index + 1
|
||
|
|
} else {
|
||
|
|
0
|
||
|
|
};
|
||
|
|
|
||
|
|
for (key, value) in literal_values.0 {
|
||
|
|
if let Some(key) = key {
|
||
|
|
new_key = key.parse().unwrap_or(0);
|
||
|
|
}
|
||
|
|
|
||
|
|
existing_values.insert(new_key, value);
|
||
|
|
new_key += 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns a new associative array value constructed from the given literals.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `literals` - The literals to construct the associative array from.
|
||
|
|
pub fn associative_array_from_literals(literals: ArrayLiteral) -> Result<Self, error::Error> {
|
||
|
|
let mut values = BTreeMap::new();
|
||
|
|
Self::update_associative_array_from_literals(&mut values, literals)?;
|
||
|
|
|
||
|
|
Ok(Self::AssociativeArray(values))
|
||
|
|
}
|
||
|
|
|
||
|
|
fn update_associative_array_from_literals(
|
||
|
|
existing_values: &mut BTreeMap<String, String>,
|
||
|
|
literal_values: ArrayLiteral,
|
||
|
|
) -> Result<(), error::Error> {
|
||
|
|
let mut current_key = None;
|
||
|
|
for (key, value) in literal_values.0 {
|
||
|
|
if let Some(current_key) = current_key.take() {
|
||
|
|
if key.is_some() {
|
||
|
|
return error::unimp("misaligned keys/values in associative array literal");
|
||
|
|
} else {
|
||
|
|
existing_values.insert(current_key, value);
|
||
|
|
}
|
||
|
|
} else if let Some(key) = key {
|
||
|
|
existing_values.insert(key, value);
|
||
|
|
} else {
|
||
|
|
current_key = Some(value);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if let Some(current_key) = current_key {
|
||
|
|
existing_values.insert(current_key, String::new());
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Formats the value using the given style.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `style` - The style to use for formatting the value.
|
||
|
|
pub fn format(
|
||
|
|
&self,
|
||
|
|
style: FormatStyle,
|
||
|
|
shell: &Shell<impl extensions::ShellExtensions>,
|
||
|
|
) -> Result<Cow<'_, str>, error::Error> {
|
||
|
|
match self {
|
||
|
|
Self::Unset(_) => Ok("".into()),
|
||
|
|
Self::String(s) => match style {
|
||
|
|
FormatStyle::Basic => Ok(escape::quote_if_needed(
|
||
|
|
s.as_str(),
|
||
|
|
escape::QuoteMode::SingleQuote,
|
||
|
|
)),
|
||
|
|
FormatStyle::DeclarePrint => {
|
||
|
|
Ok(escape::force_quote(s.as_str(), escape::QuoteMode::DoubleQuote).into())
|
||
|
|
}
|
||
|
|
},
|
||
|
|
Self::AssociativeArray(values) => {
|
||
|
|
let mut result = String::new();
|
||
|
|
result.push('(');
|
||
|
|
|
||
|
|
for (key, value) in values {
|
||
|
|
let formatted_key =
|
||
|
|
escape::quote_if_needed(key.as_str(), escape::QuoteMode::DoubleQuote);
|
||
|
|
let formatted_value =
|
||
|
|
escape::force_quote(value.as_str(), escape::QuoteMode::DoubleQuote);
|
||
|
|
|
||
|
|
// N.B. We include an unconditional trailing space character (even after the
|
||
|
|
// last entry in the associative array) to match standard
|
||
|
|
// output behavior.
|
||
|
|
write!(result, "[{formatted_key}]={formatted_value} ")?;
|
||
|
|
}
|
||
|
|
|
||
|
|
result.push(')');
|
||
|
|
Ok(result.into())
|
||
|
|
}
|
||
|
|
Self::IndexedArray(values) => {
|
||
|
|
let mut result = String::new();
|
||
|
|
result.push('(');
|
||
|
|
|
||
|
|
for (i, (key, value)) in values.iter().enumerate() {
|
||
|
|
if i > 0 {
|
||
|
|
result.push(' ');
|
||
|
|
}
|
||
|
|
|
||
|
|
let formatted_value =
|
||
|
|
escape::force_quote(value.as_str(), escape::QuoteMode::DoubleQuote);
|
||
|
|
write!(result, "[{key}]={formatted_value}")?;
|
||
|
|
}
|
||
|
|
|
||
|
|
result.push(')');
|
||
|
|
Ok(result.into())
|
||
|
|
}
|
||
|
|
Self::Dynamic { getter, .. } => {
|
||
|
|
let dynamic_value = getter(shell);
|
||
|
|
let result = dynamic_value.format(style, shell)?.to_string();
|
||
|
|
Ok(result.into())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Tries to retrieve the value stored at the given index in this variable.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `index` - The index at which to retrieve the value.
|
||
|
|
pub fn get_at(
|
||
|
|
&self,
|
||
|
|
index: &str,
|
||
|
|
shell: &Shell<impl extensions::ShellExtensions>,
|
||
|
|
) -> Result<Option<Cow<'_, str>>, error::Error> {
|
||
|
|
match self {
|
||
|
|
Self::Unset(_) => Ok(None),
|
||
|
|
Self::String(s) => {
|
||
|
|
if index.parse::<u64>().unwrap_or(0) == 0 {
|
||
|
|
Ok(Some(Cow::Borrowed(s)))
|
||
|
|
} else {
|
||
|
|
Ok(None)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Self::AssociativeArray(values) => {
|
||
|
|
Ok(values.get(index).map(|s| Cow::Borrowed(s.as_str())))
|
||
|
|
}
|
||
|
|
Self::IndexedArray(values) => {
|
||
|
|
let key = get_key_for_indexed_array(values, index)?;
|
||
|
|
Ok(values.get(&key).map(|s| Cow::Borrowed(s.as_str())))
|
||
|
|
}
|
||
|
|
Self::Dynamic { getter, .. } => {
|
||
|
|
let dynamic_value = getter(shell);
|
||
|
|
let result = dynamic_value.get_at(index, shell)?;
|
||
|
|
Ok(result.map(|s| s.to_string().into()))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns the keys of the elements in this variable.
|
||
|
|
pub fn element_keys(&self, shell: &Shell<impl extensions::ShellExtensions>) -> Vec<String> {
|
||
|
|
match self {
|
||
|
|
Self::Unset(_) => vec![],
|
||
|
|
Self::String(_) => vec!["0".to_owned()],
|
||
|
|
Self::AssociativeArray(array) => array.keys().map(|k| k.to_owned()).collect(),
|
||
|
|
Self::IndexedArray(array) => array.keys().map(|k| k.to_string()).collect(),
|
||
|
|
Self::Dynamic { getter, .. } => getter(shell).element_keys(shell),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns the values of the elements in this variable.
|
||
|
|
pub fn element_values(&self, shell: &Shell<impl extensions::ShellExtensions>) -> Vec<String> {
|
||
|
|
match self {
|
||
|
|
Self::Unset(_) => vec![],
|
||
|
|
Self::String(s) => vec![s.to_owned()],
|
||
|
|
Self::AssociativeArray(array) => array.values().map(|v| v.to_owned()).collect(),
|
||
|
|
Self::IndexedArray(array) => array.values().map(|v| v.to_owned()).collect(),
|
||
|
|
Self::Dynamic { getter, .. } => getter(shell).element_values(shell),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Converts this value to a string.
|
||
|
|
pub fn to_cow_str(&self, shell: &Shell<impl extensions::ShellExtensions>) -> Cow<'_, str> {
|
||
|
|
self.try_get_cow_str(shell).unwrap_or(Cow::Borrowed(""))
|
||
|
|
}
|
||
|
|
|
||
|
|
fn to_cow_str_without_dynamic_support(&self) -> Cow<'_, str> {
|
||
|
|
self.try_get_cow_str_without_dynamic_support()
|
||
|
|
.unwrap_or(Cow::Borrowed(""))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Tries to convert this value to a string; returns `None` if the value is unset
|
||
|
|
/// or otherwise doesn't exist.
|
||
|
|
pub fn try_get_cow_str(
|
||
|
|
&self,
|
||
|
|
shell: &Shell<impl extensions::ShellExtensions>,
|
||
|
|
) -> Option<Cow<'_, str>> {
|
||
|
|
match self {
|
||
|
|
Self::Dynamic { getter, .. } => {
|
||
|
|
let dynamic_value = getter(shell);
|
||
|
|
dynamic_value
|
||
|
|
.try_get_cow_str(shell)
|
||
|
|
.map(|s| s.to_string().into())
|
||
|
|
}
|
||
|
|
_ => self.try_get_cow_str_without_dynamic_support(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn try_get_cow_str_without_dynamic_support(&self) -> Option<Cow<'_, str>> {
|
||
|
|
match self {
|
||
|
|
Self::Unset(_) => None,
|
||
|
|
Self::String(s) => Some(Cow::Borrowed(s.as_str())),
|
||
|
|
Self::AssociativeArray(values) => values.get("0").map(|s| Cow::Borrowed(s.as_str())),
|
||
|
|
Self::IndexedArray(values) => values.get(&0).map(|s| Cow::Borrowed(s.as_str())),
|
||
|
|
Self::Dynamic { .. } => None,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Formats this value as a program string usable in an assignment.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `index` - The index at which to retrieve the value, if indexing is to be performed.
|
||
|
|
pub fn to_assignable_str(
|
||
|
|
&self,
|
||
|
|
index: Option<&str>,
|
||
|
|
shell: &Shell<impl extensions::ShellExtensions>,
|
||
|
|
) -> Result<String, error::Error> {
|
||
|
|
match self {
|
||
|
|
Self::Unset(_) => Ok(String::new()),
|
||
|
|
Self::String(s) => Ok(escape::force_quote(
|
||
|
|
s.as_str(),
|
||
|
|
escape::QuoteMode::SingleQuote,
|
||
|
|
)),
|
||
|
|
Self::AssociativeArray(_) | Self::IndexedArray(_) => {
|
||
|
|
if let Some(index) = index {
|
||
|
|
if let Ok(Some(value)) = self.get_at(index, shell) {
|
||
|
|
Ok(escape::force_quote(
|
||
|
|
value.as_ref(),
|
||
|
|
escape::QuoteMode::SingleQuote,
|
||
|
|
))
|
||
|
|
} else {
|
||
|
|
Ok(String::new())
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
Ok(self.format(FormatStyle::DeclarePrint, shell)?.into_owned())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Self::Dynamic { getter, .. } => getter(shell).to_assignable_str(index, shell),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn get_key_for_indexed_array(
|
||
|
|
values: &BTreeMap<u64, String>,
|
||
|
|
index_str: &str,
|
||
|
|
) -> Result<u64, error::Error> {
|
||
|
|
let mut index_value = index_str.parse::<i64>().unwrap_or(0);
|
||
|
|
|
||
|
|
// Handle negative indices, but check for out-of-range values.
|
||
|
|
#[expect(clippy::cast_possible_wrap)]
|
||
|
|
if index_value < 0 {
|
||
|
|
index_value += values.len() as i64;
|
||
|
|
if index_value < 0 {
|
||
|
|
return Err(error::ErrorKind::ArrayIndexOutOfRange(index_str.to_owned()).into());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Now that we've confirmed that the index is non-negative, we can safely convert it
|
||
|
|
// to a u64 without any fuss.
|
||
|
|
#[expect(clippy::cast_sign_loss)]
|
||
|
|
Ok(index_value as u64)
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<&str> for ShellValue {
|
||
|
|
fn from(value: &str) -> Self {
|
||
|
|
Self::String(value.to_owned())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<&String> for ShellValue {
|
||
|
|
fn from(value: &String) -> Self {
|
||
|
|
Self::String(value.clone())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<String> for ShellValue {
|
||
|
|
fn from(value: String) -> Self {
|
||
|
|
Self::String(value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<OsString> for ShellValue {
|
||
|
|
fn from(value: OsString) -> Self {
|
||
|
|
Self::String(value.to_string_lossy().into_owned())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<Vec<String>> for ShellValue {
|
||
|
|
fn from(values: Vec<String>) -> Self {
|
||
|
|
Self::indexed_array_from_strings(values)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<Vec<&str>> for ShellValue {
|
||
|
|
fn from(values: Vec<&str>) -> Self {
|
||
|
|
Self::indexed_array_from_strs(values.as_slice())
|
||
|
|
}
|
||
|
|
}
|