diff --git a/brush-core/src/shell/fs.rs b/brush-core/src/shell/fs.rs index 3ea8cc6e..e2a7fd66 100644 --- a/brush-core/src/shell/fs.rs +++ b/brush-core/src/shell/fs.rs @@ -185,6 +185,14 @@ impl crate::Shell { path: impl AsRef, params: &ExecutionParameters, ) -> Result { + // Give platform-specific code a chance to handle special files + // (e.g. /dev/null on Windows, which needs to open NUL instead). + // This is checked before absolute_path so that paths like /dev/null + // are intercepted on platforms where they aren't valid native paths. + if let Some(result) = crate::sys::fs::try_open_special_file(path.as_ref()) { + return result.map(openfiles::OpenFile::from); + } + let path_to_open = self.absolute_path(path.as_ref()); // See if this is a reference to a file descriptor, in which case the actual diff --git a/brush-core/src/sys/stubs/fs.rs b/brush-core/src/sys/stubs/fs.rs index 6be1898f..1393ebaa 100644 --- a/brush-core/src/sys/stubs/fs.rs +++ b/brush-core/src/sys/stubs/fs.rs @@ -76,3 +76,12 @@ pub fn get_default_standard_utils_paths() -> Vec { pub fn open_null_file() -> Result { Err(error::ErrorKind::NotSupportedOnThisPlatform("opening null file").into()) } + +/// Gives the platform an opportunity to handle a special file path (e.g. `/dev/null`). +// +// This is a stub implementation that returns no result. +pub fn try_open_special_file( + _path: &std::path::Path, +) -> Option> { + None +} diff --git a/brush-core/src/sys/unix/fs.rs b/brush-core/src/sys/unix/fs.rs index 3aaf98ec..d6ca7294 100644 --- a/brush-core/src/sys/unix/fs.rs +++ b/brush-core/src/sys/unix/fs.rs @@ -191,3 +191,8 @@ pub fn open_null_file() -> Result { Ok(f) } + +/// Gives the platform an opportunity to handle a special file path (e.g. `/dev/null`). +pub const fn try_open_special_file(_path: &Path) -> Option> { + None +} diff --git a/brush-core/src/sys/windows/fs.rs b/brush-core/src/sys/windows/fs.rs index 0aa75530..967b2aaf 100644 --- a/brush-core/src/sys/windows/fs.rs +++ b/brush-core/src/sys/windows/fs.rs @@ -2,9 +2,31 @@ pub use crate::sys::stubs::fs::*; +use std::path::Path; + +use crate::error; + /// Splits a platform-specific PATH-like value into individual paths. /// /// On Windows, this delegates to [`std::env::split_paths`]. pub fn split_paths + ?Sized>(s: &T) -> std::env::SplitPaths<'_> { std::env::split_paths(s) } + +/// Opens a null file that will discard all I/O. +pub fn open_null_file() -> Result { + let f = std::fs::File::options() + .read(true) + .write(true) + .open("NUL")?; + Ok(f) +} + +/// Gives the platform an opportunity to handle a special file path (e.g. `/dev/null`). +pub fn try_open_special_file(path: &Path) -> Option> { + if path == Path::new("/dev/null") { + Some(open_null_file().map_err(std::io::Error::other)) + } else { + None + } +} diff --git a/brush-shell/tests/cases/compat/redirection.yaml b/brush-shell/tests/cases/compat/redirection.yaml index d0e12bb8..344b1f43 100644 --- a/brush-shell/tests/cases/compat/redirection.yaml +++ b/brush-shell/tests/cases/compat/redirection.yaml @@ -107,6 +107,32 @@ cases: echo "Done." echo "${var}" + - name: "Input redirection from /dev/null" + stdin: | + cat < /dev/null + echo "exit: $?" + + - name: "Redirect stderr to /dev/null" + stdin: | + ls /non-existent-path 2>/dev/null + echo "exit: $?" + + - name: "Append redirection to /dev/null" + stdin: | + echo hi >>/dev/null + echo "exit: $?" + + - name: "Redirect to /dev/null via variable" + stdin: | + target=/dev/null + echo hi >"$target" + echo "exit: $?" + + - name: "Read and write /dev/null" + stdin: | + cat <>/dev/null + echo "exit: $?" + - name: "Redirect stdout and stderr" stdin: | ls -d . non-existent-dir &>/dev/null