mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 03:27:44 +00:00
numfmt: implement --field
This commit is contained in:
parent
200310be18
commit
0e02607dc7
8 changed files with 238 additions and 61 deletions
|
@ -14,11 +14,10 @@ use std::fs::File;
|
||||||
use std::io::{stdin, stdout, BufRead, BufReader, Read, Stdout, Write};
|
use std::io::{stdin, stdout, BufRead, BufReader, Read, Stdout, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use self::ranges::Range;
|
|
||||||
use self::searcher::Searcher;
|
use self::searcher::Searcher;
|
||||||
|
use uucore::ranges::Range;
|
||||||
|
|
||||||
mod buffer;
|
mod buffer;
|
||||||
mod ranges;
|
|
||||||
mod searcher;
|
mod searcher;
|
||||||
|
|
||||||
static SYNTAX: &str =
|
static SYNTAX: &str =
|
||||||
|
@ -125,7 +124,7 @@ enum Mode {
|
||||||
|
|
||||||
fn list_to_ranges(list: &str, complement: bool) -> Result<Vec<Range>, String> {
|
fn list_to_ranges(list: &str, complement: bool) -> Result<Vec<Range>, String> {
|
||||||
if complement {
|
if complement {
|
||||||
Range::from_list(list).map(|r| ranges::complement(&r))
|
Range::from_list(list).map(|r| uucore::ranges::complement(&r))
|
||||||
} else {
|
} else {
|
||||||
Range::from_list(list)
|
Range::from_list(list)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,13 @@
|
||||||
// * For the full copyright and license information, please view the LICENSE
|
// * For the full copyright and license information, please view the LICENSE
|
||||||
// * file that was distributed with this source code.
|
// * file that was distributed with this source code.
|
||||||
|
|
||||||
use std::fmt;
|
|
||||||
use std::io::BufRead;
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate uucore;
|
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 VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
static ABOUT: &str = "Convert numbers from/to human-readable strings";
|
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:
|
iec-i accept optional two-letter suffix:
|
||||||
|
|
||||||
1Ki = 1024, 1Mi = 1048576, ...
|
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 {
|
mod options {
|
||||||
|
pub const FIELD: &str = "field";
|
||||||
|
pub const FIELD_DEFAULT: &str = "1";
|
||||||
pub const FROM: &str = "from";
|
pub const FROM: &str = "from";
|
||||||
pub const FROM_DEFAULT: &str = "none";
|
pub const FROM_DEFAULT: &str = "none";
|
||||||
pub const HEADER: &str = "header";
|
pub const HEADER: &str = "header";
|
||||||
|
@ -113,6 +123,10 @@ impl fmt::Display for DisplayableSuffix {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_suffix(s: &str) -> Result<(f64, Option<Suffix>)> {
|
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 with_i = s.ends_with('i');
|
||||||
let mut iter = s.chars();
|
let mut iter = s.chars();
|
||||||
if with_i {
|
if with_i {
|
||||||
|
@ -168,6 +182,64 @@ struct NumfmtOptions {
|
||||||
transform: TransformOptions,
|
transform: TransformOptions,
|
||||||
padding: isize,
|
padding: isize,
|
||||||
header: usize,
|
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> {
|
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.
|
/// Otherwise, truncate the result to the next highest whole number.
|
||||||
///
|
///
|
||||||
/// Examples:
|
/// # Examples:
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use uu_numfmt::div_ceil;
|
/// use uu_numfmt::div_ceil;
|
||||||
|
@ -301,15 +373,34 @@ fn format_string(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_and_print(s: &str, options: &NumfmtOptions) -> Result<()> {
|
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 {
|
if field_selected {
|
||||||
true => Some((prefix.len() + field.len()) as isize),
|
let empty_prefix = prefix.is_empty();
|
||||||
false => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let field = format_string(field, options, implicit_padding)?;
|
// print delimiter before second and subsequent fields
|
||||||
println!("{}{}", field, suffix);
|
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(())
|
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 {
|
Ok(NumfmtOptions {
|
||||||
transform,
|
transform,
|
||||||
padding,
|
padding,
|
||||||
header,
|
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<()> {
|
fn handle_args<'a>(args: impl Iterator<Item = &'a str>, options: NumfmtOptions) -> Result<()> {
|
||||||
for l in args {
|
for l in args {
|
||||||
format_and_print(l, &options)?;
|
format_and_print(l, &options)?;
|
||||||
|
@ -430,6 +485,14 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
.about(ABOUT)
|
.about(ABOUT)
|
||||||
.usage(&usage[..])
|
.usage(&usage[..])
|
||||||
.after_help(LONG_HELP)
|
.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(
|
||||||
Arg::with_name(options::FROM)
|
Arg::with_name(options::FROM)
|
||||||
.long(options::FROM)
|
.long(options::FROM)
|
||||||
|
@ -477,6 +540,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
std::io::stdout().flush().expect("error flushing stdout");
|
||||||
show_info!("{}", e);
|
show_info!("{}", e);
|
||||||
1
|
1
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ mod mods; // core cross-platform modules
|
||||||
// * cross-platform modules
|
// * cross-platform modules
|
||||||
pub use crate::mods::coreopts;
|
pub use crate::mods::coreopts;
|
||||||
pub use crate::mods::panic;
|
pub use crate::mods::panic;
|
||||||
|
pub use crate::mods::ranges;
|
||||||
|
|
||||||
// * feature-gated modules
|
// * feature-gated modules
|
||||||
#[cfg(feature = "encoding")]
|
#[cfg(feature = "encoding")]
|
||||||
|
|
|
@ -2,3 +2,4 @@
|
||||||
|
|
||||||
pub mod coreopts;
|
pub mod coreopts;
|
||||||
pub mod panic;
|
pub mod panic;
|
||||||
|
pub mod ranges;
|
||||||
|
|
|
@ -144,3 +144,31 @@ pub fn complement(ranges: &[Range]) -> Vec<Range> {
|
||||||
|
|
||||||
complements
|
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()
|
.succeeds()
|
||||||
.stdout_is_fixture("gnutest_iec-i_result.txt");
|
.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…
Add table
Add a link
Reference in a new issue