Skip to main content

yash_builtin/unset/
semantics.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2023 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//! Defines the behavior of the unset built-in.
18
19use crate::common::report::prepare_report_message_and_divert;
20use thiserror::Error;
21use yash_env::Env;
22use yash_env::semantics::ExitStatus;
23use yash_env::semantics::Field;
24use yash_env::source::Location;
25use yash_env::source::pretty::{Report, ReportType, Span, SpanRole, add_span};
26#[cfg(doc)]
27use yash_env::system::Concurrent;
28use yash_env::system::Fcntl;
29use yash_env::system::Isatty;
30use yash_env::system::Write;
31use yash_env::variable::Scope::Global;
32
33/// Error returned by [`unset_variables`].
34#[derive(Clone, Debug, Eq, Error, PartialEq)]
35pub struct UnsetVariablesError<'a> {
36    /// The name of the read-only variable
37    pub name: &'a Field,
38    /// The location where the variable was made read-only
39    pub read_only_location: Location,
40}
41
42impl std::fmt::Display for UnsetVariablesError<'_> {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        let error = yash_env::variable::UnsetError {
45            name: &self.name.value,
46            read_only_location: &self.read_only_location,
47        };
48        error.fmt(f)
49    }
50}
51
52impl UnsetVariablesError<'_> {
53    /// Converts the error to a report.
54    pub fn to_report(&self) -> Report<'_> {
55        let mut report = Report::new();
56        report.r#type = ReportType::Error;
57        report.title = "cannot unset variable".into();
58        add_span(
59            &self.name.origin.code,
60            Span {
61                range: self.name.origin.byte_range(),
62                role: SpanRole::Primary {
63                    label: format!("read-only variable `{}`", self.name).into(),
64                },
65            },
66            &mut report.snippets,
67        );
68        add_span(
69            &self.read_only_location.code,
70            Span {
71                range: self.read_only_location.byte_range(),
72                role: SpanRole::Supplementary {
73                    label: format!("variable `{}` was made read-only here", self.name).into(),
74                },
75            },
76            &mut report.snippets,
77        );
78        report
79    }
80}
81
82impl<'a> From<&'a UnsetVariablesError<'_>> for Report<'a> {
83    #[inline]
84    fn from(error: &'a UnsetVariablesError<'_>) -> Self {
85        error.to_report()
86    }
87}
88
89/// Unsets shell variables.
90///
91/// This function tries to unset all the variables named by `names`. Any error
92/// unsetting a variable is reported in the returned vector and the function
93/// continues to unset the remaining variables. The returned vector is empty if
94/// all the variables are unset successfully.
95///
96/// TODO Allow unsetting local variables only.
97pub fn unset_variables<'a, S>(
98    env: &mut Env<S>,
99    names: &'a [Field],
100) -> Vec<UnsetVariablesError<'a>> {
101    let mut errors = Vec::new();
102    for name in names {
103        match env.variables.unset(&name.value, Global) {
104            Ok(_) => (),
105            Err(error) => errors.push(UnsetVariablesError {
106                name,
107                read_only_location: error.read_only_location.clone(),
108            }),
109        }
110    }
111    errors
112}
113
114/// Creates a message that describes the errors.
115///
116/// See [`prepare_report_message_and_divert`] for the second return value.
117#[deprecated(
118    note = "use `merge_reports` and `prepare_report_message_and_divert` directly",
119    since = "0.11.0"
120)]
121#[must_use = "returned message should be printed"]
122pub fn unset_variables_error_message<S>(
123    env: &Env<S>,
124    errors: &[UnsetVariablesError],
125) -> (String, yash_env::semantics::Result)
126where
127    S: Fcntl + Isatty + Write,
128{
129    let mut report = Report::new();
130    report.r#type = ReportType::Error;
131    report.title = "cannot unset variable".into();
132    for error in errors {
133        add_span(
134            &error.name.origin.code,
135            Span {
136                range: error.name.origin.byte_range(),
137                role: SpanRole::Primary {
138                    label: error.to_string().into(),
139                },
140            },
141            &mut report.snippets,
142        );
143        add_span(
144            &error.read_only_location.code,
145            Span {
146                range: error.read_only_location.byte_range(),
147                role: SpanRole::Supplementary {
148                    label: format!("variable `{}` was made read-only here", error.name).into(),
149                },
150            },
151            &mut report.snippets,
152        );
153    }
154    prepare_report_message_and_divert(env, report)
155}
156
157/// Prints an error message to the standard error.
158///
159/// This function constructs a message with [`unset_variables_error_message`]
160/// and prints it with [`Concurrent::print_error`].
161#[deprecated(
162    note = "use `merge_reports` and `report_failure` directly",
163    since = "0.11.0"
164)]
165pub async fn report_variables_error<S>(
166    env: &mut Env<S>,
167    errors: &[UnsetVariablesError<'_>],
168) -> crate::Result
169where
170    S: Fcntl + Isatty + Write,
171{
172    #[allow(deprecated)]
173    let (message, divert) = unset_variables_error_message(env, errors);
174    env.system.print_error(&message).await;
175    crate::Result::with_exit_status_and_divert(ExitStatus::FAILURE, divert)
176}
177
178/// Error returned by [`unset_functions`].
179#[derive(Clone, Debug, Eq, Error, PartialEq)]
180#[error("cannot unset read-only function `{name}`")]
181pub struct UnsetFunctionsError<'a> {
182    /// The name of the function
183    pub name: &'a Field,
184    /// The location where the function was made read-only
185    pub read_only_location: Location,
186}
187
188impl UnsetFunctionsError<'_> {
189    /// Converts the error to a report.
190    #[must_use]
191    pub fn to_report(&self) -> Report<'_> {
192        let mut report = Report::new();
193        report.r#type = ReportType::Error;
194        report.title = "cannot unset function".into();
195        add_span(
196            &self.name.origin.code,
197            Span {
198                range: self.name.origin.byte_range(),
199                role: SpanRole::Primary {
200                    label: format!("read-only function `{}`", self.name).into(),
201                },
202            },
203            &mut report.snippets,
204        );
205        add_span(
206            &self.read_only_location.code,
207            Span {
208                range: self.read_only_location.byte_range(),
209                role: SpanRole::Supplementary {
210                    label: format!("function `{}` was made read-only here", self.name).into(),
211                },
212            },
213            &mut report.snippets,
214        );
215        report
216    }
217}
218
219impl<'a> From<&'a UnsetFunctionsError<'_>> for Report<'a> {
220    #[inline]
221    fn from(error: &'a UnsetFunctionsError<'_>) -> Self {
222        error.to_report()
223    }
224}
225
226/// Unsets shell functions.
227///
228/// This function tries to unset all the functions named by `names`. Any error
229/// unsetting a function is reported in the returned vector and the function
230/// continues to unset the remaining functions. The returned vector is empty if
231/// all the functions are unset successfully.
232pub fn unset_functions<'a, S>(
233    env: &mut Env<S>,
234    names: &'a [Field],
235) -> Vec<UnsetFunctionsError<'a>> {
236    let mut errors = Vec::new();
237    for name in names {
238        match env.functions.unset(&name.value) {
239            Ok(_) => (),
240            Err(error) => errors.push(UnsetFunctionsError {
241                name,
242                read_only_location: error.existing.read_only_location.clone().unwrap(),
243            }),
244        }
245    }
246    errors
247}
248
249/// Creates a message that describes the errors.
250///
251/// See [`prepare_report_message_and_divert`] for the second return value.
252#[deprecated(
253    note = "use `merge_reports` and `prepare_report_message_and_divert` directly",
254    since = "0.11.0"
255)]
256#[must_use = "returned message should be printed"]
257pub fn unset_functions_error_message<S>(
258    env: &mut Env<S>,
259    errors: &[UnsetFunctionsError<'_>],
260) -> (String, yash_env::semantics::Result)
261where
262    S: Fcntl + Isatty + Write,
263{
264    let mut report = Report::new();
265    report.r#type = ReportType::Error;
266    report.title = "cannot unset function".into();
267    for error in errors {
268        add_span(
269            &error.name.origin.code,
270            Span {
271                range: error.name.origin.byte_range(),
272                role: SpanRole::Primary {
273                    label: error.to_string().into(),
274                },
275            },
276            &mut report.snippets,
277        );
278        add_span(
279            &error.read_only_location.code,
280            Span {
281                range: error.read_only_location.byte_range(),
282                role: SpanRole::Supplementary {
283                    label: format!("function `{}` was made read-only here", error.name).into(),
284                },
285            },
286            &mut report.snippets,
287        );
288    }
289    prepare_report_message_and_divert(env, report)
290}
291
292/// Prints an error message to the standard error.
293///
294/// This function constructs a message with [`unset_functions_error_message`]
295/// and prints it with [`Concurrent::print_error`].
296#[deprecated(
297    note = "use `merge_reports` and `report_failure` directly",
298    since = "0.11.0"
299)]
300pub async fn report_functions_error<S>(
301    env: &mut Env<S>,
302    errors: &[UnsetFunctionsError<'_>],
303) -> crate::Result
304where
305    S: Fcntl + Isatty + Write,
306{
307    #[allow(deprecated)]
308    let (message, divert) = unset_functions_error_message(env, errors);
309    env.system.print_error(&message).await;
310    crate::Result::with_exit_status_and_divert(ExitStatus::FAILURE, divert)
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316    use assert_matches::assert_matches;
317    use yash_env::function::Function;
318    use yash_env::source::Location;
319    use yash_env::variable::Value;
320
321    #[test]
322    fn unsetting_one_variable() {
323        let mut env = Env::new_virtual();
324        env.get_or_create_variable("foo", Global)
325            .assign("FOO", None)
326            .unwrap();
327        env.get_or_create_variable("bar", Global)
328            .assign("BAR", None)
329            .unwrap();
330        env.get_or_create_variable("baz", Global)
331            .assign("BAZ", None)
332            .unwrap();
333        let names = Field::dummies(["bar"]);
334
335        let errors = unset_variables(&mut env, &names);
336        assert_eq!(errors, []);
337        assert_eq!(
338            env.variables.get("foo").unwrap().value,
339            Some(Value::scalar("FOO")),
340        );
341        assert_eq!(env.variables.get("bar"), None);
342        assert_eq!(
343            env.variables.get("baz").unwrap().value,
344            Some(Value::scalar("BAZ")),
345        );
346    }
347
348    #[test]
349    fn unsetting_many_variables() {
350        let mut env = Env::new_virtual();
351        env.get_or_create_variable("foo", Global)
352            .assign("FOO", None)
353            .unwrap();
354        env.get_or_create_variable("bar", Global)
355            .assign("BAR", None)
356            .unwrap();
357        env.get_or_create_variable("baz", Global)
358            .assign("BAZ", None)
359            .unwrap();
360        let names = Field::dummies(["bar", "foo", "baz"]);
361
362        let errors = unset_variables(&mut env, &names);
363        assert_eq!(errors, []);
364        assert_eq!(env.variables.get("foo"), None);
365        assert_eq!(env.variables.get("bar"), None);
366        assert_eq!(env.variables.get("baz"), None);
367    }
368
369    #[test]
370    fn unsetting_readonly_variables() {
371        let mut env = Env::new_virtual();
372        let mut a = env.get_or_create_variable("a", Global);
373        a.assign("A", None).unwrap();
374        let mut b = env.get_or_create_variable("b", Global);
375        b.assign("B", None).unwrap();
376        let location_b = Location::dummy("readonly b");
377        b.make_read_only(location_b.clone());
378        let mut c = env.get_or_create_variable("c", Global);
379        c.assign("C", None).unwrap();
380        let location_c = Location::dummy("readonly c");
381        c.make_read_only(location_c.clone());
382        let mut d = env.get_or_create_variable("d", Global);
383        d.assign("D", None).unwrap();
384        let names = Field::dummies(["a", "b", "c", "d"]);
385
386        let errors = unset_variables(&mut env, &names);
387        assert_matches!(&errors[..], [e1, e2] => {
388            assert_eq!(e1.name, &Field::dummy("b"));
389            assert_eq!(e1.read_only_location, location_b);
390            assert_eq!(e2.name, &Field::dummy("c"));
391            assert_eq!(e2.read_only_location, location_c);
392        });
393        assert_eq!(env.variables.get("a"), None);
394        assert_eq!(
395            env.variables.get("b").unwrap().value,
396            Some(Value::scalar("B")),
397        );
398        assert_eq!(
399            env.variables.get("c").unwrap().value,
400            Some(Value::scalar("C")),
401        );
402        assert_eq!(env.variables.get("d"), None);
403    }
404
405    fn dummy_function<S>(name: &str) -> Function<S> {
406        let body = yash_env::test_helper::function::FunctionBodyStub::rc_dyn();
407        Function::new(name, body, Location::dummy(name))
408    }
409
410    #[test]
411    fn unsetting_one_function() {
412        let mut env = Env::new_virtual();
413        env.functions.define(dummy_function("foo")).unwrap();
414        env.functions.define(dummy_function("bar")).unwrap();
415        env.functions.define(dummy_function("baz")).unwrap();
416        let names = Field::dummies(["foo"]);
417
418        let errors = unset_functions(&mut env, &names);
419        assert_eq!(errors, []);
420        assert_eq!(env.functions.get("foo"), None);
421        assert_eq!(env.functions.get("bar").unwrap().name, "bar");
422        assert_eq!(env.functions.get("baz").unwrap().name, "baz");
423    }
424
425    #[test]
426    fn unsetting_many_functions() {
427        let mut env = Env::new_virtual();
428        env.functions.define(dummy_function("foo")).unwrap();
429        env.functions.define(dummy_function("bar")).unwrap();
430        env.functions.define(dummy_function("baz")).unwrap();
431        let names = Field::dummies(["bar", "foo", "baz"]);
432
433        let errors = unset_functions(&mut env, &names);
434        assert_eq!(errors, []);
435        assert_eq!(env.functions.get("foo"), None);
436        assert_eq!(env.functions.get("bar"), None);
437        assert_eq!(env.functions.get("baz"), None);
438    }
439
440    #[test]
441    fn unsetting_readonly_function() {
442        let mut env = Env::new_virtual();
443        env.functions.define(dummy_function("a")).unwrap();
444        let location_b = Location::dummy("readonly b");
445        env.functions
446            .define(dummy_function("b").make_read_only(location_b.clone()))
447            .unwrap();
448        let location_c = Location::dummy("readonly c");
449        env.functions
450            .define(dummy_function("c").make_read_only(location_c.clone()))
451            .unwrap();
452        env.functions.define(dummy_function("d")).unwrap();
453        let names = Field::dummies(["a", "b", "c", "d"]);
454
455        let errors = unset_functions(&mut env, &names);
456        assert_matches!(&errors[..], [e1, e2] => {
457            assert_eq!(e1.name, &Field::dummy("b"));
458            assert_eq!(e1.read_only_location, location_b);
459            assert_eq!(e2.name, &Field::dummy("c"));
460            assert_eq!(e2.read_only_location, location_c);
461        });
462        assert_eq!(env.functions.get("a"), None);
463        assert_eq!(env.functions.get("b").unwrap().name, "b");
464        assert_eq!(env.functions.get("c").unwrap().name, "c");
465        assert_eq!(env.functions.get("d"), None);
466    }
467
468    // TODO unsetting_global_variable_hidden_by_local_variable: should unset the both
469    // TODO unsetting_readonly_global_variable_hidden_by_local_variable: should unset the local only
470}