mirror of
https://github.com/sharkdp/bat
synced 2026-07-30 18:11:44 +00:00
feat: implement --unbuffered mode for streaming input (#3555)
Repurpose the existing --unbuffered/-u flag (previously a POSIX no-op) to enable unbuffered input reading using fill_buf()/consume() instead of read_until(b'\n'). This allows partial lines to display immediately when piping streaming input like `tail -f` into bat. - Add unbuffered field to Config and InputReader - Add read_line_unbuffered() using BufRead::fill_buf()/consume() - Add flush() to OutputHandle, called after each line in unbuffered mode - Auto-disable line numbers in unbuffered mode to avoid partial line confusion - Update help text, man page, and shell completions - Add unit tests and integration tests
This commit is contained in:
@@ -462,6 +462,7 @@ impl App {
|
||||
_ => unreachable!("other values for --strip-ansi are not allowed"),
|
||||
},
|
||||
quiet_empty: self.matches.get_flag("quiet-empty"),
|
||||
unbuffered: self.matches.get_flag("unbuffered"),
|
||||
theme: theme(self.theme_options()).to_string(),
|
||||
visible_lines: match self.matches.try_contains_id("diff").unwrap_or_default()
|
||||
&& self.matches.get_flag("diff")
|
||||
@@ -619,6 +620,11 @@ impl App {
|
||||
bat_warning!("Style 'rule' is a subset of style 'grid', 'rule' will not be visible.");
|
||||
}
|
||||
|
||||
// Auto-disable line numbers in unbuffered mode to avoid confusion with partial lines
|
||||
if self.matches.get_flag("unbuffered") {
|
||||
styled_components.0.remove(&StyleComponent::LineNumbers);
|
||||
}
|
||||
|
||||
Ok(styled_components)
|
||||
}
|
||||
|
||||
|
||||
@@ -548,11 +548,14 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||
.short('u')
|
||||
.long("unbuffered")
|
||||
.action(ArgAction::SetTrue)
|
||||
.hide_short_help(true)
|
||||
.help("Enable unbuffered input reading for streaming use cases.")
|
||||
.long_help(
|
||||
"This option exists for POSIX-compliance reasons ('u' is for \
|
||||
'unbuffered'). The output is always unbuffered - this option \
|
||||
is simply ignored.",
|
||||
"Enable unbuffered input reading. When this flag is set, bat will \
|
||||
display data as soon as it is available, without waiting for a \
|
||||
complete line. This is useful for streaming use cases like \
|
||||
'tail -f logfile | bat -u --paging=never'. Note that line numbers \
|
||||
are automatically disabled in unbuffered mode, and syntax \
|
||||
highlighting may be imperfect on partial lines.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
|
||||
@@ -110,6 +110,9 @@ pub struct Config<'a> {
|
||||
|
||||
/// Whether or not to produce no output when input is empty
|
||||
pub quiet_empty: bool,
|
||||
|
||||
/// Whether or not to use unbuffered input reading for streaming use cases
|
||||
pub unbuffered: bool,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "minimal-application", feature = "paging"))]
|
||||
|
||||
@@ -158,6 +158,7 @@ impl Controller<'_> {
|
||||
#[cfg(not(feature = "lessopen"))]
|
||||
input.open(stdin, stdout_identifier)?
|
||||
};
|
||||
opened_input.reader.unbuffered = self.config.unbuffered;
|
||||
#[cfg(feature = "git")]
|
||||
let line_changes = if self.config.visible_lines.diff_mode()
|
||||
|| (!self.config.loop_through && self.config.style_components.changes())
|
||||
@@ -327,6 +328,9 @@ impl Controller<'_> {
|
||||
}
|
||||
|
||||
printer.print_line(false, writer, line_nr, &line, max_buffered_line_number)?;
|
||||
if self.config.unbuffered {
|
||||
writer.flush()?;
|
||||
}
|
||||
}
|
||||
RangeCheckResult::AfterLastRange => {
|
||||
break;
|
||||
|
||||
+105
@@ -253,6 +253,7 @@ pub(crate) struct InputReader<'a> {
|
||||
inner: Box<dyn BufRead + 'a>,
|
||||
pub(crate) first_line: Vec<u8>,
|
||||
pub(crate) content_type: Option<ContentType>,
|
||||
pub(crate) unbuffered: bool,
|
||||
}
|
||||
|
||||
impl<'a> InputReader<'a> {
|
||||
@@ -276,6 +277,7 @@ impl<'a> InputReader<'a> {
|
||||
inner: Box::new(reader),
|
||||
first_line,
|
||||
content_type,
|
||||
unbuffered: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,9 +294,29 @@ impl<'a> InputReader<'a> {
|
||||
return read_utf16_line(&mut self.inner, buf, 0x0A, 0x00);
|
||||
}
|
||||
|
||||
if self.unbuffered {
|
||||
return self.read_line_unbuffered(buf);
|
||||
}
|
||||
|
||||
let res = self.inner.read_until(b'\n', buf).map(|size| size > 0)?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn read_line_unbuffered(&mut self, buf: &mut Vec<u8>) -> io::Result<bool> {
|
||||
let available = self.inner.fill_buf()?;
|
||||
if available.is_empty() {
|
||||
return Ok(!buf.is_empty());
|
||||
}
|
||||
if let Some(pos) = available.iter().position(|&b| b == b'\n') {
|
||||
buf.extend_from_slice(&available[..=pos]);
|
||||
self.inner.consume(pos + 1);
|
||||
} else {
|
||||
let len = available.len();
|
||||
buf.extend_from_slice(available);
|
||||
self.inner.consume(len);
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
fn read_utf16_line<R: BufRead>(
|
||||
@@ -381,6 +403,89 @@ fn utf16le() {
|
||||
assert!(buffer.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbuffered_returns_partial_data() {
|
||||
use std::io::Cursor;
|
||||
|
||||
let content = b"first line\npartial";
|
||||
let mut reader = InputReader::new(Cursor::new(&content[..]));
|
||||
reader.unbuffered = true;
|
||||
|
||||
// First call returns first_line (buffered during new())
|
||||
let mut buffer = vec![];
|
||||
let res = reader.read_line(&mut buffer);
|
||||
assert!(res.is_ok());
|
||||
assert!(res.unwrap());
|
||||
assert_eq!(b"first line\n", &buffer[..]);
|
||||
|
||||
// Subsequent calls use unbuffered reading
|
||||
buffer.clear();
|
||||
let res = reader.read_line(&mut buffer);
|
||||
assert!(res.is_ok());
|
||||
assert!(res.unwrap());
|
||||
assert_eq!(b"partial", &buffer[..]);
|
||||
|
||||
// EOF
|
||||
buffer.clear();
|
||||
let res = reader.read_line(&mut buffer);
|
||||
assert!(res.is_ok());
|
||||
assert!(!res.unwrap());
|
||||
assert!(buffer.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbuffered_returns_complete_lines() {
|
||||
use std::io::Cursor;
|
||||
|
||||
let content = b"line1\nline2\n";
|
||||
let mut reader = InputReader::new(Cursor::new(&content[..]));
|
||||
reader.unbuffered = true;
|
||||
|
||||
// First call returns first_line
|
||||
let mut buffer = vec![];
|
||||
let res = reader.read_line(&mut buffer);
|
||||
assert!(res.is_ok());
|
||||
assert!(res.unwrap());
|
||||
assert_eq!(b"line1\n", &buffer[..]);
|
||||
|
||||
// Second call returns line2 (complete line with newline)
|
||||
buffer.clear();
|
||||
let res = reader.read_line(&mut buffer);
|
||||
assert!(res.is_ok());
|
||||
assert!(res.unwrap());
|
||||
assert_eq!(b"line2\n", &buffer[..]);
|
||||
|
||||
// EOF
|
||||
buffer.clear();
|
||||
let res = reader.read_line(&mut buffer);
|
||||
assert!(res.is_ok());
|
||||
assert!(!res.unwrap());
|
||||
assert!(buffer.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbuffered_eof_handling() {
|
||||
use std::io::Cursor;
|
||||
|
||||
let content = b"only line\n";
|
||||
let mut reader = InputReader::new(Cursor::new(&content[..]));
|
||||
reader.unbuffered = true;
|
||||
|
||||
// First call returns first_line
|
||||
let mut buffer = vec![];
|
||||
let res = reader.read_line(&mut buffer);
|
||||
assert!(res.is_ok());
|
||||
assert!(res.unwrap());
|
||||
assert_eq!(b"only line\n", &buffer[..]);
|
||||
|
||||
// EOF - empty buffer returns false
|
||||
buffer.clear();
|
||||
let res = reader.read_line(&mut buffer);
|
||||
assert!(res.is_ok());
|
||||
assert!(!res.unwrap());
|
||||
assert!(buffer.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf16le_issue3367() {
|
||||
let content = b"\xFF\xFE\x0A\x4E\x00\x4E\x0A\x4F\x00\x52\x0A\x00\
|
||||
|
||||
@@ -236,4 +236,11 @@ impl OutputHandle<'_> {
|
||||
Self::FmtWrite(handle) => handle.write_fmt(args).map_err(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn flush(&mut self) -> Result<()> {
|
||||
match self {
|
||||
Self::IoWrite(handle) => handle.flush().map_err(Into::into),
|
||||
Self::FmtWrite(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user