Skip to main content

yash_builtin/
set.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//! Set built-in
18//!
19//! This module implements the [`set` built-in], which modifies shell options and
20//! positional parameters.
21//!
22//! [`set` built-in]: https://magicant.github.io/yash-rs/builtins/set.html
23//!
24//! # Implementation notes
25//!
26//! See [`parse_short`] for a list of available short options and [`parse_long`]
27//! to learn how long options are parsed.
28//! Long options are [canonicalize]d before being passed to `parse_long`.
29
30use crate::common::output;
31use crate::common::report::report_error;
32use yash_env::Env;
33use yash_env::builtin::Result;
34use yash_env::option::State;
35#[cfg(doc)]
36use yash_env::option::canonicalize;
37#[cfg(doc)]
38use yash_env::option::parse_long;
39#[cfg(doc)]
40use yash_env::option::parse_short;
41use yash_env::option::{Interactive, Monitor};
42use yash_env::parser::IsName;
43use yash_env::semantics::ExitStatus;
44use yash_env::semantics::Field;
45use yash_env::stack::Frame::Subshell;
46use yash_env::system::{
47    Close, Dup, Fcntl, GetPid, Isatty, Open, Sigaction, Sigmask, Signals, TcSetPgrp, Write,
48};
49use yash_env::variable::Scope::Global;
50
51/// Interpretation of command-line arguments that determine the behavior of the
52/// set built-in
53#[derive(Clone, Debug, Eq, PartialEq)]
54pub enum Command {
55    /// No arguments: print all variables
56    PrintVariables,
57
58    /// Single argument `-o`: print options (human-readable)
59    PrintOptionsHumanReadable,
60
61    /// Single argument `+o`: print options (machine-readable)
62    PrintOptionsMachineReadable,
63
64    /// Other: modify options and/or positional parameters
65    Modify {
66        /// Options to be modified
67        options: Vec<(yash_env::option::Option, State)>,
68        /// New positional parameters (unless `None`)
69        positional_params: std::option::Option<Vec<Field>>,
70    },
71}
72
73pub mod syntax;
74// TODO pub mod semantics;
75
76/// Enables or disables the internal dispositions for the "stopper" signals
77/// depending on the `Interactive` and `Monitor` option states.
78async fn update_internal_dispositions_for_stoppers<S>(env: &mut Env<S>)
79where
80    S: Signals + Sigmask + Sigaction,
81{
82    if env.options.get(Interactive) == State::On && env.options.get(Monitor) == State::On {
83        env.traps
84            .enable_internal_dispositions_for_stoppers(&env.system)
85            .await
86    } else {
87        env.traps
88            .disable_internal_dispositions_for_stoppers(&env.system)
89            .await
90    }
91    .ok();
92}
93
94/// Ensures that the shell is in the foreground process group if the `Monitor`
95/// option is enabled.
96async fn ensure_foreground<S>(env: &mut Env<S>)
97where
98    S: Open + Dup + Close + GetPid + Signals + Sigmask + Sigaction + TcSetPgrp,
99{
100    if env.options.get(Monitor) == State::On {
101        env.ensure_foreground().await.ok();
102    }
103}
104
105/// Modifies shell options and positional parameters.
106async fn modify<S>(
107    env: &mut Env<S>,
108    options: Vec<(yash_env::option::Option, State)>,
109    positional_params: Option<Vec<Field>>,
110) where
111    S: Open + Dup + Close + GetPid + Signals + Sigmask + Sigaction + TcSetPgrp,
112{
113    // Modify options
114    let mut monitor_changed = false;
115    for (option, state) in options {
116        env.options.set(option, state);
117        monitor_changed |= option == Monitor;
118    }
119
120    // Reinitialize job control
121    if monitor_changed && !env.stack.contains(&Subshell) {
122        // We ignore errors in theses functions because they are not essential
123        // for updating the options.
124        update_internal_dispositions_for_stoppers(env).await;
125        ensure_foreground(env).await;
126    }
127
128    // Modify positional parameters
129    if let Some(fields) = positional_params {
130        let params = env.variables.positional_params_mut();
131        params.values = fields.into_iter().map(|f| f.value).collect();
132        params.last_modified_location = env.stack.current_builtin().map(|b| b.name.origin.clone());
133    }
134}
135
136/// Entry point for executing the `set` built-in
137///
138/// This function requires an instance of [`IsName`] to be present in the
139/// environment's [`any`](Env::any) storage to check for valid variable names.
140/// If no such instance is found, this function will **panic**.
141pub async fn main<S>(env: &mut Env<S>, args: Vec<Field>) -> Result
142where
143    S: Open
144        + Dup
145        + Close
146        + Fcntl
147        + GetPid
148        + Isatty
149        + Signals
150        + Sigmask
151        + Sigaction
152        + TcSetPgrp
153        + Write
154        + 'static,
155{
156    use std::fmt::Write as _;
157
158    match syntax::parse(args) {
159        Ok(Command::PrintVariables) => {
160            let IsName(is_name) = env.any.get().expect("`IsName` should be in `env.any`");
161            let mut vars: Vec<_> = env
162                .variables
163                .iter(Global)
164                .filter(|(name, _)| is_name(env, name))
165                .collect();
166            // TODO apply current locale's collation
167            vars.sort_unstable_by_key(|&(name, _)| name);
168
169            let mut print = String::new();
170            for (name, var) in vars {
171                if let Some(value) = &var.value {
172                    writeln!(print, "{}={}", name, value.quote()).unwrap();
173                }
174            }
175            output(env, &print).await
176        }
177
178        Ok(Command::PrintOptionsHumanReadable) => {
179            let mut print = String::new();
180            for option in yash_env::option::Option::iter() {
181                let state = env.options.get(option);
182                writeln!(print, "{option:16} {state}").unwrap();
183            }
184            output(env, &print).await
185        }
186
187        Ok(Command::PrintOptionsMachineReadable) => {
188            let mut print = String::new();
189            for option in yash_env::option::Option::iter() {
190                let skip = if option.is_modifiable() { "" } else { "#" };
191                let flag = match env.options.get(option) {
192                    State::On => '-',
193                    State::Off => '+',
194                };
195                writeln!(print, "{skip}set {flag}o {option}").unwrap();
196            }
197            output(env, &print).await
198        }
199
200        Ok(Command::Modify {
201            options,
202            positional_params,
203        }) => {
204            modify(env, options, positional_params).await;
205            Result::new(ExitStatus::SUCCESS)
206        }
207
208        Err(error) => report_error(env, &error).await,
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215    use futures_util::FutureExt;
216    use std::ops::ControlFlow::Continue;
217    use std::rc::Rc;
218    use yash_env::VirtualSystem;
219    use yash_env::builtin::Builtin;
220    use yash_env::builtin::Type::Special;
221    use yash_env::option::Option::*;
222    use yash_env::option::OptionSet;
223    use yash_env::option::State::*;
224    use yash_env::system::Disposition;
225    use yash_env::system::r#virtual::SIGTSTP;
226    use yash_env::test_helper::assert_stderr;
227    use yash_env::test_helper::assert_stdout;
228    use yash_env::variable::Scope;
229    use yash_env::variable::Value;
230    use yash_semantics::command::Command as _;
231    use yash_syntax::syntax::List;
232
233    #[test]
234    fn printing_variables() {
235        let system = VirtualSystem::new();
236        let state = Rc::clone(&system.state);
237        let mut env = Env::with_system(system);
238        env.any
239            .insert(Box::new(IsName::<VirtualSystem>(|_env, name| {
240                yash_syntax::parser::lex::is_name(name)
241            })));
242        let mut var = env.variables.get_or_new("foo", Scope::Global);
243        var.assign("value", None).unwrap();
244        var.export(true);
245        let mut var = env.variables.get_or_new("bar", Scope::Global);
246        var.assign("Hello, world!", None).unwrap();
247        let mut var = env.variables.get_or_new("baz", Scope::Global);
248        var.assign(Value::array(["one", ""]), None).unwrap();
249        let mut var = env.variables.get_or_new("bad=name", Scope::Global);
250        var.assign("Oops!", None).unwrap();
251
252        let args = vec![];
253        let result = main(&mut env, args).now_or_never().unwrap();
254        assert_eq!(result, Result::new(ExitStatus::SUCCESS));
255        assert_stdout(&state, |stdout| {
256            assert_eq!(stdout, "bar='Hello, world!'\nbaz=(one '')\nfoo=value\n")
257        });
258    }
259
260    #[test]
261    fn printing_options_human_readable() {
262        let system = VirtualSystem::new();
263        let state = Rc::clone(&system.state);
264        let mut env = Env::with_system(system);
265        env.options.set(AllExport, On);
266        env.options.set(Unset, Off);
267
268        let args = Field::dummies(["-o"]);
269        let result = main(&mut env, args).now_or_never().unwrap();
270        assert_eq!(result, Result::new(ExitStatus::SUCCESS));
271        assert_stdout(&state, |stdout| {
272            assert_eq!(
273                stdout,
274                "allexport        on
275clobber          on
276cmdline          off
277errexit          off
278exec             on
279glob             on
280hashondefinition off
281ignoreeof        off
282interactive      off
283log              on
284login            off
285monitor          off
286notify           off
287pipefail         off
288posixlycorrect   off
289stdin            off
290unset            off
291verbose          off
292vi               off
293xtrace           off
294"
295            )
296        });
297    }
298
299    #[test]
300    fn printing_options_machine_readable() {
301        let system = VirtualSystem::new();
302        let state = Rc::clone(&system.state);
303        let mut env = Env::with_system(system);
304        env.options.set(Clobber, Off);
305        env.options.set(Verbose, On);
306        let options = env.options;
307
308        let args = Field::dummies(["+o"]);
309        let result = main(&mut env, args).now_or_never().unwrap();
310        assert_eq!(result, Result::new(ExitStatus::SUCCESS));
311
312        // The output from `set +o` should be parsable
313        let commands: List = assert_stdout(&state, |stdout| stdout.parse().unwrap());
314
315        env.builtins.insert(
316            "set",
317            Builtin::new(Special, |env, args| Box::pin(main(env, args))),
318        );
319        env.options = Default::default();
320
321        // Executing the parsed command should restore the previous options
322        let result = commands.execute(&mut env).now_or_never().unwrap();
323        assert_eq!(result, Continue(()));
324        assert_eq!(env.exit_status, ExitStatus::SUCCESS);
325        assert_eq!(env.options, options);
326
327        // And there should be no errors doing that
328        assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
329    }
330
331    #[test]
332    fn setting_some_options() {
333        let mut env = Env::new_virtual();
334        let args = Field::dummies(["-a", "-n"]);
335        let result = main(&mut env, args).now_or_never().unwrap();
336        assert_eq!(result, Result::new(ExitStatus::SUCCESS));
337
338        let mut options = OptionSet::default();
339        options.set(AllExport, On);
340        options.set(Exec, Off);
341        assert_eq!(env.options, options);
342    }
343
344    #[test]
345    fn setting_some_positional_parameters() {
346        let name = Field::dummy("set");
347        let location = name.origin.clone();
348        let is_special = true;
349        let mut env = Env::new_virtual();
350        let mut env = env.push_frame(yash_env::stack::Builtin { name, is_special }.into());
351        let args = Field::dummies(["a", "b", "z"]);
352
353        let result = main(&mut env, args).now_or_never().unwrap();
354        assert_eq!(result, Result::new(ExitStatus::SUCCESS));
355
356        let params = env.variables.positional_params();
357        assert_eq!(
358            params.values,
359            ["a".to_string(), "b".to_string(), "z".to_string()],
360        );
361        assert_eq!(params.last_modified_location, Some(location));
362    }
363
364    #[test]
365    fn enabling_monitor_option() {
366        let system = VirtualSystem::new();
367        let state = Rc::clone(&system.state);
368        let mut env = Env::with_system(system);
369        env.options.set(Interactive, On);
370        let args = Field::dummies(["-m"]);
371
372        let result = main(&mut env, args).now_or_never().unwrap();
373        assert_eq!(result, Result::new(ExitStatus::SUCCESS));
374        let mut expected_options = OptionSet::default();
375        expected_options.extend([Interactive, Monitor]);
376        assert_eq!(env.options, expected_options);
377        let state = state.borrow();
378        let disposition = state.processes[&env.main_pid].disposition(SIGTSTP);
379        assert_eq!(disposition, Disposition::Ignore);
380    }
381
382    #[test]
383    fn disabling_monitor_option() {
384        let system = VirtualSystem::new();
385        let state = Rc::clone(&system.state);
386        let mut env = Env::with_system(system);
387        env.options.set(Interactive, On);
388        let args = Field::dummies(["-m"]);
389        _ = main(&mut env, args).now_or_never().unwrap();
390        let args = Field::dummies(["+m"]);
391
392        let result = main(&mut env, args).now_or_never().unwrap();
393        assert_eq!(result, Result::new(ExitStatus::SUCCESS));
394        let mut expected_options = OptionSet::default();
395        expected_options.set(Interactive, On);
396        assert_eq!(env.options, expected_options);
397        let state = state.borrow();
398        let disposition = state.processes[&env.main_pid].disposition(SIGTSTP);
399        assert_eq!(disposition, Disposition::Default);
400    }
401
402    #[test]
403    fn internal_dispositions_not_enabled_for_stoppers_in_non_interactive_shell() {
404        let system = VirtualSystem::new();
405        let state = Rc::clone(&system.state);
406        let mut env = Env::with_system(system);
407        let args = Field::dummies(["-m"]);
408
409        let result = main(&mut env, args).now_or_never().unwrap();
410        assert_eq!(result, Result::new(ExitStatus::SUCCESS));
411        let mut expected_options = OptionSet::default();
412        expected_options.set(Monitor, On);
413        assert_eq!(env.options, expected_options);
414        let state = state.borrow();
415        let disposition = state.processes[&env.main_pid].disposition(SIGTSTP);
416        assert_eq!(disposition, Disposition::Default);
417    }
418
419    #[test]
420    fn internal_dispositions_not_enabled_for_stoppers_in_subshell() {
421        let system = VirtualSystem::new();
422        let state = Rc::clone(&system.state);
423        let mut env = Env::with_system(system);
424        let mut env = env.push_frame(Subshell);
425        env.options.set(Interactive, On);
426        let args = Field::dummies(["-m"]);
427
428        let result = main(&mut env, args).now_or_never().unwrap();
429        assert_eq!(result, Result::new(ExitStatus::SUCCESS));
430        let mut expected_options = OptionSet::default();
431        expected_options.extend([Interactive, Monitor]);
432        assert_eq!(env.options, expected_options);
433        let state = state.borrow();
434        let disposition = state.processes[&env.main_pid].disposition(SIGTSTP);
435        assert_eq!(disposition, Disposition::Default);
436    }
437
438    // TODO Test the case when the -m option is enabled while the shell is not
439    // in the foreground. This requires the correct implementation of the
440    // `VirtualSystem::tcsetpgrp` method.
441}