Skip to main content

brush_core/
variables.rs

1//! Implements variables for a shell environment.
2
3use std::borrow::Cow;
4use std::collections::BTreeMap;
5use std::ffi::OsString;
6use std::fmt::{Display, Write};
7
8use crate::shell::{Shell, ShellState};
9use crate::{error, escape, extensions};
10
11/// A shell variable.
12#[derive(Clone, Debug)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub struct ShellVariable {
15    /// The value currently associated with the variable.
16    value: ShellValue,
17    /// Whether or not the variable is marked as exported to child processes.
18    exported: bool,
19    /// Whether or not the variable is marked as read-only.
20    readonly: bool,
21    /// Whether or not the variable should be enumerated in the shell's environment.
22    enumerable: bool,
23    /// The transformation to apply to the variable's value when it is updated.
24    transform_on_update: ShellVariableUpdateTransform,
25    /// Whether or not the variable is marked as being traced.
26    trace: bool,
27    /// Whether or not the variable should be treated as an integer.
28    treat_as_integer: bool,
29    /// Whether or not the variable should be treated as a name reference.
30    treat_as_nameref: bool,
31}
32
33/// Kind of transformation to apply to a variable's value when it is updated.
34#[derive(Clone, Copy, Debug)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
36pub enum ShellVariableUpdateTransform {
37    /// No transformation.
38    None,
39    /// Convert the value to lowercase.
40    Lowercase,
41    /// Convert the value to uppercase.
42    Uppercase,
43    /// Convert the value to lowercase, with the first character capitalized.
44    Capitalize,
45}
46
47impl Default for ShellVariable {
48    fn default() -> Self {
49        Self {
50            value: ShellValue::String(String::new()),
51            exported: false,
52            readonly: false,
53            enumerable: true,
54            transform_on_update: ShellVariableUpdateTransform::None,
55            trace: false,
56            treat_as_integer: false,
57            treat_as_nameref: false,
58        }
59    }
60}
61
62impl ShellVariable {
63    /// Returns a new shell variable, initialized with the given value.
64    ///
65    /// # Arguments
66    ///
67    /// * `value` - The value to associate with the variable.
68    pub fn new<I: Into<ShellValue>>(value: I) -> Self {
69        Self {
70            value: value.into(),
71            ..Self::default()
72        }
73    }
74
75    /// Returns the value associated with the variable.
76    pub const fn value(&self) -> &ShellValue {
77        &self.value
78    }
79
80    /// Returns whether or not the variable is exported to child processes.
81    pub const fn is_exported(&self) -> bool {
82        self.exported
83    }
84
85    /// Marks the variable as exported to child processes.
86    pub const fn export(&mut self) -> &mut Self {
87        self.exported = true;
88        self
89    }
90
91    /// Marks the variable as not exported to child processes.
92    pub const fn unexport(&mut self) -> &mut Self {
93        self.exported = false;
94        self
95    }
96
97    /// Returns whether or not the variable is read-only.
98    pub const fn is_readonly(&self) -> bool {
99        self.readonly
100    }
101
102    /// Marks the variable as read-only.
103    pub const fn set_readonly(&mut self) -> &mut Self {
104        self.readonly = true;
105        self
106    }
107
108    /// Marks the variable as not read-only.
109    pub fn unset_readonly(&mut self) -> Result<&mut Self, error::Error> {
110        if self.readonly {
111            return Err(error::ErrorKind::ReadonlyVariable.into());
112        }
113
114        self.readonly = false;
115        Ok(self)
116    }
117
118    /// Returns whether or not the variable is traced.
119    pub const fn is_trace_enabled(&self) -> bool {
120        self.trace
121    }
122
123    /// Marks the variable as traced.
124    pub const fn enable_trace(&mut self) -> &mut Self {
125        self.trace = true;
126        self
127    }
128
129    /// Marks the variable as not traced.
130    pub const fn disable_trace(&mut self) -> &mut Self {
131        self.trace = false;
132        self
133    }
134
135    /// Returns whether or not the variable should be enumerated in the shell's environment.
136    pub const fn is_enumerable(&self) -> bool {
137        self.enumerable
138    }
139
140    /// Marks the variable as not enumerable in the shell's environment.
141    pub const fn hide_from_enumeration(&mut self) -> &mut Self {
142        self.enumerable = false;
143        self
144    }
145
146    /// Return the update transform associated with the variable.
147    pub const fn get_update_transform(&self) -> ShellVariableUpdateTransform {
148        self.transform_on_update
149    }
150
151    /// Set the update transform associated with the variable.
152    pub const fn set_update_transform(&mut self, transform: ShellVariableUpdateTransform) {
153        self.transform_on_update = transform;
154    }
155
156    /// Returns whether or not the variable should be treated as an integer.
157    pub const fn is_treated_as_integer(&self) -> bool {
158        self.treat_as_integer
159    }
160
161    /// Marks the variable as being treated as an integer.
162    pub const fn treat_as_integer(&mut self) -> &mut Self {
163        self.treat_as_integer = true;
164        self
165    }
166
167    /// Marks the variable as not being treated as an integer.
168    pub const fn unset_treat_as_integer(&mut self) -> &mut Self {
169        self.treat_as_integer = false;
170        self
171    }
172
173    /// Returns whether or not the variable should be treated as a name reference.
174    pub const fn is_treated_as_nameref(&self) -> bool {
175        self.treat_as_nameref
176    }
177
178    /// Marks the variable as being treated as a name reference.
179    pub const fn treat_as_nameref(&mut self) -> &mut Self {
180        self.treat_as_nameref = true;
181        self
182    }
183
184    /// Marks the variable as not being treated as a name reference.
185    pub const fn unset_treat_as_nameref(&mut self) -> &mut Self {
186        self.treat_as_nameref = false;
187        self
188    }
189
190    /// Converts the variable to an indexed array.
191    pub fn convert_to_indexed_array(&mut self) -> Result<(), error::Error> {
192        match self.value() {
193            ShellValue::IndexedArray(_) => Ok(()),
194            ShellValue::AssociativeArray(_) => {
195                Err(error::ErrorKind::ConvertingAssociativeArrayToIndexedArray.into())
196            }
197            _ => {
198                let mut new_values = BTreeMap::new();
199                new_values.insert(
200                    0,
201                    self.value.to_cow_str_without_dynamic_support().to_string(),
202                );
203                self.value = ShellValue::IndexedArray(new_values);
204                Ok(())
205            }
206        }
207    }
208
209    /// Converts the variable to an associative array.
210    pub fn convert_to_associative_array(&mut self) -> Result<(), error::Error> {
211        match self.value() {
212            ShellValue::AssociativeArray(_) => Ok(()),
213            ShellValue::IndexedArray(_) => {
214                Err(error::ErrorKind::ConvertingIndexedArrayToAssociativeArray.into())
215            }
216            _ => {
217                let mut new_values: BTreeMap<String, String> = BTreeMap::new();
218                new_values.insert(
219                    String::from("0"),
220                    self.value.to_cow_str_without_dynamic_support().to_string(),
221                );
222                self.value = ShellValue::AssociativeArray(new_values);
223                Ok(())
224            }
225        }
226    }
227
228    /// Assign the given value to the variable, conditionally appending to the preexisting value.
229    ///
230    /// # Arguments
231    ///
232    /// * `value` - The value to assign to the variable.
233    /// * `append` - Whether or not to append the value to the preexisting value.
234    #[expect(clippy::too_many_lines)]
235    pub fn assign(&mut self, value: ShellValueLiteral, append: bool) -> Result<(), error::Error> {
236        if self.is_readonly() {
237            return Err(error::ErrorKind::ReadonlyVariable.into());
238        }
239
240        let value = self.convert_value_literal_for_assignment(value);
241
242        if append {
243            match (&self.value, &value) {
244                // If we're appending an array to a declared-but-unset variable (or appending
245                // anything to a declared-but-unset array), then fill it out first.
246                (ShellValue::Unset(_), ShellValueLiteral::Array(_))
247                | (
248                    ShellValue::Unset(
249                        ShellValueUnsetType::IndexedArray | ShellValueUnsetType::AssociativeArray,
250                    ),
251                    _,
252                ) => {
253                    self.assign(ShellValueLiteral::Array(ArrayLiteral(vec![])), false)?;
254                }
255                // If we're appending a scalar to a declared-but-unset variable, then
256                // start with the empty string. This will result in the right thing happening,
257                // even in treat-as-integer cases.
258                (ShellValue::Unset(_), ShellValueLiteral::Scalar(_)) => {
259                    self.assign(ShellValueLiteral::Scalar(String::new()), false)?;
260                }
261                // If we're trying to append an array to a string, we first promote the string to be
262                // an array with the string being present at index 0.
263                (ShellValue::String(_), ShellValueLiteral::Array(_)) => {
264                    self.convert_to_indexed_array()?;
265                }
266                _ => (),
267            }
268
269            let treat_as_int = self.is_treated_as_integer();
270            let update_transform = self.get_update_transform();
271
272            match &mut self.value {
273                ShellValue::String(base) => match value {
274                    ShellValueLiteral::Scalar(suffix) => {
275                        if treat_as_int {
276                            let int_value = base.parse::<i64>().unwrap_or(0)
277                                + suffix.parse::<i64>().unwrap_or(0);
278                            base.clear();
279                            base.push_str(int_value.to_string().as_str());
280                        } else {
281                            base.push_str(suffix.as_str());
282                            Self::apply_value_transforms(base, treat_as_int, update_transform);
283                        }
284                        Ok(())
285                    }
286                    ShellValueLiteral::Array(_) => {
287                        // This case was already handled (see above).
288                        Ok(())
289                    }
290                },
291                ShellValue::IndexedArray(existing_values) => match value {
292                    ShellValueLiteral::Scalar(new_value) => {
293                        self.assign_at_index(String::from("0"), new_value, append)
294                    }
295                    ShellValueLiteral::Array(new_values) => {
296                        ShellValue::update_indexed_array_from_literals(existing_values, new_values);
297                        Ok(())
298                    }
299                },
300                ShellValue::AssociativeArray(existing_values) => match value {
301                    ShellValueLiteral::Scalar(new_value) => {
302                        self.assign_at_index(String::from("0"), new_value, append)
303                    }
304                    ShellValueLiteral::Array(new_values) => {
305                        ShellValue::update_associative_array_from_literals(
306                            existing_values,
307                            new_values,
308                        )
309                    }
310                },
311                ShellValue::Unset(_) => unreachable!("covered in conversion above"),
312                // TODO(dynamic): implement appending to dynamic vars
313                ShellValue::Dynamic { .. } => Ok(()),
314            }
315        } else {
316            match (&self.value, value) {
317                // If we're updating an array value with a string, then treat it as an update to
318                // just the "0"-indexed element of the array.
319                (
320                    ShellValue::IndexedArray(_)
321                    | ShellValue::AssociativeArray(_)
322                    | ShellValue::Unset(
323                        ShellValueUnsetType::AssociativeArray | ShellValueUnsetType::IndexedArray,
324                    ),
325                    ShellValueLiteral::Scalar(s),
326                ) => self.assign_at_index(String::from("0"), s, false),
327
328                // If we're updating an indexed array value with an array, then preserve the array
329                // type. We also default to using an indexed array if we are
330                // assigning an array to a previously string-holding variable.
331                (
332                    ShellValue::IndexedArray(_)
333                    | ShellValue::Unset(
334                        ShellValueUnsetType::IndexedArray | ShellValueUnsetType::Untyped,
335                    )
336                    | ShellValue::String(_)
337                    | ShellValue::Dynamic { .. },
338                    ShellValueLiteral::Array(literal_values),
339                ) => {
340                    self.value = ShellValue::indexed_array_from_literals(literal_values);
341                    Ok(())
342                }
343
344                // If we're updating an associative array value with an array, then preserve the
345                // array type.
346                (
347                    ShellValue::AssociativeArray(_)
348                    | ShellValue::Unset(ShellValueUnsetType::AssociativeArray),
349                    ShellValueLiteral::Array(literal_values),
350                ) => {
351                    self.value = ShellValue::associative_array_from_literals(literal_values)?;
352                    Ok(())
353                }
354
355                // Handle updates to dynamic values; for now we just drop them.
356                // TODO(dynamic): Allow updates to dynamic values
357                (ShellValue::Dynamic { .. }, _) => Ok(()),
358
359                // Assign a scalar value to a scalar or unset (and untyped) variable.
360                (ShellValue::String(_) | ShellValue::Unset(_), ShellValueLiteral::Scalar(s)) => {
361                    self.value = ShellValue::String(s);
362                    Ok(())
363                }
364            }
365        }
366    }
367
368    /// Assign the given value to the variable at the given index, conditionally appending to the
369    /// preexisting value present at that element within the value.
370    ///
371    /// # Arguments
372    ///
373    /// * `array_index` - The index at which to assign the value.
374    /// * `value` - The value to assign to the variable at the given index.
375    /// * `append` - Whether or not to append the value to the preexisting value stored at the given
376    ///   index.
377    pub fn assign_at_index(
378        &mut self,
379        array_index: String,
380        value: String,
381        append: bool,
382    ) -> Result<(), error::Error> {
383        match &self.value {
384            ShellValue::Unset(_) => {
385                self.assign(ShellValueLiteral::Array(ArrayLiteral(vec![])), false)?;
386            }
387            ShellValue::String(_) => {
388                self.convert_to_indexed_array()?;
389            }
390            _ => (),
391        }
392
393        let treat_as_int = self.is_treated_as_integer();
394        let value = self.convert_value_str_for_assignment(value);
395
396        match &mut self.value {
397            ShellValue::IndexedArray(arr) => {
398                let key = get_key_for_indexed_array(arr, array_index.as_str())?;
399
400                if append {
401                    let existing_value = arr.get(&key).map_or_else(|| "", |v| v.as_str());
402
403                    let mut new_value;
404                    if treat_as_int {
405                        new_value = (existing_value.parse::<i64>().unwrap_or(0)
406                            + value.parse::<i64>().unwrap_or(0))
407                        .to_string();
408                    } else {
409                        new_value = existing_value.to_owned();
410                        new_value.push_str(value.as_str());
411                    }
412
413                    arr.insert(key, new_value);
414                } else {
415                    arr.insert(key, value);
416                }
417
418                Ok(())
419            }
420            ShellValue::AssociativeArray(arr) => {
421                if append {
422                    let existing_value = arr
423                        .get(array_index.as_str())
424                        .map_or_else(|| "", |v| v.as_str());
425
426                    let mut new_value;
427                    if treat_as_int {
428                        new_value = (existing_value.parse::<i64>().unwrap_or(0)
429                            + value.parse::<i64>().unwrap_or(0))
430                        .to_string();
431                    } else {
432                        new_value = existing_value.to_owned();
433                        new_value.push_str(value.as_str());
434                    }
435
436                    arr.insert(array_index, new_value.clone());
437                } else {
438                    arr.insert(array_index, value);
439                }
440                Ok(())
441            }
442            _ => {
443                tracing::error!("assigning to index {array_index} of {:?}", self.value);
444                error::unimp("assigning to index of non-array variable")
445            }
446        }
447    }
448
449    fn convert_value_literal_for_assignment(&self, value: ShellValueLiteral) -> ShellValueLiteral {
450        match value {
451            ShellValueLiteral::Scalar(s) => {
452                ShellValueLiteral::Scalar(self.convert_value_str_for_assignment(s))
453            }
454            ShellValueLiteral::Array(literals) => ShellValueLiteral::Array(ArrayLiteral(
455                literals
456                    .0
457                    .into_iter()
458                    .map(|(k, v)| (k, self.convert_value_str_for_assignment(v)))
459                    .collect(),
460            )),
461        }
462    }
463
464    fn convert_value_str_for_assignment(&self, mut s: String) -> String {
465        Self::apply_value_transforms(
466            &mut s,
467            self.is_treated_as_integer(),
468            self.get_update_transform(),
469        );
470
471        s
472    }
473
474    fn apply_value_transforms(
475        s: &mut String,
476        treat_as_int: bool,
477        update_transform: ShellVariableUpdateTransform,
478    ) {
479        if treat_as_int {
480            *s = (*s).parse::<i64>().unwrap_or(0).to_string();
481        } else {
482            match update_transform {
483                ShellVariableUpdateTransform::None => (),
484                ShellVariableUpdateTransform::Lowercase => *s = (*s).to_lowercase(),
485                ShellVariableUpdateTransform::Uppercase => *s = (*s).to_uppercase(),
486                ShellVariableUpdateTransform::Capitalize => {
487                    // This isn't really title-case; only the first character is capitalized.
488                    *s = s.to_lowercase();
489                    if let Some(c) = s.chars().next() {
490                        s.replace_range(0..1, &c.to_uppercase().to_string());
491                    }
492                }
493            }
494        }
495    }
496
497    /// Tries to unset the value stored at the given index in the variable. Returns
498    /// whether or not a value was unset.
499    ///
500    /// # Arguments
501    ///
502    /// * `index` - The index at which to unset the value.
503    pub fn unset_index(&mut self, index: &str) -> Result<bool, error::Error> {
504        match &mut self.value {
505            ShellValue::Unset(ty) => match ty {
506                ShellValueUnsetType::Untyped => Err(error::ErrorKind::NotArray.into()),
507                ShellValueUnsetType::AssociativeArray | ShellValueUnsetType::IndexedArray => {
508                    Ok(false)
509                }
510            },
511            ShellValue::String(_) => Err(error::ErrorKind::NotArray.into()),
512            ShellValue::AssociativeArray(values) => Ok(values.remove(index).is_some()),
513            ShellValue::IndexedArray(values) => {
514                let key = get_key_for_indexed_array(values, index)?;
515                Ok(values.remove(&key).is_some())
516            }
517            ShellValue::Dynamic { .. } => Ok(false),
518        }
519    }
520
521    /// Returns the variable's value; for dynamic values, this will resolve the value.
522    ///
523    /// # Arguments
524    ///
525    /// * `shell` - The shell in which the variable is being resolved.
526    pub fn resolve_value(&self, shell: &Shell<impl extensions::ShellExtensions>) -> ShellValue {
527        // N.B. We do *not* specially handle a dynamic value that resolves to a dynamic value.
528        match &self.value {
529            ShellValue::Dynamic { getter, .. } => getter(shell),
530            _ => self.value.clone(),
531        }
532    }
533
534    /// Returns the canonical attribute flag string for this variable.
535    pub fn attribute_flags(&self, shell: &Shell<impl extensions::ShellExtensions>) -> String {
536        let value = self.resolve_value(shell);
537
538        let mut result = String::new();
539
540        if matches!(
541            value,
542            ShellValue::IndexedArray(_) | ShellValue::Unset(ShellValueUnsetType::IndexedArray)
543        ) {
544            result.push('a');
545        }
546        if matches!(
547            value,
548            ShellValue::AssociativeArray(_)
549                | ShellValue::Unset(ShellValueUnsetType::AssociativeArray)
550        ) {
551            result.push('A');
552        }
553        if matches!(
554            self.get_update_transform(),
555            ShellVariableUpdateTransform::Capitalize
556        ) {
557            result.push('c');
558        }
559        if self.is_treated_as_integer() {
560            result.push('i');
561        }
562        if self.is_treated_as_nameref() {
563            result.push('n');
564        }
565        if self.is_readonly() {
566            result.push('r');
567        }
568        if matches!(
569            self.get_update_transform(),
570            ShellVariableUpdateTransform::Lowercase
571        ) {
572            result.push('l');
573        }
574        if self.is_trace_enabled() {
575            result.push('t');
576        }
577        if matches!(
578            self.get_update_transform(),
579            ShellVariableUpdateTransform::Uppercase
580        ) {
581            result.push('u');
582        }
583        if self.is_exported() {
584            result.push('x');
585        }
586
587        result
588    }
589}
590
591type DynamicValueGetter = fn(&dyn ShellState) -> ShellValue;
592type DynamicValueSetter = fn(&dyn ShellState) -> ();
593
594/// A shell value.
595#[derive(Clone, Debug)]
596#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
597pub enum ShellValue {
598    /// A value that has been typed but not yet set.
599    Unset(ShellValueUnsetType),
600    /// A string.
601    String(String),
602    /// An associative array.
603    AssociativeArray(BTreeMap<String, String>),
604    /// An indexed array.
605    IndexedArray(BTreeMap<u64, String>),
606    /// A value that is dynamically computed.
607    Dynamic {
608        /// Function that can query the value.
609        /// TODO(serde): figure out how to serialize/deserialize dynamic values.
610        #[cfg_attr(
611            feature = "serde",
612            serde(skip, default = "default_dynamic_value_getter")
613        )]
614        getter: DynamicValueGetter,
615        /// Function that receives value update requests.
616        /// TODO(serde): figure out how to serialize/deserialize dynamic values.
617        #[cfg_attr(
618            feature = "serde",
619            serde(skip, default = "default_dynamic_value_setter")
620        )]
621        setter: DynamicValueSetter,
622    },
623}
624
625#[cfg(feature = "serde")]
626fn default_dynamic_value_getter() -> DynamicValueGetter {
627    |_shell: &dyn ShellState| ShellValue::String(String::new())
628}
629
630#[cfg(feature = "serde")]
631fn default_dynamic_value_setter() -> DynamicValueSetter {
632    |_shell: &dyn ShellState| {}
633}
634
635/// The type of an unset shell value.
636#[derive(Clone, Debug)]
637#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
638pub enum ShellValueUnsetType {
639    /// The value is untyped.
640    Untyped,
641    /// The value is an associative array.
642    AssociativeArray,
643    /// The value is an indexed array.
644    IndexedArray,
645}
646
647/// A shell value literal; used for assignment.
648#[derive(Clone, Debug)]
649pub enum ShellValueLiteral {
650    /// A scalar value.
651    Scalar(String),
652    /// An array value.
653    Array(ArrayLiteral),
654}
655
656impl ShellValueLiteral {
657    pub(crate) fn fmt_for_tracing(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
658        match self {
659            Self::Scalar(s) => Self::fmt_scalar_for_tracing(s.as_str(), f),
660            Self::Array(elements) => {
661                write!(f, "(")?;
662                for (i, (key, value)) in elements.0.iter().enumerate() {
663                    if i > 0 {
664                        write!(f, " ")?;
665                    }
666                    if let Some(key) = key {
667                        write!(f, "[")?;
668                        Self::fmt_scalar_for_tracing(key.as_str(), f)?;
669                        write!(f, "]=")?;
670                    }
671                    Self::fmt_scalar_for_tracing(value.as_str(), f)?;
672                }
673                write!(f, ")")
674            }
675        }
676    }
677
678    fn fmt_scalar_for_tracing(s: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
679        let processed = escape::quote_if_needed(s, escape::QuoteMode::SingleQuote);
680        write!(f, "{processed}")
681    }
682}
683
684impl Display for ShellValueLiteral {
685    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
686        self.fmt_for_tracing(f)
687    }
688}
689
690impl From<&str> for ShellValueLiteral {
691    fn from(value: &str) -> Self {
692        Self::Scalar(value.to_owned())
693    }
694}
695
696impl From<String> for ShellValueLiteral {
697    fn from(value: String) -> Self {
698        Self::Scalar(value)
699    }
700}
701
702impl From<Vec<&str>> for ShellValueLiteral {
703    fn from(value: Vec<&str>) -> Self {
704        Self::Array(ArrayLiteral(
705            value.into_iter().map(|s| (None, s.to_owned())).collect(),
706        ))
707    }
708}
709
710/// An array literal.
711#[derive(Clone, Debug)]
712pub struct ArrayLiteral(pub Vec<(Option<String>, String)>);
713
714/// Style for formatting a shell variable's value.
715#[derive(Copy, Clone, Debug)]
716pub enum FormatStyle {
717    /// Basic formatting.
718    Basic,
719    /// Formatting as appropriate in the `declare` built-in command.
720    DeclarePrint,
721}
722
723impl ShellValue {
724    /// Returns whether or not the value is an array.
725    pub const fn is_array(&self) -> bool {
726        matches!(
727            self,
728            Self::IndexedArray(_)
729                | Self::AssociativeArray(_)
730                | Self::Unset(
731                    ShellValueUnsetType::IndexedArray | ShellValueUnsetType::AssociativeArray
732                )
733        )
734    }
735
736    /// Returns whether or not the value is set.
737    pub const fn is_set(&self) -> bool {
738        !matches!(self, Self::Unset(_))
739    }
740
741    /// Returns a new indexed array value constructed from the given slice of owned strings.
742    ///
743    /// # Arguments
744    ///
745    /// * `values` - The slice of strings to construct the indexed array from.
746    pub fn indexed_array_from_strings<S>(values: S) -> Self
747    where
748        S: IntoIterator<Item = String>,
749    {
750        let mut owned_values = BTreeMap::new();
751        for (i, value) in values.into_iter().enumerate() {
752            owned_values.insert(i as u64, value);
753        }
754
755        Self::IndexedArray(owned_values)
756    }
757
758    /// Returns a new indexed array value constructed from the given slice of unowned strings.
759    ///
760    /// # Arguments
761    ///
762    /// * `values` - The slice of strings to construct the indexed array from.
763    pub fn indexed_array_from_strs(values: &[&str]) -> Self {
764        let mut owned_values = BTreeMap::new();
765        for (i, value) in values.iter().enumerate() {
766            owned_values.insert(i as u64, (*value).to_string());
767        }
768
769        Self::IndexedArray(owned_values)
770    }
771
772    /// Returns a new indexed array value constructed from the given literals.
773    ///
774    /// # Arguments
775    ///
776    /// * `literals` - The literals to construct the indexed array from.
777    pub fn indexed_array_from_literals(literals: ArrayLiteral) -> Self {
778        let mut values = BTreeMap::new();
779        Self::update_indexed_array_from_literals(&mut values, literals);
780
781        Self::IndexedArray(values)
782    }
783
784    fn update_indexed_array_from_literals(
785        existing_values: &mut BTreeMap<u64, String>,
786        literal_values: ArrayLiteral,
787    ) {
788        let mut new_key = if let Some((largest_index, _)) = existing_values.last_key_value() {
789            largest_index + 1
790        } else {
791            0
792        };
793
794        for (key, value) in literal_values.0 {
795            if let Some(key) = key {
796                new_key = key.parse().unwrap_or(0);
797            }
798
799            existing_values.insert(new_key, value);
800            new_key += 1;
801        }
802    }
803
804    /// Returns a new associative array value constructed from the given literals.
805    ///
806    /// # Arguments
807    ///
808    /// * `literals` - The literals to construct the associative array from.
809    pub fn associative_array_from_literals(literals: ArrayLiteral) -> Result<Self, error::Error> {
810        let mut values = BTreeMap::new();
811        Self::update_associative_array_from_literals(&mut values, literals)?;
812
813        Ok(Self::AssociativeArray(values))
814    }
815
816    fn update_associative_array_from_literals(
817        existing_values: &mut BTreeMap<String, String>,
818        literal_values: ArrayLiteral,
819    ) -> Result<(), error::Error> {
820        let mut current_key = None;
821        for (key, value) in literal_values.0 {
822            if let Some(current_key) = current_key.take() {
823                if key.is_some() {
824                    return error::unimp("misaligned keys/values in associative array literal");
825                } else {
826                    existing_values.insert(current_key, value);
827                }
828            } else if let Some(key) = key {
829                existing_values.insert(key, value);
830            } else {
831                current_key = Some(value);
832            }
833        }
834
835        if let Some(current_key) = current_key {
836            existing_values.insert(current_key, String::new());
837        }
838
839        Ok(())
840    }
841
842    /// Formats the value using the given style.
843    ///
844    /// # Arguments
845    ///
846    /// * `style` - The style to use for formatting the value.
847    pub fn format(
848        &self,
849        style: FormatStyle,
850        shell: &Shell<impl extensions::ShellExtensions>,
851    ) -> Result<Cow<'_, str>, error::Error> {
852        match self {
853            Self::Unset(_) => Ok("".into()),
854            Self::String(s) => match style {
855                FormatStyle::Basic => Ok(escape::quote_if_needed(
856                    s.as_str(),
857                    escape::QuoteMode::SingleQuote,
858                )),
859                FormatStyle::DeclarePrint => {
860                    Ok(escape::force_quote(s.as_str(), escape::QuoteMode::DoubleQuote).into())
861                }
862            },
863            Self::AssociativeArray(values) => {
864                let mut result = String::new();
865                result.push('(');
866
867                for (key, value) in values {
868                    let formatted_key =
869                        escape::quote_if_needed(key.as_str(), escape::QuoteMode::DoubleQuote);
870                    let formatted_value =
871                        escape::force_quote(value.as_str(), escape::QuoteMode::DoubleQuote);
872
873                    // N.B. We include an unconditional trailing space character (even after the
874                    // last entry in the associative array) to match standard
875                    // output behavior.
876                    write!(result, "[{formatted_key}]={formatted_value} ")?;
877                }
878
879                result.push(')');
880                Ok(result.into())
881            }
882            Self::IndexedArray(values) => {
883                let mut result = String::new();
884                result.push('(');
885
886                for (i, (key, value)) in values.iter().enumerate() {
887                    if i > 0 {
888                        result.push(' ');
889                    }
890
891                    let formatted_value =
892                        escape::force_quote(value.as_str(), escape::QuoteMode::DoubleQuote);
893                    write!(result, "[{key}]={formatted_value}")?;
894                }
895
896                result.push(')');
897                Ok(result.into())
898            }
899            Self::Dynamic { getter, .. } => {
900                let dynamic_value = getter(shell);
901                let result = dynamic_value.format(style, shell)?.to_string();
902                Ok(result.into())
903            }
904        }
905    }
906
907    /// Tries to retrieve the value stored at the given index in this variable.
908    ///
909    /// # Arguments
910    ///
911    /// * `index` - The index at which to retrieve the value.
912    pub fn get_at(
913        &self,
914        index: &str,
915        shell: &Shell<impl extensions::ShellExtensions>,
916    ) -> Result<Option<Cow<'_, str>>, error::Error> {
917        match self {
918            Self::Unset(_) => Ok(None),
919            Self::String(s) => {
920                if index.parse::<u64>().unwrap_or(0) == 0 {
921                    Ok(Some(Cow::Borrowed(s)))
922                } else {
923                    Ok(None)
924                }
925            }
926            Self::AssociativeArray(values) => {
927                Ok(values.get(index).map(|s| Cow::Borrowed(s.as_str())))
928            }
929            Self::IndexedArray(values) => {
930                let key = get_key_for_indexed_array(values, index)?;
931                Ok(values.get(&key).map(|s| Cow::Borrowed(s.as_str())))
932            }
933            Self::Dynamic { getter, .. } => {
934                let dynamic_value = getter(shell);
935                let result = dynamic_value.get_at(index, shell)?;
936                Ok(result.map(|s| s.to_string().into()))
937            }
938        }
939    }
940
941    /// Returns the keys of the elements in this variable.
942    pub fn element_keys(&self, shell: &Shell<impl extensions::ShellExtensions>) -> Vec<String> {
943        match self {
944            Self::Unset(_) => vec![],
945            Self::String(_) => vec!["0".to_owned()],
946            Self::AssociativeArray(array) => array.keys().map(|k| k.to_owned()).collect(),
947            Self::IndexedArray(array) => array.keys().map(|k| k.to_string()).collect(),
948            Self::Dynamic { getter, .. } => getter(shell).element_keys(shell),
949        }
950    }
951
952    /// Returns the values of the elements in this variable.
953    pub fn element_values(&self, shell: &Shell<impl extensions::ShellExtensions>) -> Vec<String> {
954        match self {
955            Self::Unset(_) => vec![],
956            Self::String(s) => vec![s.to_owned()],
957            Self::AssociativeArray(array) => array.values().map(|v| v.to_owned()).collect(),
958            Self::IndexedArray(array) => array.values().map(|v| v.to_owned()).collect(),
959            Self::Dynamic { getter, .. } => getter(shell).element_values(shell),
960        }
961    }
962
963    /// Converts this value to a string.
964    pub fn to_cow_str(&self, shell: &Shell<impl extensions::ShellExtensions>) -> Cow<'_, str> {
965        self.try_get_cow_str(shell).unwrap_or(Cow::Borrowed(""))
966    }
967
968    fn to_cow_str_without_dynamic_support(&self) -> Cow<'_, str> {
969        self.try_get_cow_str_without_dynamic_support()
970            .unwrap_or(Cow::Borrowed(""))
971    }
972
973    /// Tries to convert this value to a string; returns `None` if the value is unset
974    /// or otherwise doesn't exist.
975    pub fn try_get_cow_str(
976        &self,
977        shell: &Shell<impl extensions::ShellExtensions>,
978    ) -> Option<Cow<'_, str>> {
979        match self {
980            Self::Dynamic { getter, .. } => {
981                let dynamic_value = getter(shell);
982                dynamic_value
983                    .try_get_cow_str(shell)
984                    .map(|s| s.to_string().into())
985            }
986            _ => self.try_get_cow_str_without_dynamic_support(),
987        }
988    }
989
990    fn try_get_cow_str_without_dynamic_support(&self) -> Option<Cow<'_, str>> {
991        match self {
992            Self::Unset(_) => None,
993            Self::String(s) => Some(Cow::Borrowed(s.as_str())),
994            Self::AssociativeArray(values) => values.get("0").map(|s| Cow::Borrowed(s.as_str())),
995            Self::IndexedArray(values) => values.get(&0).map(|s| Cow::Borrowed(s.as_str())),
996            Self::Dynamic { .. } => None,
997        }
998    }
999
1000    /// Formats this value as a program string usable in an assignment.
1001    ///
1002    /// # Arguments
1003    ///
1004    /// * `index` - The index at which to retrieve the value, if indexing is to be performed.
1005    pub fn to_assignable_str(
1006        &self,
1007        index: Option<&str>,
1008        shell: &Shell<impl extensions::ShellExtensions>,
1009    ) -> Result<String, error::Error> {
1010        match self {
1011            Self::Unset(_) => Ok(String::new()),
1012            Self::String(s) => Ok(escape::force_quote(
1013                s.as_str(),
1014                escape::QuoteMode::SingleQuote,
1015            )),
1016            Self::AssociativeArray(_) | Self::IndexedArray(_) => {
1017                if let Some(index) = index {
1018                    if let Ok(Some(value)) = self.get_at(index, shell) {
1019                        Ok(escape::force_quote(
1020                            value.as_ref(),
1021                            escape::QuoteMode::SingleQuote,
1022                        ))
1023                    } else {
1024                        Ok(String::new())
1025                    }
1026                } else {
1027                    Ok(self.format(FormatStyle::DeclarePrint, shell)?.into_owned())
1028                }
1029            }
1030            Self::Dynamic { getter, .. } => getter(shell).to_assignable_str(index, shell),
1031        }
1032    }
1033}
1034
1035fn get_key_for_indexed_array(
1036    values: &BTreeMap<u64, String>,
1037    index_str: &str,
1038) -> Result<u64, error::Error> {
1039    let mut index_value = index_str.parse::<i64>().unwrap_or(0);
1040
1041    // Handle negative indices, but check for out-of-range values.
1042    #[expect(clippy::cast_possible_wrap)]
1043    if index_value < 0 {
1044        index_value += values.len() as i64;
1045        if index_value < 0 {
1046            return Err(error::ErrorKind::ArrayIndexOutOfRange(index_str.to_owned()).into());
1047        }
1048    }
1049
1050    // Now that we've confirmed that the index is non-negative, we can safely convert it
1051    // to a u64 without any fuss.
1052    #[expect(clippy::cast_sign_loss)]
1053    Ok(index_value as u64)
1054}
1055
1056impl From<&str> for ShellValue {
1057    fn from(value: &str) -> Self {
1058        Self::String(value.to_owned())
1059    }
1060}
1061
1062impl From<&String> for ShellValue {
1063    fn from(value: &String) -> Self {
1064        Self::String(value.clone())
1065    }
1066}
1067
1068impl From<String> for ShellValue {
1069    fn from(value: String) -> Self {
1070        Self::String(value)
1071    }
1072}
1073
1074impl From<OsString> for ShellValue {
1075    fn from(value: OsString) -> Self {
1076        Self::String(value.to_string_lossy().into_owned())
1077    }
1078}
1079
1080impl From<Vec<String>> for ShellValue {
1081    fn from(values: Vec<String>) -> Self {
1082        Self::indexed_array_from_strings(values)
1083    }
1084}
1085
1086impl From<Vec<&str>> for ShellValue {
1087    fn from(values: Vec<&str>) -> Self {
1088        Self::indexed_array_from_strs(values.as_slice())
1089    }
1090}