There are two separate questions here.
- "I do not want it to ask for user input in case there is no redirection." - as stated by @trentcl, the best answer is atty
- "Is there a way to check if the stdin buffer is empty?"
This answer is for the second question. I also wrote it as a self-contained fiddle with test cases: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=a265b9c4e76d6a9e53d8cc3e5334e42c.
The Rust standard library doesn't provide a way. I've gone to the underlying OS file descriptor so I can switch it between blocking and non-blocking mode. So: start with an FD, and we'll construct a normal BufReader
around it:
struct NonblockingBufReader {
buffered: BufReader<RawFd2>,
}
struct RawFd2 {
fd: RawFd,
}
impl NonblockingBufReader {
/// Takes ownership of the underlying FD
fn new<R: IntoRawFd>(underlying: R) -> NonblockingBufReader {
let buffered = BufReader::new(RawFd2 {
fd: underlying.into_raw_fd(),
});
return NonblockingBufReader { buffered };
}
}
// Here's a private implementation of 'Read' for raw file descriptors,
// for use in BufReader...
impl std::io::Read for RawFd2 {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
assert!(buf.len() <= isize::max_value() as usize);
match unsafe { libc::read(self.fd, buf.as_mut_ptr() as _, buf.len()) } {
x if x < 0 => Err(std::io::Error::last_os_error()),
x => Ok(x as usize),
}
}
}
But because we know our BufReader wraps an FD (rather than just some arbitrary Reader) we can get to that FD to switch it to non-blocking: (this method goes inside impl NonblockingBufReader
)
/// Does BufReader::read_line but only if there's already at
/// least one byte available on the FD. In case of EOF, returns
/// an empty string.
/// Possible outcomes: (0) no-data-yet, (1) data, (2) EOF, (3) Error
fn read_line_only_if_data(&mut self) -> std::io::Result<Option<String>>
{
let r = unsafe {
// The reason this is safe is we know 'inner' wraps a valid FD,
// and we're not doing any reads on it such as would disturb BufReader.
let fd = self.buffered.get_ref().fd;
let flags = libc::fcntl(fd, libc::F_GETFL);
libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
let r = self.buffered.fill_buf();
libc::fcntl(fd, libc::F_SETFL, flags);
r
};
// Behavior of fill_buf is "Returns the contents of the internal buffer,
// filling it with more data from the inner reader if it is empty."
// If there were no bytes available, then (1) the internal buffer is
// empty, (2) it'll call inner.read(), (3) that call will error WouldBlock.
match r {
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(None), // (0) no-data-yet
Ok(buf) if !buf.is_empty() => {
let mut line = String::new();
self.buffered.read_line(&mut line)?;
Ok(Some(line)) // (1) data, or further error
},
Ok(_) => Ok(Some(String::new())), // (2) EOF
Err(e) => Err(e), // (3) Error
}
}
For completeness, here's the normal read_line:
/// Wraps BufReader::read_line.
/// Possible outcomes: (1) data, (2) EOF, (3) Error
fn read_line(&mut self) -> std::io::Result<String> {
let mut line = String::new();
match self.buffered.read_line(&mut line) {
Ok(_) => Ok(line), // EOF/data
Err(e) => Err(e), // Error
}
}
isatty
instead (which is also an OS specific feature). – Geosyncline