brush_core/
variables.rs

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