Skip to main content

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