Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/uu/expand/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ fluent = { workspace = true }
thiserror = { workspace = true }
uucore = { workspace = true }

[target.'cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd"))'.dependencies]
[target.'cfg(unix)'.dependencies]
rustix = { workspace = true, features = ["fs"] }

[dev-dependencies]
Expand Down
22 changes: 22 additions & 0 deletions src/uu/expand/src/expand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -439,12 +439,29 @@ fn expand_buf(
Ok(())
}

/// Whether stdin is a regular file, i.e. reads from it never wait for a writer.
fn stdin_is_regular_file() -> bool {
#[cfg(unix)]
{
rustix::fs::fstat(stdin())
.is_ok_and(|st| rustix::fs::FileType::from_raw_mode(st.st_mode).is_file())
}
#[cfg(not(unix))]
{
false
}
}

fn expand_file(
file: &OsString,
output: &mut BufWriter<std::io::Stdout>,
options: &Options,
) -> UResult<()> {
let mut input = open(file)?;
// Only input that can block between lines (a pipe or terminal on stdin)
// needs output flushed before the next read. Regular files keep the fully
// buffered path.
let flush_lines = file == "-" && !stdin_is_regular_file();
let ts = options.tabstops.as_ref();
let mut col = 0;
loop {
Expand All @@ -455,6 +472,11 @@ fn expand_file(
.map_err_context(|| translate!("expand-error-failed-to-write-output"))?;
let len = buf.len();
input.consume(len);
if flush_lines {
output
.flush()
.map_err_context(|| translate!("expand-error-failed-to-write-output"))?;
}
}
Err(e) => return Err(e.map_err_context(|| file.maybe_quote().to_string())),
}
Expand Down
2 changes: 1 addition & 1 deletion src/uu/unexpand/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ fluent = { workspace = true }
thiserror = { workspace = true }
uucore = { workspace = true }

[target.'cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd"))'.dependencies]
[target.'cfg(unix)'.dependencies]
rustix = { workspace = true, features = ["fs"] }

[dev-dependencies]
Expand Down
18 changes: 18 additions & 0 deletions src/uu/unexpand/src/unexpand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,19 @@ fn unexpand_buf(
Ok(())
}

/// Whether stdin is a regular file, i.e. reads from it never wait for a writer.
fn stdin_is_regular_file() -> bool {
#[cfg(unix)]
{
rustix::fs::fstat(stdin())
.is_ok_and(|st| rustix::fs::FileType::from_raw_mode(st.st_mode).is_file())
}
#[cfg(not(unix))]
{
false
}
}

fn unexpand_file(
file: &OsString,
output: &mut BufWriter<Stdout>,
Expand All @@ -645,6 +658,8 @@ fn unexpand_file(
tab_config: &TabConfig,
) -> UResult<()> {
let mut input = open(file)?;
// See expand: flush before the next read only when stdin can block.
let flush_lines = file == "-" && !stdin_is_regular_file();
let mut print_state = PrintState {
col: 0,
scol: 0,
Expand All @@ -665,6 +680,9 @@ fn unexpand_file(
}
let len = buf.len();
input.consume(len);
if flush_lines {
output.flush()?;
}
}
// write out anything remaining
write_tabs(output, tab_config, &mut print_state, options.aflag)
Expand Down
22 changes: 22 additions & 0 deletions tests/by-util/test_expand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

// spell-checker:ignore (ToDO) taaaa tbbbb tcccc

#[cfg(unix)]
use std::process::Stdio;
use uucore::display::Quotable;
use uutests::new_ucmd;

Expand Down Expand Up @@ -496,3 +498,23 @@ fn test_buffered_reads_new_line_no_tabs_in_first_chunk() {
.succeeds()
.stdout_is_fixture("new_line_in_chunk_expected.txt");
}

/// Interactive / line-at-a-time stdin must show each expanded line before the
/// next input arrives. BufWriter alone only flushes on exit.
#[cfg(unix)]
#[test]
fn test_stdin_tty_flushes_each_line() {
let mut child = new_ucmd!()
.args(&["-t3"])
.set_stdin(Stdio::piped())
.set_stdout(Stdio::piped())
.run_no_wait();

child.write_in(b"\ta\n");
assert_eq!(&child.stdout_exact_bytes(5), b" a\n");

child.write_in(b"b\tc\n");
assert_eq!(&child.stdout_exact_bytes(5), b"b c\n");

child.wait().unwrap().success();
}
20 changes: 20 additions & 0 deletions tests/by-util/test_unexpand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

// spell-checker:ignore contenta edgecase behaviour tcaf

#[cfg(unix)]
use std::process::Stdio;
use uutests::{at_and_ucmd, new_ucmd};

#[test]
Expand Down Expand Up @@ -544,3 +546,21 @@ fn test_buffered_read_edgecase_behaviour() {
.stdout_only(String::from_utf8(expected).unwrap());
}
}

/// Interactive / line-at-a-time stdin must show each unexpanded line before the
/// next input arrives.
#[cfg(unix)]
#[test]
fn test_stdin_tty_flushes_each_line() {
let mut child = new_ucmd!()
.args(&["-t3"])
.set_stdin(Stdio::piped())
.set_stdout(Stdio::piped())
.run_no_wait();

// three leading spaces -> one tab at tabstop 3
child.write_in(b" a\n");
assert_eq!(&child.stdout_exact_bytes(3), b"\ta\n");

child.wait().unwrap().success();
}
Loading