diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index 33399aba0..95411e3fb 100644 --- a/src/uu/cut/src/cut.rs +++ b/src/uu/cut/src/cut.rs @@ -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, 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) } diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index 0ee25e96a..ea750c024 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -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)> { + 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, +} + +/// 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 { + 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, u: &Unit) -> Result { @@ -214,7 +286,7 @@ fn transform_from(s: &str, opts: &Transform) -> Result { /// /// 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 { } }?; + 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, 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 } diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 768843409..324095b6a 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -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")] diff --git a/src/uucore/src/lib/mods.rs b/src/uucore/src/lib/mods.rs index 0becc71bd..c73909dcc 100644 --- a/src/uucore/src/lib/mods.rs +++ b/src/uucore/src/lib/mods.rs @@ -2,3 +2,4 @@ pub mod coreopts; pub mod panic; +pub mod ranges; diff --git a/src/uu/cut/src/ranges.rs b/src/uucore/src/lib/mods/ranges.rs similarity index 83% rename from src/uu/cut/src/ranges.rs rename to src/uucore/src/lib/mods/ranges.rs index 74fec08e6..d4a6bf601 100644 --- a/src/uu/cut/src/ranges.rs +++ b/src/uucore/src/lib/mods/ranges.rs @@ -144,3 +144,31 @@ pub fn complement(ranges: &[Range]) -> Vec { 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 +} diff --git a/tests/by-util/test_numfmt.rs b/tests/by-util/test_numfmt.rs index 787ec6832..c22db3bf5 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.rs @@ -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"); +} diff --git a/tests/fixtures/numfmt/df_expected.txt b/tests/fixtures/numfmt/df_expected.txt new file mode 100644 index 000000000..ea8c3d79f --- /dev/null +++ b/tests/fixtures/numfmt/df_expected.txt @@ -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 diff --git a/tests/fixtures/numfmt/df_input.txt b/tests/fixtures/numfmt/df_input.txt new file mode 100644 index 000000000..4c094d54f --- /dev/null +++ b/tests/fixtures/numfmt/df_input.txt @@ -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