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