yash_semantics/expansion/initial/
word.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//! Initial expansion of words and word units.
18
19use super::super::Error;
20use super::super::attr::AttrChar;
21use super::super::attr::Origin;
22use super::Env;
23use super::Expand;
24use super::Phrase;
25use yash_syntax::syntax::Unquote as _;
26use yash_syntax::syntax::Word;
27use yash_syntax::syntax::WordUnit::{self, *};
28
29const SINGLE_QUOTE: AttrChar = AttrChar {
30    value: '\'',
31    origin: Origin::Literal,
32    is_quoted: false,
33    is_quoting: true,
34};
35
36/// Adds single quotes around the string.
37fn single_quote(value: &str) -> Phrase {
38    let mut field = Vec::with_capacity(value.chars().count() + 2);
39    field.push(SINGLE_QUOTE);
40    field.extend(value.chars().map(|c| AttrChar {
41        value: c,
42        origin: Origin::Literal,
43        is_quoted: true,
44        is_quoting: false,
45    }));
46    field.push(SINGLE_QUOTE);
47    Phrase::Field(field)
48}
49
50/// Adds dollar-single-quotes around the string.
51fn dollar_single_quote(s: &str) -> Phrase {
52    const DOLLAR: AttrChar = AttrChar {
53        value: '$',
54        origin: Origin::Literal,
55        is_quoted: false,
56        is_quoting: true,
57    };
58    let mut field = Vec::with_capacity(s.chars().count() + 3);
59    field.push(DOLLAR);
60    field.push(SINGLE_QUOTE);
61    field.extend(s.chars().map(|c| AttrChar {
62        value: c,
63        origin: Origin::Literal,
64        is_quoted: true,
65        is_quoting: false,
66    }));
67    field.push(SINGLE_QUOTE);
68    Phrase::Field(field)
69}
70
71/// Add double quotes around each field in the phrase.
72///
73/// This function sets the `is_quoted` flag of the characters in the phrase.
74fn double_quote(phrase: &mut Phrase) {
75    const QUOTE: AttrChar = AttrChar {
76        value: '"',
77        origin: Origin::Literal,
78        is_quoted: false,
79        is_quoting: true,
80    };
81
82    fn quote_field(chars: &mut Vec<AttrChar>) {
83        for c in chars.iter_mut() {
84            c.is_quoted = true;
85        }
86        chars.reserve_exact(2);
87        chars.insert(0, QUOTE);
88        chars.push(QUOTE);
89    }
90
91    match phrase {
92        Phrase::Char(c) => {
93            let is_quoted = true;
94            let c = AttrChar { is_quoted, ..*c };
95            *phrase = Phrase::Field(vec![QUOTE, c, QUOTE]);
96        }
97        Phrase::Field(chars) => quote_field(chars),
98        Phrase::Full(fields) => fields.iter_mut().for_each(quote_field),
99    }
100}
101
102/// Expands the word unit.
103///
104/// # Unquoted
105///
106/// Expansion of `Unquoted(text_unit)` delegates to expansion of `text_unit`.
107///
108/// # Single quote
109///
110/// `SingleQuote(value)` expands to `value` surrounded by `'`.
111///
112/// # Double quote
113///
114/// A double-quoted text expands to a phrase in a non-splitting context and
115/// surrounds each field in the phrase with `"`.
116///
117/// # Dollar-single-quote
118///
119/// `DollarSingleQuote(string)` expands to
120/// `dollar_single_quote(&string.unquote().0)` surrounded by `$'` and `'`.
121///
122/// # Tilde
123///
124/// `Tilde("")` expands to the value of the `HOME` scalar variable.
125///
126/// `Tilde(user)` expands to the `user`'s home directory.
127///
128/// TODO: `~+`, `~-`, `~+n`, `~-n`
129///
130/// In all cases, if the result would be empty, it expands to a dummy quote to
131/// prevent it from being removed in field splitting. The quote is expected to
132/// be removed by quote removal.
133impl Expand for WordUnit {
134    async fn expand(&self, env: &mut Env<'_>) -> Result<Phrase, Error> {
135        match self {
136            Unquoted(text_unit) => text_unit.expand(env).await,
137            SingleQuote(value) => Ok(single_quote(value)),
138            DoubleQuote(text) => {
139                let would_split = std::mem::replace(&mut env.will_split, false);
140                let result = text.expand(env).await;
141                env.will_split = would_split;
142
143                let mut phrase = result?;
144                double_quote(&mut phrase);
145                Ok(phrase)
146            }
147            DollarSingleQuote(string) => Ok(dollar_single_quote(&string.unquote().0)),
148            Tilde {
149                name,
150                followed_by_slash,
151            } => Ok(super::tilde::expand(name, *followed_by_slash, env.inner).into()),
152        }
153    }
154}
155
156/// Expands a word.
157///
158/// This implementation delegates to `[WordUnit] as Expand`.
159impl Expand for Word {
160    #[inline]
161    async fn expand(&self, env: &mut Env<'_>) -> Result<Phrase, Error> {
162        self.units.expand(env).await
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::super::param::tests::braced_param;
169    use super::super::param::tests::env_with_positional_params_and_ifs;
170    use super::*;
171    use futures_util::FutureExt;
172    use yash_syntax::syntax::SpecialParam;
173    use yash_syntax::syntax::Text;
174    use yash_syntax::syntax::TextUnit;
175
176    #[test]
177    fn double_quote_char() {
178        let mut phrase = Phrase::Char(AttrChar {
179            value: 'C',
180            origin: Origin::SoftExpansion,
181            is_quoted: false,
182            is_quoting: false,
183        });
184        double_quote(&mut phrase);
185        let quote = AttrChar {
186            value: '"',
187            origin: Origin::Literal,
188            is_quoted: false,
189            is_quoting: true,
190        };
191        let c = AttrChar {
192            value: 'C',
193            origin: Origin::SoftExpansion,
194            is_quoted: true,
195            is_quoting: false,
196        };
197        assert_eq!(phrase, Phrase::Field(vec![quote, c, quote]));
198    }
199
200    #[test]
201    fn double_quote_field() {
202        let mut phrase = Phrase::Field(vec![]);
203        double_quote(&mut phrase);
204        let quote = AttrChar {
205            value: '"',
206            origin: Origin::Literal,
207            is_quoted: false,
208            is_quoting: true,
209        };
210        assert_eq!(phrase, Phrase::Field(vec![quote, quote]));
211
212        let i = AttrChar {
213            value: 'i',
214            origin: Origin::SoftExpansion,
215            is_quoted: false,
216            is_quoting: false,
217        };
218        let f = AttrChar {
219            value: 'f',
220            origin: Origin::Literal,
221            is_quoted: false,
222            is_quoting: false,
223        };
224        phrase = Phrase::Field(vec![i, f]);
225        double_quote(&mut phrase);
226        let is_quoted = true;
227        let i = AttrChar { is_quoted, ..i };
228        let f = AttrChar { is_quoted, ..f };
229        assert_eq!(phrase, Phrase::Field(vec![quote, i, f, quote]));
230    }
231
232    #[test]
233    fn double_quote_full() {
234        let mut phrase = Phrase::Full(vec![]);
235        double_quote(&mut phrase);
236        assert_eq!(phrase, Phrase::zero_fields());
237
238        let a = AttrChar {
239            value: 'a',
240            origin: Origin::HardExpansion,
241            is_quoted: false,
242            is_quoting: false,
243        };
244        let b = AttrChar { value: 'b', ..a };
245        phrase = Phrase::Full(vec![vec![a], vec![b]]);
246        double_quote(&mut phrase);
247        let quote = AttrChar {
248            value: '"',
249            origin: Origin::Literal,
250            is_quoted: false,
251            is_quoting: true,
252        };
253        let is_quoted = true;
254        let a = AttrChar { is_quoted, ..a };
255        let b = AttrChar { is_quoted, ..b };
256        assert_eq!(
257            phrase,
258            Phrase::Full(vec![vec![quote, a, quote], vec![quote, b, quote]])
259        );
260    }
261
262    #[test]
263    fn unquoted() {
264        let mut env = yash_env::Env::new_virtual();
265        let mut env = Env::new(&mut env);
266        let unit: WordUnit = "x".parse().unwrap();
267        let result = unit.expand(&mut env).now_or_never().unwrap();
268
269        let c = AttrChar {
270            value: 'x',
271            origin: Origin::Literal,
272            is_quoted: false,
273            is_quoting: false,
274        };
275        assert_eq!(result, Ok(Phrase::Char(c)));
276    }
277
278    #[test]
279    fn empty_single_quote() {
280        let result = single_quote("");
281        let q = AttrChar {
282            value: '\'',
283            origin: Origin::Literal,
284            is_quoted: false,
285            is_quoting: true,
286        };
287        assert_eq!(result, Phrase::Field(vec![q, q]));
288    }
289
290    #[test]
291    fn non_empty_single_quote() {
292        let result = single_quote("do");
293        let q = AttrChar {
294            value: '\'',
295            origin: Origin::Literal,
296            is_quoted: false,
297            is_quoting: true,
298        };
299        let d = AttrChar {
300            value: 'd',
301            origin: Origin::Literal,
302            is_quoted: true,
303            is_quoting: false,
304        };
305        let o = AttrChar { value: 'o', ..d };
306        assert_eq!(result, Phrase::Field(vec![q, d, o, q]));
307    }
308
309    #[test]
310    fn expand_dollar_single_quote() {
311        let mut env = yash_env::Env::new_virtual();
312        let mut env = Env::new(&mut env);
313        let unit = DollarSingleQuote(r"\\\n".parse().unwrap());
314        let result = unit.expand(&mut env).now_or_never().unwrap();
315
316        let dollar = AttrChar {
317            value: '$',
318            origin: Origin::Literal,
319            is_quoted: false,
320            is_quoting: true,
321        };
322        let quote = AttrChar {
323            value: '\'',
324            origin: Origin::Literal,
325            is_quoted: false,
326            is_quoting: true,
327        };
328        let backslash = AttrChar {
329            value: '\\',
330            origin: Origin::Literal,
331            is_quoted: true,
332            is_quoting: false,
333        };
334        let newline = AttrChar {
335            value: '\n',
336            origin: Origin::Literal,
337            is_quoted: true,
338            is_quoting: false,
339        };
340        assert_eq!(
341            result,
342            Ok(Phrase::Field(vec![
343                dollar, quote, backslash, newline, quote
344            ]))
345        );
346    }
347
348    #[test]
349    fn expand_double_quote() {
350        let mut env = yash_env::Env::new_virtual();
351        let mut env = Env::new(&mut env);
352        let unit = DoubleQuote(Text(vec![TextUnit::Literal('X')]));
353        let result = unit.expand(&mut env).now_or_never().unwrap();
354
355        let quote = AttrChar {
356            value: '"',
357            origin: Origin::Literal,
358            is_quoted: false,
359            is_quoting: true,
360        };
361        let x = AttrChar {
362            value: 'X',
363            origin: Origin::Literal,
364            is_quoted: true,
365            is_quoting: false,
366        };
367        assert_eq!(result, Ok(Phrase::Field(vec![quote, x, quote])));
368    }
369
370    #[test]
371    fn inside_double_quote_is_non_splitting_context() {
372        let mut env = env_with_positional_params_and_ifs();
373        let mut env = Env::new(&mut env);
374        let unit = TextUnit::BracedParam(braced_param(SpecialParam::Asterisk));
375        let unit = DoubleQuote(Text(vec![unit]));
376        let result = unit.expand(&mut env).now_or_never().unwrap();
377
378        assert!(env.will_split);
379        let quote = AttrChar {
380            value: '"',
381            origin: Origin::Literal,
382            is_quoted: false,
383            is_quoting: true,
384        };
385        let a = AttrChar {
386            value: 'a',
387            origin: Origin::SoftExpansion,
388            is_quoted: true,
389            is_quoting: false,
390        };
391        let amp = AttrChar { value: '&', ..a };
392        let c = AttrChar { value: 'c', ..a };
393        assert_eq!(result, Ok(Phrase::Field(vec![quote, a, amp, c, quote])));
394    }
395}