yash_semantics/expansion/initial/
text.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2021 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 texts and text 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 super::param::ParamRef;
26use yash_syntax::syntax::Text;
27use yash_syntax::syntax::TextUnit::{self, *};
28use yash_syntax::syntax::Unquote;
29
30/// Expands the text unit.
31///
32/// - `Literal` expands to its character value.
33/// - `Backslashed` expands to two characters: a quoting backslash (`\`)
34///   followed by its quoted character value.
35/// - `RawParam` and `BracedParam` perform parameter expansion, detailed below.
36/// - `CommandSubst` and `Backquote` perform command substitution: The `content`
37///   string is [parsed and executed](crate::ReadEvalLoop) in a subshell where
38///   its standard output is redirected to a pipe read by the shell. The
39///   substitution expands to the output with trailing newlines removed.
40/// - `Arith` performs arithmetic expansion: The content text is expanded,
41///   parsing the result as an arithmetic expression. The evaluated value of the
42///   expression will be the final result of the expansion.
43///
44/// # Parameter expansion
45///
46/// A parameter expansion expands to the value of a parameter, optionally
47/// modified by a modifier.
48///
49/// The parameter name selects a parameter to expand. If the name is a
50/// positive decimal integer, it is regarded as a positional parameter. If
51/// the name matches one of the following special parameter symbols, that
52/// special parameter is expanded. Otherwise, the name [selects a
53/// variable](yash_env::variable::VariableSet::get) from the environment. A
54/// non-existent variable expands to an empty string by default.
55///
56/// - `?` expands to the [last exit status](yash_env::Env::exit_status).
57/// - `!` expands to the [process ID of the last asynchronous
58///   command](yash_env::job::JobList::last_async_pid). If no asynchronous
59///   command has been executed (that is, if the value is zero), the parameter
60///   is considered unset.
61/// - `@` expands to all positional parameters. When expanded in double-quotes
62///   as in `"${@}"`, it produces the correct number of fields exactly matching
63///   the current positional parameters. Especially if there are zero positional
64///   parameters, it expands to zero fields.
65/// - `*` expands to all positional parameters. When expanded in double-quotes
66///   as in `"${*}"`, the result is a concatenation of all the positional
67///   parameters, each separated by the first character of the `IFS` variable
68///   (or by a space if the variable is unset, or by nothing if it is an empty
69///   string). When expanded outside double-quotes, `*` expands the same as `@`.
70/// - `#` expands to the number of positional parameters.
71/// - `-` expands to a string that is a concatenation of the short names of
72///   options matching the current option states in the environment.
73/// - `$` expands to the process ID of the main shell process
74///   ([`Env::main_pid`](yash_env::Env::main_pid)). Note that this value does
75///   _not_ change in subshells.
76/// - `0` expands to the name of the current shell executable or shell script
77///   ([`Env::arg0`](yash_env::Env::arg0)).
78///
79/// TODO Elaborate on index and modifiers
80impl Expand for TextUnit {
81    async fn expand(&self, env: &mut Env<'_>) -> Result<Phrase, Error> {
82        match self {
83            &Literal(value) => Ok(Phrase::Char(AttrChar {
84                value,
85                origin: Origin::Literal,
86                is_quoted: false,
87                is_quoting: false,
88            })),
89
90            &Backslashed(value) => {
91                let bs = AttrChar {
92                    value: '\\',
93                    origin: Origin::Literal,
94                    is_quoted: false,
95                    is_quoting: true,
96                };
97                let c = AttrChar {
98                    value,
99                    origin: Origin::Literal,
100                    is_quoted: true,
101                    is_quoting: false,
102                };
103                Ok(Phrase::Field(vec![bs, c]))
104            }
105
106            RawParam { param, location } => {
107                let param_ref = ParamRef {
108                    param,
109                    modifier: &yash_syntax::syntax::Modifier::None,
110                    location,
111                };
112                Box::pin(param_ref.expand(env)).await // Boxing needed for recursion
113            }
114
115            // Boxing needed for recursion
116            BracedParam(param) => Box::pin(ParamRef::from(param).expand(env)).await,
117
118            CommandSubst { content, location } => {
119                let command = content.clone();
120                let location = location.clone();
121                super::command_subst::expand(command, location, env).await
122            }
123
124            Backquote { content, location } => {
125                let command = content.unquote().0;
126                let location = location.clone();
127                super::command_subst::expand(command, location, env).await
128            }
129
130            Arith { content, location } => {
131                // Boxing needed for recursion
132                Box::pin(super::arith::expand(content, location, env)).await
133            }
134        }
135    }
136}
137
138/// Expands a text.
139///
140/// This implementation delegates to `[TextUnit] as Expand`.
141impl Expand for Text {
142    async fn expand(&self, env: &mut Env<'_>) -> Result<Phrase, Error> {
143        self.0.expand(env).await
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::super::param::tests::braced_variable;
150    use super::*;
151    use crate::tests::echo_builtin;
152    use futures_util::FutureExt;
153    use yash_env::variable::Scope;
154    use yash_env_test_helper::in_virtual_system;
155    use yash_syntax::source::Location;
156    use yash_syntax::syntax::BracedParam;
157    use yash_syntax::syntax::Param;
158
159    #[test]
160    fn literal() {
161        let mut env = yash_env::Env::new_virtual();
162        let mut env = Env::new(&mut env);
163        let result = Literal('L').expand(&mut env).now_or_never().unwrap();
164
165        let c = AttrChar {
166            value: 'L',
167            origin: Origin::Literal,
168            is_quoted: false,
169            is_quoting: false,
170        };
171        assert_eq!(result, Ok(Phrase::Char(c)));
172    }
173
174    #[test]
175    fn backslashed() {
176        let mut env = yash_env::Env::new_virtual();
177        let mut env = Env::new(&mut env);
178        let result = Backslashed('L').expand(&mut env).now_or_never().unwrap();
179
180        let bs = AttrChar {
181            value: '\\',
182            origin: Origin::Literal,
183            is_quoted: false,
184            is_quoting: true,
185        };
186        let c = AttrChar {
187            value: 'L',
188            origin: Origin::Literal,
189            is_quoted: true,
190            is_quoting: false,
191        };
192        assert_eq!(result, Ok(Phrase::Field(vec![bs, c])));
193    }
194
195    #[test]
196    fn raw_param() {
197        let mut env = yash_env::Env::new_virtual();
198        env.variables
199            .get_or_new("foo", Scope::Global)
200            .assign("x", None)
201            .unwrap();
202        let mut env = Env::new(&mut env);
203        let raw_param = RawParam {
204            param: Param::variable("foo"),
205            location: Location::dummy(""),
206        };
207        let result = raw_param.expand(&mut env).now_or_never().unwrap();
208
209        let c = AttrChar {
210            value: 'x',
211            origin: Origin::SoftExpansion,
212            is_quoted: false,
213            is_quoting: false,
214        };
215        assert_eq!(result, Ok(Phrase::Char(c)));
216    }
217
218    #[test]
219    fn braced_param() {
220        let mut env = yash_env::Env::new_virtual();
221        env.variables
222            .get_or_new("foo", Scope::Global)
223            .assign("x", None)
224            .unwrap();
225        let mut env = Env::new(&mut env);
226        let param = BracedParam(braced_variable("foo"));
227        let result = param.expand(&mut env).now_or_never().unwrap();
228
229        let c = AttrChar {
230            value: 'x',
231            origin: Origin::SoftExpansion,
232            is_quoted: false,
233            is_quoting: false,
234        };
235        assert_eq!(result, Ok(Phrase::Char(c)));
236    }
237
238    #[test]
239    fn command_subst() {
240        in_virtual_system(|mut env, _state| async move {
241            env.builtins.insert("echo", echo_builtin());
242            let mut env = Env::new(&mut env);
243            let subst = TextUnit::CommandSubst {
244                content: "echo .".into(),
245                location: Location::dummy(""),
246            };
247            let result = subst.expand(&mut env).await;
248
249            let c = AttrChar {
250                value: '.',
251                origin: Origin::SoftExpansion,
252                is_quoted: false,
253                is_quoting: false,
254            };
255            assert_eq!(result, Ok(Phrase::Char(c)));
256        })
257    }
258
259    #[test]
260    fn backquote() {
261        in_virtual_system(|mut env, _state| async move {
262            env.builtins.insert("echo", echo_builtin());
263            let mut env = Env::new(&mut env);
264            use yash_syntax::syntax::BackquoteUnit::*;
265            let subst = TextUnit::Backquote {
266                content: vec![
267                    Literal('e'),
268                    Literal('c'),
269                    Literal('h'),
270                    Literal('o'),
271                    Literal(' '),
272                    Backslashed('\\'),
273                    Backslashed('\\'),
274                ],
275                location: Location::dummy(""),
276            };
277            let result = subst.expand(&mut env).await;
278
279            let c = AttrChar {
280                value: '\\',
281                origin: Origin::SoftExpansion,
282                is_quoted: false,
283                is_quoting: false,
284            };
285            assert_eq!(result, Ok(Phrase::Char(c)));
286        })
287    }
288
289    #[test]
290    fn arithmetic() {
291        let mut env = yash_env::Env::new_virtual();
292        let mut env = Env::new(&mut env);
293        let arith = TextUnit::Arith {
294            content: "1+2*3".parse().unwrap(),
295            location: Location::dummy(""),
296        };
297        let result = arith.expand(&mut env).now_or_never().unwrap();
298
299        let c = AttrChar {
300            value: '7',
301            origin: Origin::SoftExpansion,
302            is_quoted: false,
303            is_quoting: false,
304        };
305        assert_eq!(result, Ok(Phrase::Char(c)));
306    }
307}