numfmt: implement --field

This commit is contained in:
Daniel Rocco 2021-02-28 13:06:58 -05:00
parent 200310be18
commit 0e02607dc7
8 changed files with 238 additions and 61 deletions

View file

@ -14,11 +14,10 @@ use std::fs::File;
use std::io::{stdin, stdout, BufRead, BufReader, Read, Stdout, Write};
use std::path::Path;
use self::ranges::Range;
use self::searcher::Searcher;
use uucore::ranges::Range;
mod buffer;
mod ranges;
mod searcher;
static SYNTAX: &str =
@ -125,7 +124,7 @@ enum Mode {
fn list_to_ranges(list: &str, complement: bool) -> Result<Vec<Range>, String> {
if complement {
Range::from_list(list).map(|r| ranges::complement(&r))
Range::from_list(list).map(|r| uucore::ranges::complement(&r))
} else {
Range::from_list(list)
}

View file

@ -5,13 +5,13 @@
// * For the full copyright and license information, please view the LICENSE
// * file that was distributed with this source code.
use std::fmt;
use std::io::BufRead;
#[macro_use]
extern crate uucore;
use clap::{App, Arg, ArgMatches};
use clap::{App, AppSettings, Arg, ArgMatches};
use std::fmt;
use std::io::{BufRead, Write};
use uucore::ranges::Range;
static VERSION: &str = env!("CARGO_PKG_VERSION");
static ABOUT: &str = "Convert numbers from/to human-readable strings";
@ -33,9 +33,19 @@ static LONG_HELP: &str = "UNIT options:
iec-i accept optional two-letter suffix:
1Ki = 1024, 1Mi = 1048576, ...
FIELDS supports cut(1) style field ranges:
N N'th field, counted from 1
N- from N'th field, to end of line
N-M from N'th to M'th field (inclusive)
-M from first to M'th field (inclusive)
- all fields
Multiple fields/ranges can be separated with commas
";
mod options {
pub const FIELD: &str = "field";
pub const FIELD_DEFAULT: &str = "1";
pub const FROM: &str = "from";
pub const FROM_DEFAULT: &str = "none";
pub const HEADER: &str = "header";
@ -113,6 +123,10 @@ impl fmt::Display for DisplayableSuffix {
}
fn parse_suffix(s: &str) -> Result<(f64, Option<Suffix>)> {
if s.is_empty() {
return Err("invalid number: ".to_string());
}
let with_i = s.ends_with('i');
let mut iter = s.chars();
if with_i {
@ -168,6 +182,64 @@ struct NumfmtOptions {
transform: TransformOptions,
padding: isize,
header: usize,
fields: Vec<Range>,
}
/// Iterate over a line's fields, where each field is a contiguous sequence of
/// non-whitespace, optionally prefixed with one or more characters of leading
/// whitespace. Fields are returned as tuples of `(prefix, field)`.
///
/// # Examples:
///
/// ```
/// let mut fields = uu_numfmt::WhitespaceSplitter { s: Some(" 1234 5") };
///
/// assert_eq!(Some((" ", "1234")), fields.next());
/// assert_eq!(Some((" ", "5")), fields.next());
/// assert_eq!(None, fields.next());
/// ```
///
/// Delimiters are included in the results; `prefix` will be empty only for
/// the first field of the line (including the case where the input line is
/// empty):
///
/// ```
/// let mut fields = uu_numfmt::WhitespaceSplitter { s: Some("first second") };
///
/// assert_eq!(Some(("", "first")), fields.next());
/// assert_eq!(Some((" ", "second")), fields.next());
///
/// let mut fields = uu_numfmt::WhitespaceSplitter { s: Some("") };
///
/// assert_eq!(Some(("", "")), fields.next());
/// ```
pub struct WhitespaceSplitter<'a> {
pub s: Option<&'a str>,
}
impl<'a> Iterator for WhitespaceSplitter<'a> {
type Item = (&'a str, &'a str);
/// Yield the next field in the input string as a tuple `(prefix, field)`.
fn next(&mut self) -> Option<Self::Item> {
let haystack = self.s?;
let (prefix, field) = haystack.split_at(
haystack
.find(|c: char| !c.is_whitespace())
.unwrap_or_else(|| haystack.len()),
);
let (field, rest) = field.split_at(
field
.find(|c: char| c.is_whitespace())
.unwrap_or_else(|| field.len()),
);
self.s = if !rest.is_empty() { Some(rest) } else { None };
Some((prefix, field))
}
}
fn remove_suffix(i: f64, s: Option<Suffix>, u: &Unit) -> Result<f64> {
@ -214,7 +286,7 @@ fn transform_from(s: &str, opts: &Transform) -> Result<f64> {
///
/// Otherwise, truncate the result to the next highest whole number.
///
/// Examples:
/// # Examples:
///
/// ```
/// use uu_numfmt::div_ceil;
@ -301,15 +373,34 @@ fn format_string(
}
fn format_and_print(s: &str, options: &NumfmtOptions) -> Result<()> {
let (prefix, field, suffix) = extract_field(&s)?;
for (n, (prefix, field)) in (1..).zip(WhitespaceSplitter { s: Some(s) }) {
let field_selected = uucore::ranges::contain(&options.fields, n);
let implicit_padding = match !prefix.is_empty() && options.padding == 0 {
true => Some((prefix.len() + field.len()) as isize),
false => None,
};
if field_selected {
let empty_prefix = prefix.is_empty();
let field = format_string(field, options, implicit_padding)?;
println!("{}{}", field, suffix);
// print delimiter before second and subsequent fields
let prefix = if n > 1 {
print!(" ");
&prefix[1..]
} else {
&prefix
};
let implicit_padding = if !empty_prefix && options.padding == 0 {
Some((prefix.len() + field.len()) as isize)
} else {
None
};
print!("{}", format_string(&field, options, implicit_padding)?);
} else {
// print unselected field without conversion
print!("{}{}", prefix, field);
}
}
println!();
Ok(())
}
@ -344,59 +435,23 @@ fn parse_options(args: &ArgMatches) -> Result<NumfmtOptions> {
}
}?;
let fields = match args.value_of(options::FIELD) {
Some("-") => vec![Range {
low: 1,
high: std::usize::MAX,
}],
Some(v) => Range::from_list(v)?,
None => unreachable!(),
};
Ok(NumfmtOptions {
transform,
padding,
header,
fields,
})
}
/// Extract the field to convert from `line`.
///
/// The field is the first sequence of non-whitespace characters in `line`.
///
/// Returns a [`Result`] of `(prefix: &str, field: &str, suffix: &str)`, where
/// `prefix` contains any leading whitespace, `field` is the field to convert,
/// and `suffix` is everything after the field. `prefix` and `suffix` may be
/// empty.
///
/// Returns an [`Err`] if `line` is empty or consists only of whitespace.
///
/// Examples:
///
/// ```
/// use uu_numfmt::extract_field;
///
/// assert_eq!("1K", extract_field("1K").unwrap().1);
///
/// let (prefix, field, suffix) = extract_field(" 1K qux").unwrap();
/// assert_eq!(" ", prefix);
/// assert_eq!("1K", field);
/// assert_eq!(" qux", suffix);
///
/// assert!(extract_field("").is_err());
/// ```
pub fn extract_field(line: &str) -> Result<(&str, &str, &str)> {
let start = line
.find(|c: char| !c.is_whitespace())
.ok_or("invalid number: ")?;
let prefix = &line[..start];
let mut field = &line[start..];
let suffix = match field.find(|c: char| c.is_whitespace()) {
Some(i) => {
let suffix = &field[i..];
field = &field[..i];
suffix
}
None => "",
};
Ok((prefix, field, suffix))
}
fn handle_args<'a>(args: impl Iterator<Item = &'a str>, options: NumfmtOptions) -> Result<()> {
for l in args {
format_and_print(l, &options)?;
@ -430,6 +485,14 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.about(ABOUT)
.usage(&usage[..])
.after_help(LONG_HELP)
.setting(AppSettings::AllowNegativeNumbers)
.arg(
Arg::with_name(options::FIELD)
.long(options::FIELD)
.help("replace the numbers in these input fields (default=1) see FIELDS below")
.value_name("FIELDS")
.default_value(options::FIELD_DEFAULT),
)
.arg(
Arg::with_name(options::FROM)
.long(options::FROM)
@ -477,6 +540,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
match result {
Err(e) => {
std::io::stdout().flush().expect("error flushing stdout");
show_info!("{}", e);
1
}

View file

@ -27,6 +27,7 @@ mod mods; // core cross-platform modules
// * cross-platform modules
pub use crate::mods::coreopts;
pub use crate::mods::panic;
pub use crate::mods::ranges;
// * feature-gated modules
#[cfg(feature = "encoding")]

View file

@ -2,3 +2,4 @@
pub mod coreopts;
pub mod panic;
pub mod ranges;

View file

@ -144,3 +144,31 @@ pub fn complement(ranges: &[Range]) -> Vec<Range> {
complements
}
/// Test if at least one of the given Ranges contain the supplied value.
///
/// Examples:
///
/// ```
/// let ranges = uucore::ranges::Range::from_list("11,2,6-8").unwrap();
///
/// assert!(!uucore::ranges::contain(&ranges, 0));
/// assert!(!uucore::ranges::contain(&ranges, 1));
/// assert!(!uucore::ranges::contain(&ranges, 5));
/// assert!(!uucore::ranges::contain(&ranges, 10));
///
/// assert!(uucore::ranges::contain(&ranges, 2));
/// assert!(uucore::ranges::contain(&ranges, 6));
/// assert!(uucore::ranges::contain(&ranges, 7));
/// assert!(uucore::ranges::contain(&ranges, 8));
/// assert!(uucore::ranges::contain(&ranges, 11));
/// ```
pub fn contain(ranges: &[Range], n: usize) -> bool {
for range in ranges {
if n >= range.low && n <= range.high {
return true;
}
}
false
}

View file

@ -315,3 +315,71 @@ fn test_to_iec_i_should_truncate_output() {
.succeeds()
.stdout_is_fixture("gnutest_iec-i_result.txt");
}
#[test]
fn test_format_selected_field() {
new_ucmd!()
.args(&["--from=auto", "--field", "3", "1K 2K 3K"])
.succeeds()
.stdout_only("1K 2K 3000\n");
new_ucmd!()
.args(&["--from=auto", "--field", "2", "1K 2K 3K"])
.succeeds()
.stdout_only("1K 2000 3K\n");
}
#[test]
fn test_format_selected_fields() {
new_ucmd!()
.args(&["--from=auto", "--field", "1,4,3", "1K 2K 3K 4K 5K 6K"])
.succeeds()
.stdout_only("1000 2K 3000 4000 5K 6K\n");
}
#[test]
fn test_should_succeed_if_selected_field_out_of_range() {
new_ucmd!()
.args(&["--from=auto", "--field", "9", "1K 2K 3K"])
.succeeds()
.stdout_only("1K 2K 3K\n");
}
#[test]
fn test_format_selected_field_range() {
new_ucmd!()
.args(&["--from=auto", "--field", "2-5", "1K 2K 3K 4K 5K 6K"])
.succeeds()
.stdout_only("1K 2000 3000 4000 5000 6K\n");
}
#[test]
fn test_should_succeed_if_range_out_of_bounds() {
new_ucmd!()
.args(&["--from=auto", "--field", "5-10", "1K 2K 3K 4K 5K 6K"])
.succeeds()
.stdout_only("1K 2K 3K 4K 5000 6000\n");
}
#[test]
fn test_implied_initial_field_value() {
new_ucmd!()
.args(&["--from=auto", "--field", "-2", "1K 2K 3K"])
.succeeds()
.stdout_only("1000 2000 3K\n");
// same as above but with the equal sign
new_ucmd!()
.args(&["--from=auto", "--field=-2", "1K 2K 3K"])
.succeeds()
.stdout_only("1000 2000 3K\n");
}
#[test]
fn test_field_df_example() {
// df -B1 | numfmt --header --field 2-4 --to=si
new_ucmd!()
.args(&["--header", "--field", "2-4", "--to=si"])
.pipe_in_fixture("df_input.txt")
.succeeds()
.stdout_is_fixture("df_expected.txt");
}

8
tests/fixtures/numfmt/df_expected.txt vendored Normal file
View file

@ -0,0 +1,8 @@
Filesystem 1B-blocks Used Available Use% Mounted on
udev 8.2G 0 8.2G 0% /dev
tmpfs 1.7G 2.1M 1.7G 1% /run
/dev/nvme0n1p2 1.1T 433G 523G 46% /
tmpfs 8.3G 145M 8.1G 2% /dev/shm
tmpfs 5.3M 4.1K 5.3M 1% /run/lock
tmpfs 8.3G 0 8.3G 0% /sys/fs/cgroup
/dev/nvme0n1p1 536M 8.2M 528M 2% /boot/efi

8
tests/fixtures/numfmt/df_input.txt vendored Normal file
View file

@ -0,0 +1,8 @@
Filesystem 1B-blocks Used Available Use% Mounted on
udev 8192688128 0 8192688128 0% /dev
tmpfs 1643331584 2015232 1641316352 1% /run
/dev/nvme0n1p2 1006530654208 432716689408 522613624832 46% /
tmpfs 8216649728 144437248 8072212480 2% /dev/shm
tmpfs 5242880 4096 5238784 1% /run/lock
tmpfs 8216649728 0 8216649728 0% /sys/fs/cgroup
/dev/nvme0n1p1 535805952 8175616 527630336 2% /boot/efi