diff --git a/brush-core/src/interp.rs b/brush-core/src/interp.rs index a89acd9b..5be6a3e2 100644 --- a/brush-core/src/interp.rs +++ b/brush-core/src/interp.rs @@ -1316,16 +1316,39 @@ pub(crate) async fn setup_redirect( ast::IoFileRedirectTarget::Filename(f) => { let mut options = std::fs::File::options(); + let mut expanded_fields = + expansion::full_expand_and_split_word(shell, f).await?; + + if expanded_fields.len() != 1 { + return Err(error::Error::InvalidRedirection); + } + + let expanded_file_path: PathBuf = + shell.get_absolute_path(Path::new(expanded_fields.remove(0).as_str())); + let default_fd_if_unspecified = get_default_fd_for_redirect_kind(kind); match kind { ast::IoFileRedirectKind::Read => { options.read(true); } ast::IoFileRedirectKind::Write => { - // TODO: honor noclobber options - options.create(true); - options.write(true); - options.truncate(true); + if shell + .options + .disallow_overwriting_regular_files_via_output_redirection + { + // First check to see if the path points to an existing regular + // file. + if !expanded_file_path.is_file() { + options.create(true); + } else { + options.create_new(true); + } + options.write(true); + } else { + options.create(true); + options.write(true); + options.truncate(true); + } } ast::IoFileRedirectKind::Append => { options.create(true); @@ -1352,16 +1375,6 @@ pub(crate) async fn setup_redirect( fd_num = specified_fd_num.unwrap_or(default_fd_if_unspecified); - let mut expanded_fields = - expansion::full_expand_and_split_word(shell, f).await?; - - if expanded_fields.len() != 1 { - return Err(error::Error::InvalidRedirection); - } - - let expanded_file_path: PathBuf = - shell.get_absolute_path(Path::new(expanded_fields.remove(0).as_str())); - let opened_file = options.open(expanded_file_path.as_path()).map_err(|err| { error::Error::RedirectionFailure( diff --git a/brush-core/src/options.rs b/brush-core/src/options.rs index 6e9c32da..cdbe3242 100644 --- a/brush-core/src/options.rs +++ b/brush-core/src/options.rs @@ -191,6 +191,8 @@ impl RuntimeOptions { // There's a set of options enabled by default for all shells. let mut options = Self { interactive: create_options.interactive, + disallow_overwriting_regular_files_via_output_redirection: create_options + .disallow_overwriting_regular_files_via_output_redirection, do_not_execute_commands: create_options.do_not_execute_commands, enable_command_history: create_options.interactive, enable_job_control: create_options.interactive, diff --git a/brush-core/src/shell.rs b/brush-core/src/shell.rs index 19ea0201..3028029f 100644 --- a/brush-core/src/shell.rs +++ b/brush-core/src/shell.rs @@ -123,6 +123,8 @@ pub struct CreateOptions { pub disabled_shopt_options: Vec, /// Enabled shopt options. pub enabled_shopt_options: Vec, + /// Disallow overwriting regular files via output redirection. + pub disallow_overwriting_regular_files_via_output_redirection: bool, /// Do not execute commands. pub do_not_execute_commands: bool, /// Whether the shell is interactive. diff --git a/brush-shell/src/args.rs b/brush-shell/src/args.rs index 9f215a0a..c76b50dd 100644 --- a/brush-shell/src/args.rs +++ b/brush-shell/src/args.rs @@ -45,6 +45,10 @@ pub struct CommandLineArgs { #[clap(long = "version", action = clap::ArgAction::Version)] pub version: Option, + /// Enable noclobber shell option. + #[arg(short = 'C')] + pub disallow_overwriting_regular_files_via_output_redirection: bool, + /// Execute the provided command and then exit. #[arg(short = 'c', value_name = "COMMAND")] pub command: Option, diff --git a/brush-shell/src/main.rs b/brush-shell/src/main.rs index ed533fcd..312095cf 100644 --- a/brush-shell/src/main.rs +++ b/brush-shell/src/main.rs @@ -199,6 +199,8 @@ async fn instantiate_shell( let options = brush_interactive::Options { shell: brush_core::CreateOptions { disabled_shopt_options: args.disabled_shopt_options.clone(), + disallow_overwriting_regular_files_via_output_redirection: args + .disallow_overwriting_regular_files_via_output_redirection, enabled_shopt_options: args.enabled_shopt_options.clone(), do_not_execute_commands: args.do_not_execute_commands, login: args.login || argv0.as_ref().map_or(false, |a0| a0.starts_with('-')), diff --git a/brush-shell/tests/cases/options.yaml b/brush-shell/tests/cases/options.yaml index 86129c7b..e4cb8d01 100644 --- a/brush-shell/tests/cases/options.yaml +++ b/brush-shell/tests/cases/options.yaml @@ -14,6 +14,32 @@ cases: env | grep newvar env | grep unexported + - name: "set -C" + ignore_stderr: true + stdin: | + touch existing-file + + set -C + + echo hi > non-existing-file + echo "Result (non existing): $?" + echo "File contents: $(cat non-existing-file)" + echo + + echo hi > /dev/null + echo "Result (device file): $?" + echo + + echo hi > existing-file + echo "Result (existing file): $?" + echo "File contents: $(cat existing-file)" + echo + + echo hi >| existing-file + echo "Result (clobber): $?" + echo "File contents: $(cat existing-file)" + echo + - name: "set -x" stdin: | set -x