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