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

Merge pull request #1134 from kupospelov/master

join: support custom empty filler
This commit is contained in:
Alex Lyon 2018-01-17 15:17:09 -08:00 committed by GitHub
commit 1a96a0b69a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 96 additions and 25 deletions

View file

@ -15,7 +15,7 @@ extern crate clap;
extern crate uucore; extern crate uucore;
use std::fs::File; use std::fs::File;
use std::io::{BufRead, BufReader, Lines, Stdin, stdin}; use std::io::{stdin, BufRead, BufReader, Lines, Stdin};
use std::cmp::{min, Ordering}; use std::cmp::{min, Ordering};
use clap::{App, Arg}; use clap::{App, Arg};
@ -44,6 +44,7 @@ struct Settings {
separator: Sep, separator: Sep,
autoformat: bool, autoformat: bool,
format: Vec<Spec>, format: Vec<Spec>,
empty: String,
} }
impl Default for Settings { impl Default for Settings {
@ -56,6 +57,7 @@ impl Default for Settings {
separator: Sep::Whitespaces, separator: Sep::Whitespaces,
autoformat: false, autoformat: false,
format: vec![], format: vec![],
empty: String::new(),
} }
} }
} }
@ -64,17 +66,32 @@ impl Default for Settings {
struct Repr<'a> { struct Repr<'a> {
separator: char, separator: char,
format: &'a [Spec], format: &'a [Spec],
empty: &'a str,
} }
impl<'a> Repr<'a> { impl<'a> Repr<'a> {
fn new(separator: char, format: &'a [Spec]) -> Repr { fn new(separator: char, format: &'a [Spec], empty: &'a str) -> Repr<'a> {
Repr { separator, format } Repr {
separator,
format,
empty,
}
} }
fn uses_format(&self) -> bool { fn uses_format(&self) -> bool {
!self.format.is_empty() !self.format.is_empty()
} }
/// Print the field or empty filler if the field is not set.
fn print_field(&self, field: Option<&str>) {
let value = match field {
Some(field) => field,
None => self.empty,
};
print!("{}", value);
}
/// Print each field except the one at the index. /// Print each field except the one at the index.
fn print_fields(&self, line: &Line, index: usize, max_fields: usize) { fn print_fields(&self, line: &Line, index: usize, max_fields: usize) {
for i in 0..min(max_fields, line.fields.len()) { for i in 0..min(max_fields, line.fields.len()) {
@ -96,7 +113,7 @@ impl<'a> Repr<'a> {
let field = match f(&self.format[i]) { let field = match f(&self.format[i]) {
Some(value) => value, Some(value) => value,
None => "", None => self.empty,
}; };
print!("{}", field); print!("{}", field);
@ -151,11 +168,11 @@ impl Line {
} }
/// Get field at index. /// Get field at index.
fn get_field(&self, index: usize) -> &str { fn get_field(&self, index: usize) -> Option<&str> {
if index < self.fields.len() { if index < self.fields.len() {
&self.fields[index] Some(&self.fields[index])
} else { } else {
"" None
} }
} }
} }
@ -244,21 +261,21 @@ impl<'a> State<'a> {
for line2 in &other.seq { for line2 in &other.seq {
if repr.uses_format() { if repr.uses_format() {
repr.print_format(|spec| match spec { repr.print_format(|spec| match spec {
&Spec::Key => Some(key), &Spec::Key => key,
&Spec::Field(file_num, field_num) => { &Spec::Field(file_num, field_num) => {
if file_num == self.file_num { if file_num == self.file_num {
return Some(line1.get_field(field_num)); return line1.get_field(field_num);
} }
if file_num == other.file_num { if file_num == other.file_num {
return Some(line2.get_field(field_num)); return line2.get_field(field_num);
} }
None None
} }
}); });
} else { } else {
print!("{}", key); repr.print_field(key);
repr.print_fields(&line1, self.key, self.max_fields); repr.print_fields(&line1, self.key, self.max_fields);
repr.print_fields(&line2, other.key, self.max_fields); repr.print_fields(&line2, other.key, self.max_fields);
} }
@ -311,15 +328,15 @@ impl<'a> State<'a> {
fn print_unpaired_line(&self, line: &Line, repr: &Repr) { fn print_unpaired_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 => Some(line.get_field(self.key)), &Spec::Key => line.get_field(self.key),
&Spec::Field(file_num, field_num) => if file_num == self.file_num { &Spec::Field(file_num, field_num) => if file_num == self.file_num {
Some(line.get_field(field_num)) line.get_field(field_num)
} else { } else {
None None
}, },
}); });
} else { } else {
print!("{}", line.get_field(self.key)); repr.print_field(line.get_field(self.key));
repr.print_fields(line, self.key, self.max_fields); repr.print_fields(line, self.key, self.max_fields);
} }
@ -344,6 +361,11 @@ When FILE1 or FILE2 (not both) is -, read standard input.")
.value_name("FILENUM") .value_name("FILENUM")
.help("also print unpairable lines from file FILENUM, where .help("also print unpairable lines from file FILENUM, where
FILENUM is 1 or 2, corresponding to FILE1 or FILE2")) FILENUM is 1 or 2, corresponding to FILE1 or FILE2"))
.arg(Arg::with_name("e")
.short("e")
.takes_value(true)
.value_name("EMPTY")
.help("replace missing input fields with EMPTY"))
.arg(Arg::with_name("i") .arg(Arg::with_name("i")
.short("i") .short("i")
.long("ignore-case") .long("ignore-case")
@ -389,12 +411,10 @@ FILENUM is 1 or 2, corresponding to FILE1 or FILE2"))
let mut settings: Settings = Default::default(); let mut settings: Settings = Default::default();
settings.print_unpaired = match matches.value_of("a") { settings.print_unpaired = match matches.value_of("a") {
Some(value) => { Some(value) => match value {
match value { "1" => FileNum::File1,
"1" => FileNum::File1, "2" => FileNum::File2,
"2" => FileNum::File2, value => crash!(1, "invalid file number: '{}'", value),
value => crash!(1, "invalid file number: '{}'", value),
}
}, },
None => FileNum::None, None => FileNum::None,
}; };
@ -421,6 +441,10 @@ FILENUM is 1 or 2, corresponding to FILE1 or FILE2"))
} }
} }
if let Some(empty) = matches.value_of("e") {
settings.empty = empty.to_string();
}
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();
@ -456,6 +480,7 @@ fn exec(file1: &str, file2: &str, settings: &Settings) -> i32 {
_ => ' ', _ => ' ',
}, },
&settings.format, &settings.format,
&settings.empty,
); );
state1.initialize(settings.separator, settings.autoformat); state1.initialize(settings.separator, settings.autoformat);
@ -522,10 +547,20 @@ fn parse_field_number_option(value: Option<&str>) -> Option<usize> {
Some(parse_field_number(value?)) Some(parse_field_number(value?))
} }
fn compare(field1: &str, field2: &str, ignore_case: bool) -> Ordering { fn compare(field1: Option<&str>, field2: Option<&str>, ignore_case: bool) -> Ordering {
if ignore_case { if let (Some(field1), Some(field2)) = (field1, field2) {
field1.to_lowercase().cmp(&field2.to_lowercase()) return if ignore_case {
} else { field1.to_lowercase().cmp(&field2.to_lowercase())
field1.cmp(field2) } else {
field1.cmp(field2)
};
}
match field1 {
Some(_) => Ordering::Greater,
None => match field2 {
Some(_) => Ordering::Less,
None => Ordering::Equal,
},
} }
} }

View file

@ -0,0 +1,5 @@
x 1
x 2
x 3
x 5
x 8

View file

@ -0,0 +1,5 @@
1 a x
2 b x
3 c f
4 d x
5 e x

View file

@ -173,3 +173,29 @@ fn empty_format() {
.arg("") .arg("")
.fails().stderr_is("join: error: invalid file number in field spec: ''"); .fails().stderr_is("join: error: invalid file number in field spec: ''");
} }
#[test]
fn empty_key() {
new_ucmd!()
.arg("fields_1.txt")
.arg("empty.txt")
.arg("-j")
.arg("2")
.arg("-a")
.arg("1")
.arg("-e")
.arg("x")
.succeeds().stdout_only_fixture("empty_key.expected");
}
#[test]
fn missing_format_fields() {
new_ucmd!()
.arg("fields_2.txt")
.arg("different_lengths.txt")
.arg("-o")
.arg("0 1.2 2.4")
.arg("-e")
.arg("x")
.succeeds().stdout_only_fixture("missing_format_fields.expected");
}