mirror of
https://github.com/uutils/coreutils
synced 2024-10-07 00:19:14 +00:00
numfmt: implement --field
This commit is contained in:
parent
200310be18
commit
0e02607dc7
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -2,3 +2,4 @@
|
|||
|
||||
pub mod coreopts;
|
||||
pub mod panic;
|
||||
pub mod ranges;
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
8
tests/fixtures/numfmt/df_expected.txt
vendored
Normal 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
8
tests/fixtures/numfmt/df_input.txt
vendored
Normal 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
|
Loading…
Reference in a new issue