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}