yash_env/variable/
quirk.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2022 WATANABE Yuki
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Quirks of variables
18
19use super::Value;
20use super::Variable;
21use crate::source::{Location, Source};
22use either::{Left, Right};
23use std::borrow::Cow;
24
25/// Special characteristics of a variable
26///
27/// While most variables act as a simple store of a value, some variables
28/// exhibit special effects when they get expanded or assigned to. Such
29/// variables may have their value computed dynamically on expansion or may have
30/// an internal state that is updated when the value is set. `Quirk` determines
31/// the nature of a variable and contains the relevant state.
32///
33/// Use [`Variable::expand`] to apply the variable's quirk when expanding a
34/// variable.
35#[derive(Clone, Debug, Eq, PartialEq)]
36pub enum Quirk {
37    /// Quirk for the `$LINENO` variable
38    ///
39    /// The value of a variable having this variant of `Quirk` is computed
40    /// dynamically from the expanding context. The result is the line number of
41    /// the location of the parameter expansion. This `Quirk` is lost when an
42    /// assignment sets a new value to the variable.
43    LineNumber,
44    // TODO Random(RefCell<RandomState>)
45    // TODO Path(...)
46}
47
48/// Expanded value of a variable
49///
50/// Variables with a [`Quirk`] may have their values computed dynamically when
51/// expanded, hence [`Cow`] in the enum values.
52/// Use [`Variable::expand`] to get an `Expansion` instance.
53#[derive(Clone, Debug, Eq, PartialEq)]
54pub enum Expansion<'a> {
55    /// The value does not exist.
56    Unset,
57    /// The value is a single string.
58    Scalar(Cow<'a, str>),
59    /// The value is an array of strings.
60    Array(Cow<'a, [String]>),
61}
62
63/// Returns `Unset`.
64impl Default for Expansion<'_> {
65    fn default() -> Self {
66        Self::Unset
67    }
68}
69
70impl From<String> for Expansion<'static> {
71    fn from(value: String) -> Self {
72        Expansion::Scalar(Cow::Owned(value))
73    }
74}
75
76impl<'a> From<&'a str> for Expansion<'a> {
77    fn from(value: &'a str) -> Self {
78        Expansion::Scalar(Cow::Borrowed(value))
79    }
80}
81
82impl<'a> From<&'a String> for Expansion<'a> {
83    fn from(value: &'a String) -> Self {
84        Expansion::Scalar(Cow::Borrowed(value))
85    }
86}
87
88impl From<Option<String>> for Expansion<'static> {
89    fn from(value: Option<String>) -> Self {
90        match value {
91            Some(value) => value.into(),
92            None => Expansion::Unset,
93        }
94    }
95}
96
97impl From<Vec<String>> for Expansion<'static> {
98    fn from(values: Vec<String>) -> Self {
99        Expansion::Array(Cow::Owned(values))
100    }
101}
102
103impl<'a> From<&'a [String]> for Expansion<'a> {
104    fn from(values: &'a [String]) -> Self {
105        Expansion::Array(Cow::Borrowed(values))
106    }
107}
108
109impl<'a> From<&'a Vec<String>> for Expansion<'a> {
110    fn from(values: &'a Vec<String>) -> Self {
111        Expansion::Array(Cow::Borrowed(values))
112    }
113}
114
115impl From<Value> for Expansion<'static> {
116    fn from(value: Value) -> Self {
117        match value {
118            Value::Scalar(value) => Expansion::from(value),
119            Value::Array(values) => Expansion::from(values),
120        }
121    }
122}
123
124impl<'a> From<&'a Value> for Expansion<'a> {
125    fn from(value: &'a Value) -> Self {
126        match value {
127            Value::Scalar(value) => Expansion::from(value),
128            Value::Array(values) => Expansion::from(values),
129        }
130    }
131}
132
133impl From<Option<Value>> for Expansion<'static> {
134    fn from(value: Option<Value>) -> Self {
135        match value {
136            Some(value) => value.into(),
137            None => Expansion::Unset,
138        }
139    }
140}
141
142impl<'a, V> From<Option<&'a V>> for Expansion<'a>
143where
144    Expansion<'a>: From<&'a V>,
145{
146    fn from(value: Option<&'a V>) -> Self {
147        match value {
148            Some(value) => value.into(),
149            None => Expansion::Unset,
150        }
151    }
152}
153
154impl From<Expansion<'_>> for Option<Value> {
155    fn from(expansion: Expansion<'_>) -> Option<Value> {
156        match expansion {
157            Expansion::Unset => None,
158            Expansion::Scalar(value) => Some(Value::Scalar(value.into_owned())),
159            Expansion::Array(values) => Some(Value::Array(values.into_owned())),
160        }
161    }
162}
163
164impl<'a> From<&'a Expansion<'a>> for Expansion<'a> {
165    fn from(expansion: &'a Expansion<'a>) -> Expansion<'a> {
166        match expansion {
167            Expansion::Unset => Expansion::Unset,
168            Expansion::Scalar(value) => value.as_ref().into(),
169            Expansion::Array(values) => values.as_ref().into(),
170        }
171    }
172}
173
174impl Expansion<'_> {
175    /// Converts into an owned value
176    #[must_use]
177    pub fn into_owned(self) -> Option<Value> {
178        self.into()
179    }
180
181    /// Converts to a borrowed value
182    #[must_use]
183    pub fn as_ref(&self) -> Expansion<'_> {
184        self.into()
185    }
186
187    /// Returns the "length" of the value.
188    ///
189    /// For `Unset`, the length is 0.
190    /// For `Scalar`, the length is the number of characters.
191    /// For `Array`, the length is the number of strings.
192    #[must_use]
193    pub fn len(&self) -> usize {
194        match self {
195            Expansion::Unset => 0,
196            Expansion::Scalar(value) => value.len(),
197            Expansion::Array(values) => values.len(),
198        }
199    }
200
201    /// Tests whether the [length](Self::len) is zero.
202    #[must_use]
203    pub fn is_empty(&self) -> bool {
204        self.len() == 0
205    }
206
207    /// Splits the expansion by colons.
208    ///
209    /// If this expansion is `Scalar`, the value is separated at each occurrence
210    /// of colon (`:`). For `Array`, each array item is returned without further
211    /// splitting the value. For `Unset`, an empty iterator is returned.
212    ///
213    /// ```
214    /// # use yash_env::variable::Expansion;
215    /// let expansion = Expansion::from("/usr/local/bin:/usr/bin:/bin");
216    /// let values: Vec<&str> = expansion.split().collect();
217    /// assert_eq!(values, ["/usr/local/bin", "/usr/bin", "/bin"]);
218    /// ```
219    ///
220    /// ```
221    /// # use yash_env::variable::Expansion;
222    /// let expansion = Expansion::from(vec!["foo".to_string(), "bar".to_string()]);
223    /// let values: Vec<&str> = expansion.split().collect();
224    /// assert_eq!(values, ["foo", "bar"]);
225    /// ```
226    ///
227    /// ```
228    /// # use yash_env::variable::Expansion;
229    /// let expansion = Expansion::Unset;
230    /// let values: Vec<&str> = expansion.split().collect();
231    /// assert!(values.is_empty());
232    /// ```
233    pub fn split(&self) -> impl Iterator<Item = &str> {
234        match self {
235            Self::Unset => Right([].iter().map(String::as_str)),
236            Self::Scalar(value) => Left(value.split(':')),
237            Self::Array(values) => Right(values.iter().map(String::as_str)),
238        }
239    }
240}
241
242/// Implementation of [`Variable::expand`].
243pub fn expand<'a>(var: &'a Variable, mut location: &Location) -> Expansion<'a> {
244    match &var.quirk {
245        None => var.value.as_ref().into(),
246
247        Some(Quirk::LineNumber) => {
248            while let Source::Alias { original, .. } = &*location.code.source {
249                location = original;
250            }
251            let line_number = location.code.line_number(location.range.start);
252            line_number.to_string().into()
253        }
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use crate::alias::Alias;
261    use crate::source::Code;
262    use std::num::NonZeroU64;
263    use std::rc::Rc;
264
265    #[test]
266    fn expand_no_quirk() {
267        let var = Variable::new("foo");
268        let loc = Location::dummy("somewhere");
269        let result = var.expand(&loc);
270        assert_eq!(result, Expansion::Scalar("foo".into()));
271    }
272
273    fn stub_code() -> Rc<Code> {
274        Code {
275            value: "foo\nbar\nbaz\n".to_string().into(),
276            start_line_number: NonZeroU64::new(42).unwrap(),
277            source: Source::Unknown.into(),
278        }
279        .into()
280    }
281
282    #[test]
283    fn expand_line_number_of_first_line() {
284        let var = Variable {
285            quirk: Some(Quirk::LineNumber),
286            ..Default::default()
287        };
288        let code = stub_code();
289        let range = 1..3;
290        let loc = Location { code, range };
291        let result = var.expand(&loc);
292        assert_eq!(result, Expansion::Scalar("42".into()));
293    }
294
295    #[test]
296    fn expand_line_number_of_third_line() {
297        let var = Variable {
298            quirk: Some(Quirk::LineNumber),
299            ..Default::default()
300        };
301        let code = stub_code();
302        let range = 8..12;
303        let loc = Location { code, range };
304        let result = var.expand(&loc);
305        assert_eq!(result, Expansion::Scalar("44".into()));
306    }
307
308    #[test]
309    fn expand_line_number_in_alias() {
310        fn to_alias(original: Location) -> Location {
311            let alias = Alias {
312                name: "name".to_string(),
313                replacement: "replacement".to_string(),
314                global: false,
315                origin: Location::dummy("alias"),
316            }
317            .into();
318            let code = Code {
319                value: " \n \n ".to_string().into(),
320                start_line_number: NonZeroU64::new(15).unwrap(),
321                source: Source::Alias { original, alias }.into(),
322            }
323            .into();
324            let range = 0..1;
325            Location { code, range }
326        }
327
328        let var = Variable {
329            quirk: Some(Quirk::LineNumber),
330            ..Default::default()
331        };
332        let code = stub_code();
333        let range = 8..12;
334        let loc = to_alias(to_alias(Location { code, range }));
335        let result = var.expand(&loc);
336        assert_eq!(result, Expansion::Scalar("44".into()));
337    }
338}