yash_semantics/
assign.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//! Assignment.
18
19use crate::expansion::AssignReadOnlyError;
20use crate::expansion::expand_value;
21use crate::xtrace::XTrace;
22use std::fmt::Write;
23use yash_env::Env;
24use yash_env::semantics::ExitStatus;
25
26#[doc(no_inline)]
27pub use crate::expansion::{Error, ErrorCause, Result};
28#[doc(no_inline)]
29pub use yash_env::variable::Scope;
30#[doc(no_inline)]
31pub use yash_syntax::syntax::Assign;
32
33/// Performs an assignment.
34///
35/// This function [expands the value](expand_value) and then
36/// [assigns](yash_env::variable::VariableRefMut::assign) it to the environment.
37/// The return value is the exit status of the last command substitution
38/// performed during the expansion of the assigned value, if any
39///
40/// If `xtrace` is `Some` instance of `XTrace`, the expanded assignment word is
41/// written to its assignments buffer.
42pub async fn perform_assignment(
43    env: &mut Env,
44    assign: &Assign,
45    scope: Scope,
46    export: bool,
47    xtrace: Option<&mut XTrace>,
48) -> Result<Option<ExitStatus>> {
49    let name = assign.name.clone();
50    let (value, exit_status) = expand_value(env, &assign.value).await?;
51
52    if let Some(xtrace) = xtrace {
53        write!(
54            xtrace.assigns(),
55            "{}={} ",
56            yash_quote::quoted(&name),
57            value.quote()
58        )
59        .unwrap();
60    }
61
62    let mut variable = env.get_or_create_variable(name, scope);
63    variable
64        .assign(value, assign.location.clone())
65        .map_err(|e| Error {
66            cause: ErrorCause::AssignReadOnly(AssignReadOnlyError {
67                name: assign.name.clone(),
68                new_value: e.new_value,
69                read_only_location: e.read_only_location,
70                vacancy: None,
71            }),
72            location: e.assigned_location.unwrap(),
73        })?;
74    if export {
75        variable.export(true);
76    }
77    Ok(exit_status)
78}
79
80/// Performs assignments.
81///
82/// This function calls [`perform_assignment`] for each [`Assign`].
83/// The return value is the exit status of the last command substitution
84/// performed during the expansion of the assigned values, if any
85///
86/// If `xtrace` is `Some` instance of `XTrace`, the expanded assignment words
87/// are written to its assignments buffer.
88pub async fn perform_assignments(
89    env: &mut Env,
90    assigns: &[Assign],
91    scope: Scope,
92    export: bool,
93    mut xtrace: Option<&mut XTrace>,
94) -> Result<Option<ExitStatus>> {
95    let mut exit_status = None;
96    for assign in assigns {
97        let new_exit_status =
98            perform_assignment(env, assign, scope, export, xtrace.as_deref_mut()).await?;
99        exit_status = new_exit_status.or(exit_status);
100    }
101    Ok(exit_status)
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::tests::return_builtin;
108    use assert_matches::assert_matches;
109    use futures_util::FutureExt;
110    use yash_env::variable::Value;
111    use yash_env::variable::Variable;
112    use yash_env_test_helper::in_virtual_system;
113    use yash_syntax::source::Location;
114
115    #[test]
116    fn perform_assignment_new_value() {
117        let mut env = Env::new_virtual();
118        let a: Assign = "foo=bar".parse().unwrap();
119        let exit_status = perform_assignment(&mut env, &a, Scope::Global, false, None)
120            .now_or_never()
121            .unwrap()
122            .unwrap();
123        assert_eq!(exit_status, None);
124        assert_eq!(
125            env.variables.get("foo").unwrap(),
126            &Variable::new("bar").set_assigned_location(a.location)
127        );
128    }
129
130    #[test]
131    fn perform_assignment_overwriting() {
132        let mut env = Env::new_virtual();
133        let a: Assign = "foo=bar".parse().unwrap();
134        perform_assignment(&mut env, &a, Scope::Global, false, None)
135            .now_or_never()
136            .unwrap()
137            .unwrap();
138
139        let a: Assign = "foo=baz".parse().unwrap();
140        let exit_status = perform_assignment(&mut env, &a, Scope::Global, true, None)
141            .now_or_never()
142            .unwrap()
143            .unwrap();
144        assert_eq!(exit_status, None);
145        assert_eq!(
146            env.variables.get("foo").unwrap(),
147            &Variable::new("baz")
148                .export()
149                .set_assigned_location(a.location)
150        );
151
152        let a: Assign = "foo=foo".parse().unwrap();
153        let exit_status = perform_assignment(&mut env, &a, Scope::Global, false, None)
154            .now_or_never()
155            .unwrap()
156            .unwrap();
157        assert_eq!(exit_status, None);
158        // The variable is still exported.
159        assert_eq!(
160            env.variables.get("foo").unwrap(),
161            &Variable::new("foo")
162                .export()
163                .set_assigned_location(a.location)
164        );
165    }
166
167    #[test]
168    fn perform_assignment_read_only() {
169        let mut env = Env::new_virtual();
170        let location = Location::dummy("read-only location");
171        let mut var = env.variables.get_or_new("v", Scope::Global);
172        var.assign("read-only", None).unwrap();
173        var.make_read_only(location.clone());
174        let a: Assign = "v=new".parse().unwrap();
175        let e = perform_assignment(&mut env, &a, Scope::Global, false, None)
176            .now_or_never()
177            .unwrap()
178            .unwrap_err();
179        assert_matches!(e.cause, ErrorCause::AssignReadOnly(error) => {
180            assert_eq!(error.name, "v");
181            assert_eq!(error.new_value, Value::scalar("new"));
182            assert_eq!(error.read_only_location, location);
183            assert_eq!(error.vacancy, None);
184        });
185        assert_eq!(e.location, Location::dummy("v=new"));
186    }
187
188    #[test]
189    fn perform_assignment_with_xtrace() {
190        let mut xtrace = XTrace::new();
191        let mut env = Env::new_virtual();
192
193        let a: Assign = "foo=bar${unset-&}".parse().unwrap();
194        let _ = perform_assignment(&mut env, &a, Scope::Global, false, Some(&mut xtrace))
195            .now_or_never()
196            .unwrap()
197            .unwrap();
198
199        let a: Assign = "one=1".parse().unwrap();
200        let _ = perform_assignment(&mut env, &a, Scope::Global, false, Some(&mut xtrace))
201            .now_or_never()
202            .unwrap()
203            .unwrap();
204
205        let result = xtrace.finish(&mut env).now_or_never().unwrap();
206        assert_eq!(result, "foo='bar&' one=1\n");
207    }
208
209    #[test]
210    fn perform_assignments_exit_status() {
211        in_virtual_system(|mut env, _state| async move {
212            env.builtins.insert("return", return_builtin());
213            let assigns = [
214                "a=A$(return -n 1)".parse().unwrap(),
215                "b=($(return -n 2))".parse().unwrap(),
216            ];
217            let exit_status = perform_assignments(&mut env, &assigns, Scope::Global, false, None)
218                .await
219                .unwrap();
220            assert_eq!(exit_status, Some(ExitStatus(2)));
221            assert_eq!(
222                env.variables.get("a").unwrap(),
223                &Variable::new("A").set_assigned_location(assigns[0].location.clone())
224            );
225            assert_eq!(
226                env.variables.get("b").unwrap(),
227                &Variable::new_empty_array().set_assigned_location(assigns[1].location.clone())
228            );
229        })
230    }
231}