1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 03:27:44 +00:00

join: support headers

This commit is contained in:
Konstantin Pospelov 2018-04-11 22:55:44 +03:00
parent fc7b1fcaba
commit 7dc8ff62cc
6 changed files with 104 additions and 11 deletions

View file

@ -53,6 +53,7 @@ struct Settings {
format: Vec<Spec>, format: Vec<Spec>,
empty: String, empty: String,
check_order: CheckOrder, check_order: CheckOrder,
headers: bool,
} }
impl Default for Settings { impl Default for Settings {
@ -67,6 +68,7 @@ impl Default for Settings {
format: vec![], format: vec![],
empty: String::new(), empty: String::new(),
check_order: CheckOrder::Default, check_order: CheckOrder::Default,
headers: false,
} }
} }
} }
@ -256,13 +258,10 @@ impl<'a> State<'a> {
/// Skip the current unpaired line. /// Skip the current unpaired line.
fn skip_line(&mut self, input: &Input, repr: &Repr) { fn skip_line(&mut self, input: &Input, repr: &Repr) {
if self.print_unpaired { if self.print_unpaired {
self.print_unpaired_line(&self.seq[0], repr); self.print_first_line(repr);
} }
match self.next_line(input) { self.reset_next_line(input);
Some(line) => self.seq[0] = line,
None => self.seq.clear(),
}
} }
/// Keep reading line sequence until the key does not change, return /// Keep reading line sequence until the key does not change, return
@ -285,6 +284,19 @@ impl<'a> State<'a> {
return None; return None;
} }
/// Print lines in the buffers as headers.
fn print_headers(&self, other: &State, repr: &Repr) {
if self.has_line() {
if other.has_line() {
self.combine(other, repr);
} else {
self.print_first_line(repr);
}
} else if other.has_line() {
other.print_first_line(repr);
}
}
/// Combine two line sequences. /// Combine two line sequences.
fn combine(&self, other: &State, repr: &Repr) { fn combine(&self, other: &State, repr: &Repr) {
let key = self.seq[0].get_field(self.key); let key = self.seq[0].get_field(self.key);
@ -326,6 +338,16 @@ impl<'a> State<'a> {
} }
} }
fn reset_read_line(&mut self, input: &Input) {
let line = self.read_line(input.separator);
self.reset(line);
}
fn reset_next_line(&mut self, input: &Input) {
let line = self.next_line(input);
self.reset(line);
}
fn has_line(&self) -> bool { fn has_line(&self) -> bool {
!self.seq.is_empty() !self.seq.is_empty()
} }
@ -342,21 +364,22 @@ impl<'a> State<'a> {
fn finalize(&mut self, input: &Input, repr: &Repr) { fn finalize(&mut self, input: &Input, repr: &Repr) {
if self.has_line() && self.print_unpaired { if self.has_line() && self.print_unpaired {
self.print_unpaired_line(&self.seq[0], repr); self.print_first_line(repr);
while let Some(line) = self.next_line(input) { while let Some(line) = self.next_line(input) {
self.print_unpaired_line(&line, repr); self.print_line(&line, repr);
} }
} }
} }
/// Get the next line without the order check.
fn read_line(&mut self, sep: Sep) -> Option<Line> { fn read_line(&mut self, sep: Sep) -> Option<Line> {
let value = self.lines.next()?; let value = self.lines.next()?;
self.line_num += 1; self.line_num += 1;
Some(Line::new(crash_if_err!(1, value), sep)) Some(Line::new(crash_if_err!(1, value), sep))
} }
/// Prepare the next line. /// Get the next line with the order check.
fn next_line(&mut self, input: &Input) -> Option<Line> { fn next_line(&mut self, input: &Input) -> Option<Line> {
let line = self.read_line(input.separator)?; let line = self.read_line(input.separator)?;
@ -384,7 +407,7 @@ impl<'a> State<'a> {
Some(line) Some(line)
} }
fn print_unpaired_line(&self, line: &Line, repr: &Repr) { fn print_line(&self, line: &Line, repr: &Repr) {
if repr.uses_format() { if repr.uses_format() {
repr.print_format(|spec| match spec { repr.print_format(|spec| match spec {
&Spec::Key => line.get_field(self.key), &Spec::Key => line.get_field(self.key),
@ -401,6 +424,10 @@ impl<'a> State<'a> {
println!(); println!();
} }
fn print_first_line(&self, repr: &Repr) {
self.print_line(&self.seq[0], repr);
}
} }
pub fn uumain(args: Vec<String>) -> i32 { pub fn uumain(args: Vec<String>) -> i32 {
@ -482,6 +509,10 @@ FILENUM is 1 or 2, corresponding to FILE1 or FILE2",
.long("nocheck-order") .long("nocheck-order")
.help("do not check that the input is correctly sorted"), .help("do not check that the input is correctly sorted"),
) )
.arg(Arg::with_name("header").long("header").help(
"treat the first line in each file as field headers, \
print them without trying to pair them",
))
.arg( .arg(
Arg::with_name("file1") Arg::with_name("file1")
.required(true) .required(true)
@ -544,6 +575,10 @@ FILENUM is 1 or 2, corresponding to FILE1 or FILE2",
settings.check_order = CheckOrder::Enabled; settings.check_order = CheckOrder::Enabled;
} }
if matches.is_present("header") {
settings.headers = true;
}
let file1 = matches.value_of("file1").unwrap(); let file1 = matches.value_of("file1").unwrap();
let file2 = matches.value_of("file2").unwrap(); let file2 = matches.value_of("file2").unwrap();
@ -591,6 +626,12 @@ fn exec(file1: &str, file2: &str, settings: &Settings) -> i32 {
state1.initialize(settings.separator, settings.autoformat); state1.initialize(settings.separator, settings.autoformat);
state2.initialize(settings.separator, settings.autoformat); state2.initialize(settings.separator, settings.autoformat);
if settings.headers {
state1.print_headers(&state2, &repr);
state1.reset_read_line(&input);
state2.reset_read_line(&input);
}
while state1.has_line() && state2.has_line() { while state1.has_line() && state2.has_line() {
let diff = state1.compare(&state2, settings.ignore_case); let diff = state1.compare(&state2, settings.ignore_case);
@ -604,9 +645,7 @@ fn exec(file1: &str, file2: &str, settings: &Settings) -> i32 {
Ordering::Equal => { Ordering::Equal => {
let next_line1 = state1.extend(&input); let next_line1 = state1.extend(&input);
let next_line2 = state2.extend(&input); let next_line2 = state2.extend(&input);
state1.combine(&state2, &repr); state1.combine(&state2, &repr);
state1.reset(next_line1); state1.reset(next_line1);
state2.reset(next_line2); state2.reset(next_line2);
} }

4
tests/fixtures/join/header.expected vendored Normal file
View file

@ -0,0 +1,4 @@
id field count
1 a abc 10
2 b abc 25
4 d 17 xyz

6
tests/fixtures/join/header_1.txt vendored Normal file
View file

@ -0,0 +1,6 @@
id field
1 a abc
2 b abc
3 c
4 d
5 c

5
tests/fixtures/join/header_2.txt vendored Normal file
View file

@ -0,0 +1,5 @@
id count
1 10
2 25
4 17 xyz
7 18 xyz

View file

@ -0,0 +1,4 @@
id field count
1 a 10
2 b 25
4 d 17

View file

@ -207,3 +207,38 @@ fn wrong_line_order() {
.arg("fields_4.txt") .arg("fields_4.txt")
.fails().stderr_is("fields_4.txt:5: is not sorted"); .fails().stderr_is("fields_4.txt:5: is not sorted");
} }
#[test]
fn headers() {
new_ucmd!()
.arg("header_1.txt")
.arg("header_2.txt")
.arg("--header")
.succeeds().stdout_only_fixture("header.expected");
}
#[test]
fn headers_autoformat() {
new_ucmd!()
.arg("header_1.txt")
.arg("header_2.txt")
.arg("--header")
.arg("-o")
.arg("auto")
.succeeds().stdout_only_fixture("header_autoformat.expected");
}
#[test]
fn single_file_with_header() {
new_ucmd!()
.arg("capitalized.txt")
.arg("empty.txt")
.arg("--header")
.succeeds().stdout_is("A 1");
new_ucmd!()
.arg("empty.txt")
.arg("capitalized.txt")
.arg("--header")
.succeeds().stdout_is("A 1");
}