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}