Skip to main content

brush_shell/
args.rs

1//! Types for brush command-line parsing.
2
3use clap::{Parser, builder::styling};
4use std::io::IsTerminal;
5use std::path::PathBuf;
6
7use crate::{events, productinfo};
8
9const SHORT_DESCRIPTION: &str = "Bo[u]rn[e] RUsty SHell 🦀 (https://brush.sh)";
10
11const LONG_DESCRIPTION: &str = r"brush is a bash-compatible, Rust-implemented, POSIX-style shell.
12
13brush is distributed under the terms of the MIT license. If you encounter any issues or discrepancies in behavior from bash, please report them at https://github.com/reubeno/brush.
14
15For more information, visit https://brush.sh.";
16
17const USAGE: &str = color_print::cstr!(
18    "<bold>brush</bold> <italics>[OPTIONS]</italics>... <italics>[SCRIPT_PATH [SCRIPT_ARGS]...]</italics>"
19);
20
21const VERSION: &str = const_format::concatcp!(
22    productinfo::PRODUCT_VERSION,
23    " (",
24    productinfo::PRODUCT_GIT_VERSION,
25    ")"
26);
27
28const HEADING_STANDARD_OPTIONS: &str = "Standard shell options";
29
30const HEADING_CONFIG_OPTIONS: &str = "Configuration options";
31
32const HEADING_UI_OPTIONS: &str = "User interface options";
33
34const HEADING_EXPERIMENTAL_OPTIONS: &str = "*Experimental* options (unstable)";
35
36/// Identifies the input backend to use for the shell.
37#[derive(Clone, Copy, clap::ValueEnum)]
38pub enum InputBackendType {
39    /// Richest input backend, based on reedline.
40    Reedline,
41    /// Basic input backend that provides minimal completion support for testing.
42    Basic,
43    /// Most minimal input backend.
44    Minimal,
45}
46
47/// Parsed command-line arguments for the brush shell.
48#[derive(Clone, Parser)]
49#[clap(name = productinfo::PRODUCT_NAME,
50       version = VERSION,
51       about = SHORT_DESCRIPTION,
52       long_about = LONG_DESCRIPTION,
53       author,
54       override_usage = USAGE,
55       disable_help_flag = true,
56       disable_version_flag = true,
57       styles = brush_help_styles())]
58pub struct CommandLineArgs {
59    /// Display usage information.
60    #[clap(long = "help", action = clap::ArgAction::HelpShort)]
61    pub help: Option<bool>,
62
63    /// Display shell version.
64    #[clap(long = "version", action = clap::ArgAction::Version)]
65    pub version: Option<bool>,
66
67    /// Path to TOML-based `brush` config file (overrides default location).
68    #[clap(long = "config", value_name = "FILE", help_heading = HEADING_CONFIG_OPTIONS)]
69    pub config_file: Option<PathBuf>,
70
71    /// Disable loading of TOML-based `brush` config file.
72    #[clap(long = "no-config", help_heading = HEADING_CONFIG_OPTIONS)]
73    pub no_config: bool,
74
75    /// Enable `noclobber` shell option.
76    #[arg(short = 'C', help_heading = HEADING_STANDARD_OPTIONS)]
77    pub disallow_overwriting_regular_files_via_output_redirection: bool,
78
79    /// Execute the provided command and then exit.
80    #[arg(short = 'c', value_name = "COMMAND", help_heading = HEADING_STANDARD_OPTIONS)]
81    pub command: Option<String>,
82
83    /// Enable error-on-exit behavior.
84    #[clap(short = 'e', help_heading = HEADING_STANDARD_OPTIONS)]
85    pub exit_on_nonzero_command_exit: bool,
86
87    /// Disable pathname expansion (also known as filename globbing).
88    #[clap(short = 'f', help_heading = HEADING_STANDARD_OPTIONS)]
89    pub disable_pathname_expansion: bool,
90
91    /// Run in interactive mode.
92    #[clap(short = 'i', help_heading = HEADING_STANDARD_OPTIONS)]
93    pub interactive: bool,
94
95    /// Inherit the specified file descriptors injected by the parent process.
96    #[clap(long = "inherit-fd", value_name = "FD", help_heading = HEADING_STANDARD_OPTIONS)]
97    pub inherited_fds: Vec<i32>,
98
99    /// Make shell act as if it had been invoked as a login shell.
100    #[clap(short = 'l', long = "login", help_heading = HEADING_STANDARD_OPTIONS)]
101    pub login: bool,
102
103    /// Do not execute commands.
104    #[clap(short = 'n', help_heading = HEADING_STANDARD_OPTIONS)]
105    pub do_not_execute_commands: bool,
106
107    /// Don't use readline for input.
108    #[clap(long = "noediting", help_heading = HEADING_STANDARD_OPTIONS)]
109    pub no_editing: bool,
110
111    /// Don't process any profile/login files (`/etc/profile`, `~/.bash_profile`, `~/.bash_login`,
112    /// `~/.profile`).
113    #[clap(long = "noprofile", help_heading = HEADING_STANDARD_OPTIONS)]
114    pub no_profile: bool,
115
116    /// Don't process "rc" files if the shell is interactive (e.g., `~/.bashrc`, `~/.brushrc`).
117    #[clap(long = "norc", help_heading = HEADING_STANDARD_OPTIONS)]
118    pub no_rc: bool,
119
120    /// Don't inherit environment variables from the calling process.
121    #[clap(long = "noenv", help_heading = HEADING_STANDARD_OPTIONS)]
122    pub do_not_inherit_env: bool,
123
124    /// Enable option (`set -o` option).
125    #[clap(short = 'o', value_name = "OPTION", help_heading = HEADING_STANDARD_OPTIONS)]
126    pub enabled_options: Vec<String>,
127
128    /// Disable option (`set -o` option).
129    #[clap(long = "+o", value_name = "OPTION", hide = true, help_heading = HEADING_STANDARD_OPTIONS)]
130    pub disabled_options: Vec<String>,
131
132    /// Enable `shopt` option.
133    #[clap(short = 'O', value_name = "SHOPT_OPTION", help_heading = HEADING_STANDARD_OPTIONS)]
134    pub enabled_shopt_options: Vec<String>,
135
136    /// Disable `shopt` option.
137    #[clap(long = "+O", value_name = "SHOPT_OPTION", hide = true, help_heading = HEADING_STANDARD_OPTIONS)]
138    pub disabled_shopt_options: Vec<String>,
139
140    /// Disable non-POSIX extensions.
141    #[clap(long = "posix", help_heading = HEADING_STANDARD_OPTIONS)]
142    pub posix: bool,
143
144    /// Path to the rc file to load in interactive shells (instead of `bash.bashrc` and
145    /// `~/.bashrc`).
146    #[clap(long = "rcfile", alias = "init-file", value_name = "FILE", help_heading = HEADING_STANDARD_OPTIONS)]
147    pub rc_file: Option<PathBuf>,
148
149    /// Read commands from standard input.
150    #[clap(short = 's', help_heading = HEADING_STANDARD_OPTIONS)]
151    pub read_commands_from_stdin: bool,
152
153    /// Run in `sh` compatibility mode, as if run as `/bin/sh`.
154    #[clap(long = "sh")]
155    pub sh_mode: bool,
156
157    /// Run only one command and then exit.
158    #[clap(short = 't', help_heading = HEADING_STANDARD_OPTIONS)]
159    pub exit_after_one_command: bool,
160
161    /// Treat expansion of an unset variable as an error.
162    #[clap(short = 'u', help_heading = HEADING_STANDARD_OPTIONS)]
163    pub treat_unset_variables_as_error: bool,
164
165    /// Print input when it's processed.
166    #[clap(short = 'v', long = "verbose", help_heading = HEADING_STANDARD_OPTIONS)]
167    pub verbose: bool,
168
169    /// Print commands as they execute.
170    #[clap(short = 'x', help_heading = HEADING_STANDARD_OPTIONS)]
171    pub print_commands_and_arguments: bool,
172
173    /// Enable xtrace and configure for the given output file.
174    #[clap(long = "xtrace-file", value_name = "FILE", help_heading = HEADING_UI_OPTIONS)]
175    pub xtrace_file_path: Option<PathBuf>,
176
177    /// Disable bracketed paste.
178    #[clap(long = "disable-bracketed-paste", help_heading = HEADING_UI_OPTIONS)]
179    pub disable_bracketed_paste: bool,
180
181    /// Disable colorized output.
182    #[clap(long = "disable-color", help_heading = HEADING_UI_OPTIONS)]
183    pub disable_color: bool,
184
185    /// Enable syntax highlighting in input.
186    #[clap(long = "enable-highlighting", help_heading = HEADING_UI_OPTIONS, default_value_t = crate::entry::DEFAULT_ENABLE_HIGHLIGHTING)]
187    pub enable_highlighting: bool,
188
189    /// Enable experimental parser (not ready for use).
190    #[cfg(feature = "experimental-parser")]
191    #[clap(long = "experimental-parser", help_heading = HEADING_EXPERIMENTAL_OPTIONS)]
192    pub experimental_parser: bool,
193
194    /// Enable terminal integration (**experimental**).
195    #[clap(long = "enable-terminal-integration", help_heading = HEADING_EXPERIMENTAL_OPTIONS)]
196    pub terminal_shell_integration: bool,
197
198    /// Enable zsh-style preexec/precmd hooks (**experimental**).
199    #[clap(long = "enable-zsh-hooks", help_heading = HEADING_EXPERIMENTAL_OPTIONS)]
200    pub zsh_style_hooks: bool,
201
202    /// Input backend.
203    #[clap(long = "input-backend", value_name = "BACKEND", help_heading = HEADING_UI_OPTIONS)]
204    pub input_backend: Option<InputBackendType>,
205
206    /// Load state from the given file; the saved state should be in JSON format
207    /// and overrides any non-UI command-line options provided.
208    #[cfg(feature = "experimental-load")]
209    #[clap(long = "load", value_name = "FILE", help_heading = HEADING_EXPERIMENTAL_OPTIONS)]
210    pub load_file: Option<PathBuf>,
211
212    /// Enable debug logging for classes of tracing events.
213    #[clap(long = "debug", alias = "log-enable", value_name = "EVENT", help_heading = HEADING_UI_OPTIONS)]
214    pub enabled_debug_events: Vec<events::TraceEvent>,
215
216    /// Disable logging for classes of tracing events (takes same event types as `--debug`).
217    #[clap(
218        long = "disable-event",
219        alias = "log-disable",
220        value_name = "EVENT",
221        hide_possible_values = true,
222        help_heading = HEADING_UI_OPTIONS
223    )]
224    pub disabled_events: Vec<events::TraceEvent>,
225
226    /// Path and arguments for script to execute (optional).
227    #[clap(
228        trailing_var_arg = true,
229        allow_hyphen_values = false,
230        value_name = "SCRIPT_PATH [SCRIPT_ARGS]..."
231    )]
232    pub script_args: Vec<String>,
233}
234
235impl CommandLineArgs {
236    /// Returns a `CommandLineArgs` with all clap-defined default values.
237    ///
238    /// This is useful for detecting which CLI arguments were explicitly provided
239    /// vs. which retained their default values (e.g., for config file merging).
240    #[must_use]
241    #[allow(
242        clippy::missing_panics_doc,
243        reason = "parsing defaults should not panic"
244    )]
245    pub fn default_values() -> Self {
246        use clap::Parser;
247        // Parse with just the program name to get all defaults.
248        // This won't fail because all arguments have defaults or are optional.
249        #[allow(clippy::expect_used)]
250        Self::try_parse_from(["brush"]).expect("parsing defaults should never fail")
251    }
252
253    /// Returns whether or not the arguments indicate that the shell should run in interactive mode.
254    pub fn is_interactive(&self) -> bool {
255        // If -i is provided, then that overrides any further consideration; it forces
256        // interactive mode.
257        if self.interactive {
258            return true;
259        }
260
261        // If -c or non-option arguments are provided, then we're not in interactive mode.
262        if self.command.is_some() || !self.script_args.is_empty() {
263            return false;
264        }
265
266        // If *either* stdin or stderr is not a terminal, then we're not in interactive mode.
267        if !std::io::stdin().is_terminal() || !std::io::stderr().is_terminal() {
268            return false;
269        }
270
271        // In all other cases, we assume interactive mode.
272        true
273    }
274}
275
276/// Returns clap styling to be used for command-line help.
277#[doc(hidden)]
278fn brush_help_styles() -> clap::builder::Styles {
279    styling::Styles::styled()
280        .header(
281            styling::AnsiColor::Yellow.on_default()
282                | styling::Effects::BOLD
283                | styling::Effects::UNDERLINE,
284        )
285        .usage(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD)
286        .literal(styling::AnsiColor::Magenta.on_default() | styling::Effects::BOLD)
287        .placeholder(styling::AnsiColor::Cyan.on_default())
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn test_default_values() {
296        let args = CommandLineArgs::default_values();
297        // Verify some basic defaults
298        assert!(!args.interactive);
299        assert!(!args.login);
300        assert!(args.command.is_none());
301        assert!(args.script_args.is_empty());
302    }
303}