rush_sh/state/
options.rs

1//! Shell Options Module
2//!
3//! This module provides the `ShellOptions` struct and related functionality for managing
4//! POSIX shell options that control shell behavior. These options can be set using the
5//! `set` builtin command with either short flags (e.g., `-e`, `-u`) or long names
6//! (e.g., `-o errexit`, `-o nounset`).
7//!
8//! # POSIX Compliance
9//!
10//! The shell options implementation follows IEEE Std 1003.1-2008 (POSIX.1-2008) for the
11//! `set` builtin command. Options can be:
12//! - Enabled with `set -<flag>` or `set -o <option>`
13//! - Disabled with `set +<flag>` or `set +o <option>`
14//! - Listed with `set -o` (shows all options and their current state)
15//!
16//! # Available Options
17//!
18//! ## Standard POSIX Options
19//!
20//! - **errexit** (`-e`): Exit immediately if a command exits with a non-zero status.
21//!   Does not apply to commands in conditions (if/while/until), logical chains (&&/||),
22//!   or negated commands (!).
23//!
24//! - **nounset** (`-u`): Treat unset variables as an error when performing parameter expansion.
25//!   Causes the shell to write a message to stderr and exit (in non-interactive mode).
26//!
27//! - **xtrace** (`-x`): Print commands and their arguments as they are executed.
28//!   Useful for debugging shell scripts.
29//!
30//! - **verbose** (`-v`): Print shell input lines as they are read.
31//!   Shows the raw input before any processing.
32//!
33//! - **noexec** (`-n`): Read commands but do not execute them.
34//!   Useful for syntax checking scripts.
35//!
36//! - **noglob** (`-f`): Disable pathname expansion (globbing).
37//!   Wildcards like `*` and `?` are treated as literal characters.
38//!
39//! - **noclobber** (`-C`): Prevent output redirection from overwriting existing files.
40//!   The `>|` operator can be used to override this restriction.
41//!
42//! - **allexport** (`-a`): Automatically mark all variables for export to child processes.
43//!   Variables become environment variables when set.
44//!
45//! - **notify** (`-b`): Report the status of background jobs immediately.
46//!   Normally, job status is reported before the next prompt.
47//!
48//! - **monitor** (`-m`): Enable job control.
49//!   Allows background jobs, job suspension, and job management commands.
50//!
51//! ## Extended Options
52//!
53//! - **ignoreeof**: Ignore EOF (Ctrl+D) to exit the shell.
54//!   Requires explicit `exit` command to terminate the shell.
55//!   Note: This option has no short flag equivalent.
56//!
57//! # Examples
58//!
59//! ```
60//! use rush_sh::state::ShellOptions;
61//!
62//! let mut options = ShellOptions::default();
63//!
64//! // Enable errexit using short name
65//! options.set_by_short_name('e', true).unwrap();
66//! assert!(options.errexit);
67//!
68//! // Enable nounset using long name
69//! options.set_by_long_name("nounset", true).unwrap();
70//! assert!(options.nounset);
71//!
72//! // Check option value
73//! assert_eq!(options.get_by_short_name('e'), Some(true));
74//! assert_eq!(options.get_by_long_name("nounset"), Some(true));
75//!
76//! // List all options
77//! let all_options = options.get_all_options();
78//! for (name, short, value) in all_options {
79//!     println!("{} ({}): {}", name, short, if value { "on" } else { "off" });
80//! }
81//! ```
82
83/// Shell option flags that control shell behavior
84///
85/// This struct contains boolean flags for all supported shell options.
86/// Each option can be accessed directly or through the getter/setter methods
87/// that support both short and long option names.
88#[derive(Debug, Clone)]
89pub struct ShellOptions {
90    /// -e: Exit on command failure
91    pub errexit: bool,
92
93    /// -u: Treat unset variables as error
94    pub nounset: bool,
95
96    /// -x: Print commands before execution
97    pub xtrace: bool,
98
99    /// -v: Print input lines as read
100    pub verbose: bool,
101
102    /// -n: Read but don't execute commands
103    pub noexec: bool,
104
105    /// -f: Disable pathname expansion
106    pub noglob: bool,
107
108    /// -C: Prevent overwriting files with redirection
109    pub noclobber: bool,
110
111    /// -a: Auto-export all variables
112    pub allexport: bool,
113
114    /// -b: Notify of job completion immediately
115    pub notify: bool,
116
117    /// Ignore EOF (Ctrl+D) - not a standard POSIX option but commonly supported
118    pub ignoreeof: bool,
119
120    /// -m: Enable job control (monitor)
121    pub monitor: bool,
122}
123
124impl Default for ShellOptions {
125    /// Create a ShellOptions with all option flags set to false.
126    ///
127    /// # Examples
128    ///
129    /// ```
130    /// use rush_sh::state::ShellOptions;
131    /// let opts = ShellOptions::default();
132    /// assert!(!opts.errexit && !opts.nounset && !opts.xtrace);
133    /// ```
134    fn default() -> Self {
135        Self {
136            errexit: false,
137            nounset: false,
138            xtrace: false,
139            verbose: false,
140            noexec: false,
141            noglob: false,
142            noclobber: false,
143            allexport: false,
144            notify: false,
145            ignoreeof: false,
146            monitor: false,
147        }
148    }
149}
150
151impl ShellOptions {
152    /// Retrieve the value of a shell option by its short-name flag.
153    ///
154    /// Returns `Some(bool)` with the option's current value for recognized short names; `None` if the short name is not recognized.
155    ///
156    /// # Examples
157    ///
158    /// ```
159    /// use rush_sh::state::ShellOptions;
160    /// let opts = ShellOptions::default();
161    /// assert_eq!(opts.get_by_short_name('e'), Some(false)); // errexit is false by default
162    /// assert_eq!(opts.get_by_short_name('?'), None); // unknown short name
163    /// ```
164    #[allow(dead_code)]
165    pub fn get_by_short_name(&self, name: char) -> Option<bool> {
166        match name {
167            'e' => Some(self.errexit),
168            'u' => Some(self.nounset),
169            'x' => Some(self.xtrace),
170            'v' => Some(self.verbose),
171            'n' => Some(self.noexec),
172            'f' => Some(self.noglob),
173            'C' => Some(self.noclobber),
174            'a' => Some(self.allexport),
175            'b' => Some(self.notify),
176            'm' => Some(self.monitor),
177            _ => None,
178        }
179    }
180
181    /// Set a shell option identified by its single-character short name.
182    ///
183    /// Sets the option corresponding to `name` to `value`. Recognized short names:
184    /// 'e' (errexit), 'u' (nounset), 'x' (xtrace), 'v' (verbose), 'n' (noexec),
185    /// 'f' (noglob), 'C' (noclobber), 'a' (allexport), 'b' (notify), 'm' (monitor).
186    ///
187    /// # Arguments
188    ///
189    /// * `name` - single-character short option name.
190    /// * `value` - true to enable the option, false to disable it.
191    ///
192    /// # Returns
193    ///
194    /// `Ok(())` on success, or `Err(String)` if `name` is not a recognized option.
195    ///
196    /// # Examples
197    ///
198    /// ```
199    /// use rush_sh::state::ShellOptions;
200    /// let mut opts = ShellOptions::default();
201    /// opts.set_by_short_name('e', true).unwrap();
202    /// assert!(opts.errexit);
203    /// ```
204    pub fn set_by_short_name(&mut self, name: char, value: bool) -> Result<(), String> {
205        match name {
206            'e' => {
207                self.errexit = value;
208                Ok(())
209            }
210            'u' => {
211                self.nounset = value;
212                Ok(())
213            }
214            'x' => {
215                self.xtrace = value;
216                Ok(())
217            }
218            'v' => {
219                self.verbose = value;
220                Ok(())
221            }
222            'n' => {
223                self.noexec = value;
224                Ok(())
225            }
226            'f' => {
227                self.noglob = value;
228                Ok(())
229            }
230            'C' => {
231                self.noclobber = value;
232                Ok(())
233            }
234            'a' => {
235                self.allexport = value;
236                Ok(())
237            }
238            'b' => {
239                self.notify = value;
240                Ok(())
241            }
242            'm' => {
243                self.monitor = value;
244                Ok(())
245            }
246            _ => Err(format!("Invalid option: -{}", name)),
247        }
248    }
249
250    /// Retrieve the value of a shell option by its long name.
251    ///
252    /// `name` is the option's full identifier (for example: "errexit", "nounset", "xtrace").
253    ///
254    /// # Returns
255    ///
256    /// `Some(true)` if the option is enabled, `Some(false)` if the option is disabled, or `None` if the name is not recognized.
257    ///
258    /// # Examples
259    ///
260    /// ```
261    /// use rush_sh::state::ShellOptions;
262    /// let mut opts = ShellOptions::default();
263    /// opts.errexit = true;
264    /// assert_eq!(opts.get_by_long_name("errexit"), Some(true));
265    /// assert_eq!(opts.get_by_long_name("noglob"), Some(false));
266    /// assert_eq!(opts.get_by_long_name("unknown"), None);
267    /// ```
268    #[allow(dead_code)]
269    pub fn get_by_long_name(&self, name: &str) -> Option<bool> {
270        match name {
271            "errexit" => Some(self.errexit),
272            "nounset" => Some(self.nounset),
273            "xtrace" => Some(self.xtrace),
274            "verbose" => Some(self.verbose),
275            "noexec" => Some(self.noexec),
276            "noglob" => Some(self.noglob),
277            "noclobber" => Some(self.noclobber),
278            "allexport" => Some(self.allexport),
279            "notify" => Some(self.notify),
280            "ignoreeof" => Some(self.ignoreeof),
281            "monitor" => Some(self.monitor),
282            _ => None,
283        }
284    }
285
286    /// Set a shell option by its long name.
287    ///
288    /// Sets the specified long-form option (for example `"errexit"` or `"nounset"`) to the provided boolean value.
289    /// Returns `Ok(())` if the option was recognized and set, or `Err(String)` if the name is not recognized.
290    ///
291    /// # Examples
292    ///
293    /// ```
294    /// use rush_sh::state::ShellOptions;
295    /// let mut opts = ShellOptions::default();
296    /// opts.set_by_long_name("errexit", true).unwrap();
297    /// assert!(opts.errexit);
298    ///
299    /// assert!(opts.set_by_long_name("nonexistent", true).is_err());
300    /// ```
301    pub fn set_by_long_name(&mut self, name: &str, value: bool) -> Result<(), String> {
302        match name {
303            "errexit" => {
304                self.errexit = value;
305                Ok(())
306            }
307            "nounset" => {
308                self.nounset = value;
309                Ok(())
310            }
311            "xtrace" => {
312                self.xtrace = value;
313                Ok(())
314            }
315            "verbose" => {
316                self.verbose = value;
317                Ok(())
318            }
319            "noexec" => {
320                self.noexec = value;
321                Ok(())
322            }
323            "noglob" => {
324                self.noglob = value;
325                Ok(())
326            }
327            "noclobber" => {
328                self.noclobber = value;
329                Ok(())
330            }
331            "allexport" => {
332                self.allexport = value;
333                Ok(())
334            }
335            "notify" => {
336                self.notify = value;
337                Ok(())
338            }
339            "ignoreeof" => {
340                self.ignoreeof = value;
341                Ok(())
342            }
343            "monitor" => {
344                self.monitor = value;
345                Ok(())
346            }
347            _ => Err(format!("Invalid option: {}", name)),
348        }
349    }
350
351    /// Lists all shell option names with their short-letter aliases and current values.
352    ///
353    /// Returns a vector of tuples `(long_name, short_name, value)` for every supported option.
354    /// The `short_name` is `'\0'` when no short alias exists.
355    ///
356    /// # Examples
357    ///
358    /// ```
359    /// use rush_sh::state::ShellOptions;
360    /// let opts = ShellOptions::default();
361    /// let all = opts.get_all_options();
362    /// assert!(all.iter().any(|(name, _, _)| *name == "errexit"));
363    /// assert!(all.iter().any(|(name, short, _)| *name == "ignoreeof" && *short == '\0'));
364    /// ```
365    pub fn get_all_options(&self) -> Vec<(&'static str, char, bool)> {
366        vec![
367            ("allexport", 'a', self.allexport),
368            ("notify", 'b', self.notify),
369            ("noclobber", 'C', self.noclobber),
370            ("errexit", 'e', self.errexit),
371            ("noglob", 'f', self.noglob),
372            ("monitor", 'm', self.monitor),
373            ("noexec", 'n', self.noexec),
374            ("nounset", 'u', self.nounset),
375            ("verbose", 'v', self.verbose),
376            ("xtrace", 'x', self.xtrace),
377            ("ignoreeof", '\0', self.ignoreeof), // No short option
378        ]
379    }
380}