diff --git a/src/uu/unexpand/src/unexpand.rs b/src/uu/unexpand/src/unexpand.rs index 1353585a584..a5424bd843d 100644 --- a/src/uu/unexpand/src/unexpand.rs +++ b/src/uu/unexpand/src/unexpand.rs @@ -489,6 +489,16 @@ struct PrintState { } impl PrintState { + fn new() -> Self { + Self { + col: 0, + scol: 0, + leading: true, + pctype: CharType::Other, + pending_wide: Vec::new(), + } + } + // reinitializes the PrintState struct to beginning of line values fn new_line(&mut self) { self.col = 0; @@ -643,22 +653,41 @@ fn unexpand_file( options: &Options, lastcol: usize, tab_config: &TabConfig, + line_state: &mut PrintState, ) -> UResult<()> { let mut input = open(file)?; - let mut print_state = PrintState { - col: 0, - scol: 0, - leading: true, - pctype: CharType::Other, - pending_wide: Vec::new(), - }; + // Work on a local copy of the line state, so it can stay in registers + // while unexpanding, and hand it back when done with this file. + let mut print_state = std::mem::replace(line_state, PrintState::new()); + let result = unexpand_input( + &mut input, + file, + output, + options, + lastcol, + tab_config, + &mut print_state, + ); + *line_state = print_state; + result +} +/// Unexpands everything `input` yields, carrying the line state in `print_state`. +fn unexpand_input( + input: &mut impl BufRead, + file: &OsString, + output: &mut BufWriter, + options: &Options, + lastcol: usize, + tab_config: &TabConfig, + print_state: &mut PrintState, +) -> UResult<()> { while let buf @ [_, ..] = input .fill_buf() .map_err(|e| e.map_err_context(|| file.maybe_quote().to_string()) as Box)? { for line in buf.split_inclusive(|b| *b == b'\n') { - unexpand_buf(line, output, options, lastcol, tab_config, &mut print_state)?; + unexpand_buf(line, output, options, lastcol, tab_config, print_state)?; if let Some(b'\n') = line.last() { print_state.new_line(); } @@ -666,8 +695,7 @@ fn unexpand_file( let len = buf.len(); input.consume(len); } - // write out anything remaining - write_tabs(output, tab_config, &mut print_state, options.aflag) + Ok(()) } fn unexpand(options: &Options) -> UResult<()> { @@ -682,12 +710,25 @@ fn unexpand(options: &Options) -> UResult<()> { 0 }; + // Input files form a single stream: a line that does not end in a + // newline continues in the next file. + let mut print_state = PrintState::new(); + for file in &options.files { - if let Err(e) = unexpand_file(file, &mut output, options, lastcol, tab_config) { + if let Err(e) = unexpand_file( + file, + &mut output, + options, + lastcol, + tab_config, + &mut print_state, + ) { show!(e); set_exit_code(1); } } + // write out anything remaining + write_tabs(&mut output, tab_config, &mut print_state, options.aflag)?; output.flush()?; Ok(()) } diff --git a/tests/by-util/test_unexpand.rs b/tests/by-util/test_unexpand.rs index 49a28723746..b3bd705cf5d 100644 --- a/tests/by-util/test_unexpand.rs +++ b/tests/by-util/test_unexpand.rs @@ -450,3 +450,41 @@ fn test_buffered_read_edgecase_behaviour() { .stdout_only(String::from_utf8(expected).unwrap()); } } + +#[test] +fn test_line_continues_into_next_file() { + // Neither first file ends in a newline, so its blanks and text continue + // the line in the next file. + let (at, mut ucmd) = at_and_ucmd!(); + at.write("a", " "); + at.write("b", " x\n"); + ucmd.args(&["a", "b"]).succeeds().stdout_is("\tx\n"); + + let (at, mut ucmd) = at_and_ucmd!(); + at.write("c", "abcdef"); + at.write("d", " x\n"); + ucmd.args(&["-a", "c", "d"]) + .succeeds() + .stdout_is("abcdef\tx\n"); +} + +#[test] +fn test_trailing_blanks_of_last_file_are_kept() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("a", "x\n"); + at.write("b", " "); + ucmd.args(&["a", "b"]).succeeds().stdout_is("x\n "); +} + +#[test] +fn test_line_continues_past_unreadable_file() { + // A file that cannot be read does not end the line started before it. + let (at, mut ucmd) = at_and_ucmd!(); + at.write("a", " "); + at.mkdir("dir"); + at.write("b", " x\n"); + ucmd.args(&["a", "dir", "b"]) + .fails() + .stdout_is("\tx\n") + .stderr_contains("unexpand: dir: Is a directory"); +}