mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 19:47:45 +00:00
Merge pull request #1827 from drocco007/numfmt-delimiter
numfmt: implement --delimiter
This commit is contained in:
commit
ba55ef166e
5 changed files with 526 additions and 366 deletions
296
src/uu/numfmt/src/format.rs
Normal file
296
src/uu/numfmt/src/format.rs
Normal file
|
@ -0,0 +1,296 @@
|
||||||
|
use crate::options::NumfmtOptions;
|
||||||
|
use crate::units::{
|
||||||
|
DisplayableSuffix, RawSuffix, Result, Suffix, Transform, Unit, IEC_BASES, SI_BASES,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 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::format::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::format::WhitespaceSplitter { s: Some("first second") };
|
||||||
|
///
|
||||||
|
/// assert_eq!(Some(("", "first")), fields.next());
|
||||||
|
/// assert_eq!(Some((" ", "second")), fields.next());
|
||||||
|
///
|
||||||
|
/// let mut fields = uu_numfmt::format::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 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 {
|
||||||
|
iter.next_back();
|
||||||
|
}
|
||||||
|
let suffix: Option<Suffix> = match iter.next_back() {
|
||||||
|
Some('K') => Ok(Some((RawSuffix::K, with_i))),
|
||||||
|
Some('M') => Ok(Some((RawSuffix::M, with_i))),
|
||||||
|
Some('G') => Ok(Some((RawSuffix::G, with_i))),
|
||||||
|
Some('T') => Ok(Some((RawSuffix::T, with_i))),
|
||||||
|
Some('P') => Ok(Some((RawSuffix::P, with_i))),
|
||||||
|
Some('E') => Ok(Some((RawSuffix::E, with_i))),
|
||||||
|
Some('Z') => Ok(Some((RawSuffix::Z, with_i))),
|
||||||
|
Some('Y') => Ok(Some((RawSuffix::Y, with_i))),
|
||||||
|
Some('0'..='9') => Ok(None),
|
||||||
|
_ => Err(format!("invalid suffix in input: ‘{}’", s)),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let suffix_len = match suffix {
|
||||||
|
None => 0,
|
||||||
|
Some((_, false)) => 1,
|
||||||
|
Some((_, true)) => 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
let number = s[..s.len() - suffix_len]
|
||||||
|
.parse::<f64>()
|
||||||
|
.map_err(|_| format!("invalid number: ‘{}’", s))?;
|
||||||
|
|
||||||
|
Ok((number, suffix))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_suffix(i: f64, s: Option<Suffix>, u: &Unit) -> Result<f64> {
|
||||||
|
match (s, u) {
|
||||||
|
(None, _) => Ok(i),
|
||||||
|
(Some((raw_suffix, false)), &Unit::Auto) | (Some((raw_suffix, false)), &Unit::Si) => {
|
||||||
|
match raw_suffix {
|
||||||
|
RawSuffix::K => Ok(i * 1e3),
|
||||||
|
RawSuffix::M => Ok(i * 1e6),
|
||||||
|
RawSuffix::G => Ok(i * 1e9),
|
||||||
|
RawSuffix::T => Ok(i * 1e12),
|
||||||
|
RawSuffix::P => Ok(i * 1e15),
|
||||||
|
RawSuffix::E => Ok(i * 1e18),
|
||||||
|
RawSuffix::Z => Ok(i * 1e21),
|
||||||
|
RawSuffix::Y => Ok(i * 1e24),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Some((raw_suffix, false)), &Unit::Iec(false))
|
||||||
|
| (Some((raw_suffix, true)), &Unit::Auto)
|
||||||
|
| (Some((raw_suffix, true)), &Unit::Iec(true)) => match raw_suffix {
|
||||||
|
RawSuffix::K => Ok(i * IEC_BASES[1]),
|
||||||
|
RawSuffix::M => Ok(i * IEC_BASES[2]),
|
||||||
|
RawSuffix::G => Ok(i * IEC_BASES[3]),
|
||||||
|
RawSuffix::T => Ok(i * IEC_BASES[4]),
|
||||||
|
RawSuffix::P => Ok(i * IEC_BASES[5]),
|
||||||
|
RawSuffix::E => Ok(i * IEC_BASES[6]),
|
||||||
|
RawSuffix::Z => Ok(i * IEC_BASES[7]),
|
||||||
|
RawSuffix::Y => Ok(i * IEC_BASES[8]),
|
||||||
|
},
|
||||||
|
(_, _) => Err("This suffix is unsupported for specified unit".to_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transform_from(s: &str, opts: &Transform) -> Result<f64> {
|
||||||
|
let (i, suffix) = parse_suffix(s)?;
|
||||||
|
|
||||||
|
remove_suffix(i, suffix, &opts.unit).map(|n| if n < 0.0 { -n.abs().ceil() } else { n.ceil() })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Divide numerator by denominator, with ceiling.
|
||||||
|
///
|
||||||
|
/// If the result of the division is less than 10.0, truncate the result
|
||||||
|
/// to the next highest tenth.
|
||||||
|
///
|
||||||
|
/// Otherwise, truncate the result to the next highest whole number.
|
||||||
|
///
|
||||||
|
/// # Examples:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use uu_numfmt::format::div_ceil;
|
||||||
|
///
|
||||||
|
/// assert_eq!(div_ceil(1.01, 1.0), 1.1);
|
||||||
|
/// assert_eq!(div_ceil(999.1, 1000.), 1.0);
|
||||||
|
/// assert_eq!(div_ceil(1001., 10.), 101.);
|
||||||
|
/// assert_eq!(div_ceil(9991., 10.), 1000.);
|
||||||
|
/// assert_eq!(div_ceil(-12.34, 1.0), -13.0);
|
||||||
|
/// assert_eq!(div_ceil(1000.0, -3.14), -319.0);
|
||||||
|
/// assert_eq!(div_ceil(-271828.0, -271.0), 1004.0);
|
||||||
|
/// ```
|
||||||
|
pub fn div_ceil(n: f64, d: f64) -> f64 {
|
||||||
|
let v = n / (d / 10.0);
|
||||||
|
let (v, sign) = if v < 0.0 { (v.abs(), -1.0) } else { (v, 1.0) };
|
||||||
|
|
||||||
|
if v < 100.0 {
|
||||||
|
v.ceil() / 10.0 * sign
|
||||||
|
} else {
|
||||||
|
(v / 10.0).ceil() * sign
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consider_suffix(n: f64, u: &Unit) -> Result<(f64, Option<Suffix>)> {
|
||||||
|
use crate::units::RawSuffix::*;
|
||||||
|
|
||||||
|
let abs_n = n.abs();
|
||||||
|
let suffixes = [K, M, G, T, P, E, Z, Y];
|
||||||
|
|
||||||
|
let (bases, with_i) = match *u {
|
||||||
|
Unit::Si => (&SI_BASES, false),
|
||||||
|
Unit::Iec(with_i) => (&IEC_BASES, with_i),
|
||||||
|
Unit::Auto => return Err("Unit 'auto' isn't supported with --to options".to_owned()),
|
||||||
|
Unit::None => return Ok((n, None)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let i = match abs_n {
|
||||||
|
_ if abs_n <= bases[1] - 1.0 => return Ok((n, None)),
|
||||||
|
_ if abs_n < bases[2] => 1,
|
||||||
|
_ if abs_n < bases[3] => 2,
|
||||||
|
_ if abs_n < bases[4] => 3,
|
||||||
|
_ if abs_n < bases[5] => 4,
|
||||||
|
_ if abs_n < bases[6] => 5,
|
||||||
|
_ if abs_n < bases[7] => 6,
|
||||||
|
_ if abs_n < bases[8] => 7,
|
||||||
|
_ if abs_n < bases[9] => 8,
|
||||||
|
_ => return Err("Number is too big and unsupported".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let v = div_ceil(n, bases[i]);
|
||||||
|
|
||||||
|
// check if rounding pushed us into the next base
|
||||||
|
if v.abs() >= bases[1] {
|
||||||
|
Ok((v / bases[1], Some((suffixes[i], with_i))))
|
||||||
|
} else {
|
||||||
|
Ok((v, Some((suffixes[i - 1], with_i))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transform_to(s: f64, opts: &Transform) -> Result<String> {
|
||||||
|
let (i2, s) = consider_suffix(s, &opts.unit)?;
|
||||||
|
Ok(match s {
|
||||||
|
None => format!("{}", i2),
|
||||||
|
Some(s) if i2.abs() < 10.0 => format!("{:.1}{}", i2, DisplayableSuffix(s)),
|
||||||
|
Some(s) => format!("{:.0}{}", i2, DisplayableSuffix(s)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_string(
|
||||||
|
source: &str,
|
||||||
|
options: &NumfmtOptions,
|
||||||
|
implicit_padding: Option<isize>,
|
||||||
|
) -> Result<String> {
|
||||||
|
let number = transform_to(
|
||||||
|
transform_from(source, &options.transform.from)?,
|
||||||
|
&options.transform.to,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(match implicit_padding.unwrap_or(options.padding) {
|
||||||
|
p if p == 0 => number,
|
||||||
|
p if p > 0 => format!("{:>padding$}", number, padding = p as usize),
|
||||||
|
p => format!("{:<padding$}", number, padding = p.abs() as usize),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_and_print_delimited(s: &str, options: &NumfmtOptions) -> Result<()> {
|
||||||
|
let delimiter = options.delimiter.as_ref().unwrap();
|
||||||
|
|
||||||
|
for (n, field) in (1..).zip(s.split(delimiter)) {
|
||||||
|
let field_selected = uucore::ranges::contain(&options.fields, n);
|
||||||
|
|
||||||
|
// print delimiter before second and subsequent fields
|
||||||
|
if n > 1 {
|
||||||
|
print!("{}", delimiter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if field_selected {
|
||||||
|
print!("{}", format_string(&field.trim_start(), options, None)?);
|
||||||
|
} else {
|
||||||
|
// print unselected field without conversion
|
||||||
|
print!("{}", field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_and_print_whitespace(s: &str, options: &NumfmtOptions) -> Result<()> {
|
||||||
|
for (n, (prefix, field)) in (1..).zip(WhitespaceSplitter { s: Some(s) }) {
|
||||||
|
let field_selected = uucore::ranges::contain(&options.fields, n);
|
||||||
|
|
||||||
|
if field_selected {
|
||||||
|
let empty_prefix = prefix.is_empty();
|
||||||
|
|
||||||
|
// 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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a line of text according to the selected options.
|
||||||
|
///
|
||||||
|
/// Given a line of text `s`, split the line into fields, transform and format
|
||||||
|
/// any selected numeric fields, and print the result to stdout. Fields not
|
||||||
|
/// selected for conversion are passed through unmodified.
|
||||||
|
pub fn format_and_print(s: &str, options: &NumfmtOptions) -> Result<()> {
|
||||||
|
match &options.delimiter {
|
||||||
|
Some(_) => format_and_print_delimited(s, options),
|
||||||
|
None => format_and_print_whitespace(s, options),
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,11 +8,17 @@
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate uucore;
|
extern crate uucore;
|
||||||
|
|
||||||
|
use crate::format::format_and_print;
|
||||||
|
use crate::options::*;
|
||||||
|
use crate::units::{Result, Transform, Unit};
|
||||||
use clap::{App, AppSettings, Arg, ArgMatches};
|
use clap::{App, AppSettings, Arg, ArgMatches};
|
||||||
use std::fmt;
|
|
||||||
use std::io::{BufRead, Write};
|
use std::io::{BufRead, Write};
|
||||||
use uucore::ranges::Range;
|
use uucore::ranges::Range;
|
||||||
|
|
||||||
|
pub mod format;
|
||||||
|
mod options;
|
||||||
|
mod units;
|
||||||
|
|
||||||
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";
|
||||||
static LONG_HELP: &str = "UNIT options:
|
static LONG_HELP: &str = "UNIT options:
|
||||||
|
@ -43,119 +49,33 @@ FIELDS supports cut(1) style field ranges:
|
||||||
Multiple fields/ranges can be separated with commas
|
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";
|
|
||||||
pub const HEADER_DEFAULT: &str = "1";
|
|
||||||
pub const NUMBER: &str = "NUMBER";
|
|
||||||
pub const PADDING: &str = "padding";
|
|
||||||
pub const TO: &str = "to";
|
|
||||||
pub const TO_DEFAULT: &str = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_usage() -> String {
|
fn get_usage() -> String {
|
||||||
format!("{0} [OPTION]... [NUMBER]...", executable!())
|
format!("{0} [OPTION]... [NUMBER]...", executable!())
|
||||||
}
|
}
|
||||||
|
|
||||||
const SI_BASES: [f64; 10] = [1., 1e3, 1e6, 1e9, 1e12, 1e15, 1e18, 1e21, 1e24, 1e27];
|
fn handle_args<'a>(args: impl Iterator<Item = &'a str>, options: NumfmtOptions) -> Result<()> {
|
||||||
|
for l in args {
|
||||||
const IEC_BASES: [f64; 10] = [
|
format_and_print(l, &options)?;
|
||||||
1.,
|
|
||||||
1_024.,
|
|
||||||
1_048_576.,
|
|
||||||
1_073_741_824.,
|
|
||||||
1_099_511_627_776.,
|
|
||||||
1_125_899_906_842_624.,
|
|
||||||
1_152_921_504_606_846_976.,
|
|
||||||
1_180_591_620_717_411_303_424.,
|
|
||||||
1_208_925_819_614_629_174_706_176.,
|
|
||||||
1_237_940_039_285_380_274_899_124_224.,
|
|
||||||
];
|
|
||||||
|
|
||||||
type Result<T> = std::result::Result<T, String>;
|
|
||||||
|
|
||||||
type WithI = bool;
|
|
||||||
|
|
||||||
enum Unit {
|
|
||||||
Auto,
|
|
||||||
Si,
|
|
||||||
Iec(WithI),
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
enum RawSuffix {
|
|
||||||
K,
|
|
||||||
M,
|
|
||||||
G,
|
|
||||||
T,
|
|
||||||
P,
|
|
||||||
E,
|
|
||||||
Z,
|
|
||||||
Y,
|
|
||||||
}
|
|
||||||
|
|
||||||
type Suffix = (RawSuffix, WithI);
|
|
||||||
|
|
||||||
struct DisplayableSuffix(Suffix);
|
|
||||||
|
|
||||||
impl fmt::Display for DisplayableSuffix {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
let DisplayableSuffix((ref raw_suffix, ref with_i)) = *self;
|
|
||||||
match raw_suffix {
|
|
||||||
RawSuffix::K => write!(f, "K"),
|
|
||||||
RawSuffix::M => write!(f, "M"),
|
|
||||||
RawSuffix::G => write!(f, "G"),
|
|
||||||
RawSuffix::T => write!(f, "T"),
|
|
||||||
RawSuffix::P => write!(f, "P"),
|
|
||||||
RawSuffix::E => write!(f, "E"),
|
|
||||||
RawSuffix::Z => write!(f, "Z"),
|
|
||||||
RawSuffix::Y => write!(f, "Y"),
|
|
||||||
}
|
|
||||||
.and_then(|()| match with_i {
|
|
||||||
true => write!(f, "i"),
|
|
||||||
false => Ok(()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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');
|
Ok(())
|
||||||
let mut iter = s.chars();
|
}
|
||||||
if with_i {
|
|
||||||
iter.next_back();
|
fn handle_stdin(options: NumfmtOptions) -> Result<()> {
|
||||||
|
let stdin = std::io::stdin();
|
||||||
|
let locked_stdin = stdin.lock();
|
||||||
|
|
||||||
|
let mut lines = locked_stdin.lines();
|
||||||
|
for l in lines.by_ref().take(options.header) {
|
||||||
|
l.map(|s| println!("{}", s)).map_err(|e| e.to_string())?;
|
||||||
}
|
}
|
||||||
let suffix: Option<Suffix> = match iter.next_back() {
|
|
||||||
Some('K') => Ok(Some((RawSuffix::K, with_i))),
|
|
||||||
Some('M') => Ok(Some((RawSuffix::M, with_i))),
|
|
||||||
Some('G') => Ok(Some((RawSuffix::G, with_i))),
|
|
||||||
Some('T') => Ok(Some((RawSuffix::T, with_i))),
|
|
||||||
Some('P') => Ok(Some((RawSuffix::P, with_i))),
|
|
||||||
Some('E') => Ok(Some((RawSuffix::E, with_i))),
|
|
||||||
Some('Z') => Ok(Some((RawSuffix::Z, with_i))),
|
|
||||||
Some('Y') => Ok(Some((RawSuffix::Y, with_i))),
|
|
||||||
Some('0'..='9') => Ok(None),
|
|
||||||
_ => Err(format!("invalid suffix in input: ‘{}’", s)),
|
|
||||||
}?;
|
|
||||||
|
|
||||||
let suffix_len = match suffix {
|
for l in lines {
|
||||||
None => 0,
|
l.map_err(|e| e.to_string())
|
||||||
Some((_, false)) => 1,
|
.and_then(|l| format_and_print(&l, &options))?;
|
||||||
Some((_, true)) => 2,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
let number = s[..s.len() - suffix_len]
|
Ok(())
|
||||||
.parse::<f64>()
|
|
||||||
.map_err(|_| format!("invalid number: ‘{}’", s))?;
|
|
||||||
|
|
||||||
Ok((number, suffix))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_unit(s: &str) -> Result<Unit> {
|
fn parse_unit(s: &str) -> Result<Unit> {
|
||||||
|
@ -169,242 +89,6 @@ fn parse_unit(s: &str) -> Result<Unit> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TransformOptions {
|
|
||||||
from: Transform,
|
|
||||||
to: Transform,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Transform {
|
|
||||||
unit: Unit,
|
|
||||||
}
|
|
||||||
|
|
||||||
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> {
|
|
||||||
match (s, u) {
|
|
||||||
(None, _) => Ok(i),
|
|
||||||
(Some((raw_suffix, false)), &Unit::Auto) | (Some((raw_suffix, false)), &Unit::Si) => {
|
|
||||||
match raw_suffix {
|
|
||||||
RawSuffix::K => Ok(i * 1e3),
|
|
||||||
RawSuffix::M => Ok(i * 1e6),
|
|
||||||
RawSuffix::G => Ok(i * 1e9),
|
|
||||||
RawSuffix::T => Ok(i * 1e12),
|
|
||||||
RawSuffix::P => Ok(i * 1e15),
|
|
||||||
RawSuffix::E => Ok(i * 1e18),
|
|
||||||
RawSuffix::Z => Ok(i * 1e21),
|
|
||||||
RawSuffix::Y => Ok(i * 1e24),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(Some((raw_suffix, false)), &Unit::Iec(false))
|
|
||||||
| (Some((raw_suffix, true)), &Unit::Auto)
|
|
||||||
| (Some((raw_suffix, true)), &Unit::Iec(true)) => match raw_suffix {
|
|
||||||
RawSuffix::K => Ok(i * IEC_BASES[1]),
|
|
||||||
RawSuffix::M => Ok(i * IEC_BASES[2]),
|
|
||||||
RawSuffix::G => Ok(i * IEC_BASES[3]),
|
|
||||||
RawSuffix::T => Ok(i * IEC_BASES[4]),
|
|
||||||
RawSuffix::P => Ok(i * IEC_BASES[5]),
|
|
||||||
RawSuffix::E => Ok(i * IEC_BASES[6]),
|
|
||||||
RawSuffix::Z => Ok(i * IEC_BASES[7]),
|
|
||||||
RawSuffix::Y => Ok(i * IEC_BASES[8]),
|
|
||||||
},
|
|
||||||
(_, _) => Err("This suffix is unsupported for specified unit".to_owned()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transform_from(s: &str, opts: &Transform) -> Result<f64> {
|
|
||||||
let (i, suffix) = parse_suffix(s)?;
|
|
||||||
|
|
||||||
remove_suffix(i, suffix, &opts.unit).map(|n| if n < 0.0 { -n.abs().ceil() } else { n.ceil() })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Divide numerator by denominator, with ceiling.
|
|
||||||
///
|
|
||||||
/// If the result of the division is less than 10.0, truncate the result
|
|
||||||
/// to the next highest tenth.
|
|
||||||
///
|
|
||||||
/// Otherwise, truncate the result to the next highest whole number.
|
|
||||||
///
|
|
||||||
/// # Examples:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use uu_numfmt::div_ceil;
|
|
||||||
///
|
|
||||||
/// assert_eq!(div_ceil(1.01, 1.0), 1.1);
|
|
||||||
/// assert_eq!(div_ceil(999.1, 1000.), 1.0);
|
|
||||||
/// assert_eq!(div_ceil(1001., 10.), 101.);
|
|
||||||
/// assert_eq!(div_ceil(9991., 10.), 1000.);
|
|
||||||
/// assert_eq!(div_ceil(-12.34, 1.0), -13.0);
|
|
||||||
/// assert_eq!(div_ceil(1000.0, -3.14), -319.0);
|
|
||||||
/// assert_eq!(div_ceil(-271828.0, -271.0), 1004.0);
|
|
||||||
/// ```
|
|
||||||
pub fn div_ceil(n: f64, d: f64) -> f64 {
|
|
||||||
let v = n / (d / 10.0);
|
|
||||||
let (v, sign) = if v < 0.0 { (v.abs(), -1.0) } else { (v, 1.0) };
|
|
||||||
|
|
||||||
if v < 100.0 {
|
|
||||||
v.ceil() / 10.0 * sign
|
|
||||||
} else {
|
|
||||||
(v / 10.0).ceil() * sign
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn consider_suffix(n: f64, u: &Unit) -> Result<(f64, Option<Suffix>)> {
|
|
||||||
use RawSuffix::*;
|
|
||||||
|
|
||||||
let abs_n = n.abs();
|
|
||||||
let suffixes = [K, M, G, T, P, E, Z, Y];
|
|
||||||
|
|
||||||
let (bases, with_i) = match *u {
|
|
||||||
Unit::Si => (&SI_BASES, false),
|
|
||||||
Unit::Iec(with_i) => (&IEC_BASES, with_i),
|
|
||||||
Unit::Auto => return Err("Unit 'auto' isn't supported with --to options".to_owned()),
|
|
||||||
Unit::None => return Ok((n, None)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let i = match abs_n {
|
|
||||||
_ if abs_n <= bases[1] - 1.0 => return Ok((n, None)),
|
|
||||||
_ if abs_n < bases[2] => 1,
|
|
||||||
_ if abs_n < bases[3] => 2,
|
|
||||||
_ if abs_n < bases[4] => 3,
|
|
||||||
_ if abs_n < bases[5] => 4,
|
|
||||||
_ if abs_n < bases[6] => 5,
|
|
||||||
_ if abs_n < bases[7] => 6,
|
|
||||||
_ if abs_n < bases[8] => 7,
|
|
||||||
_ if abs_n < bases[9] => 8,
|
|
||||||
_ => return Err("Number is too big and unsupported".to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let v = div_ceil(n, bases[i]);
|
|
||||||
|
|
||||||
// check if rounding pushed us into the next base
|
|
||||||
if v.abs() >= bases[1] {
|
|
||||||
Ok((v / bases[1], Some((suffixes[i], with_i))))
|
|
||||||
} else {
|
|
||||||
Ok((v, Some((suffixes[i - 1], with_i))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transform_to(s: f64, opts: &Transform) -> Result<String> {
|
|
||||||
let (i2, s) = consider_suffix(s, &opts.unit)?;
|
|
||||||
Ok(match s {
|
|
||||||
None => format!("{}", i2),
|
|
||||||
Some(s) if i2.abs() < 10.0 => format!("{:.1}{}", i2, DisplayableSuffix(s)),
|
|
||||||
Some(s) => format!("{:.0}{}", i2, DisplayableSuffix(s)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_string(
|
|
||||||
source: &str,
|
|
||||||
options: &NumfmtOptions,
|
|
||||||
implicit_padding: Option<isize>,
|
|
||||||
) -> Result<String> {
|
|
||||||
let number = transform_to(
|
|
||||||
transform_from(source, &options.transform.from)?,
|
|
||||||
&options.transform.to,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(match implicit_padding.unwrap_or(options.padding) {
|
|
||||||
p if p == 0 => number,
|
|
||||||
p if p > 0 => format!("{:>padding$}", number, padding = p as usize),
|
|
||||||
p => format!("{:<padding$}", number, padding = p.abs() as usize),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_and_print(s: &str, options: &NumfmtOptions) -> Result<()> {
|
|
||||||
for (n, (prefix, field)) in (1..).zip(WhitespaceSplitter { s: Some(s) }) {
|
|
||||||
let field_selected = uucore::ranges::contain(&options.fields, n);
|
|
||||||
|
|
||||||
if field_selected {
|
|
||||||
let empty_prefix = prefix.is_empty();
|
|
||||||
|
|
||||||
// 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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_options(args: &ArgMatches) -> Result<NumfmtOptions> {
|
fn parse_options(args: &ArgMatches) -> Result<NumfmtOptions> {
|
||||||
let from = parse_unit(args.value_of(options::FROM).unwrap())?;
|
let from = parse_unit(args.value_of(options::FROM).unwrap())?;
|
||||||
let to = parse_unit(args.value_of(options::TO).unwrap())?;
|
let to = parse_unit(args.value_of(options::TO).unwrap())?;
|
||||||
|
@ -444,39 +128,23 @@ fn parse_options(args: &ArgMatches) -> Result<NumfmtOptions> {
|
||||||
None => unreachable!(),
|
None => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let delimiter = args.value_of(options::DELIMITER).map_or(Ok(None), |arg| {
|
||||||
|
if arg.len() == 1 {
|
||||||
|
Ok(Some(arg.to_string()))
|
||||||
|
} else {
|
||||||
|
Err("the delimiter must be a single character".to_string())
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(NumfmtOptions {
|
Ok(NumfmtOptions {
|
||||||
transform,
|
transform,
|
||||||
padding,
|
padding,
|
||||||
header,
|
header,
|
||||||
fields,
|
fields,
|
||||||
|
delimiter,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_args<'a>(args: impl Iterator<Item = &'a str>, options: NumfmtOptions) -> Result<()> {
|
|
||||||
for l in args {
|
|
||||||
format_and_print(l, &options)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_stdin(options: NumfmtOptions) -> Result<()> {
|
|
||||||
let stdin = std::io::stdin();
|
|
||||||
let locked_stdin = stdin.lock();
|
|
||||||
|
|
||||||
let mut lines = locked_stdin.lines();
|
|
||||||
for l in lines.by_ref().take(options.header) {
|
|
||||||
l.map(|s| println!("{}", s)).map_err(|e| e.to_string())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
for l in lines {
|
|
||||||
l.map_err(|e| e.to_string())
|
|
||||||
.and_then(|l| format_and_print(&l, &options))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn uumain(args: impl uucore::Args) -> i32 {
|
pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
let usage = get_usage();
|
let usage = get_usage();
|
||||||
|
|
||||||
|
@ -486,6 +154,13 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
.usage(&usage[..])
|
.usage(&usage[..])
|
||||||
.after_help(LONG_HELP)
|
.after_help(LONG_HELP)
|
||||||
.setting(AppSettings::AllowNegativeNumbers)
|
.setting(AppSettings::AllowNegativeNumbers)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name(options::DELIMITER)
|
||||||
|
.short("d")
|
||||||
|
.long(options::DELIMITER)
|
||||||
|
.value_name("X")
|
||||||
|
.help("use X instead of whitespace for field delimiter"),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(options::FIELD)
|
Arg::with_name(options::FIELD)
|
||||||
.long(options::FIELD)
|
.long(options::FIELD)
|
||||||
|
|
27
src/uu/numfmt/src/options.rs
Normal file
27
src/uu/numfmt/src/options.rs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
use crate::units::Transform;
|
||||||
|
use uucore::ranges::Range;
|
||||||
|
|
||||||
|
pub const DELIMITER: &str = "delimiter";
|
||||||
|
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";
|
||||||
|
pub const HEADER_DEFAULT: &str = "1";
|
||||||
|
pub const NUMBER: &str = "NUMBER";
|
||||||
|
pub const PADDING: &str = "padding";
|
||||||
|
pub const TO: &str = "to";
|
||||||
|
pub const TO_DEFAULT: &str = "none";
|
||||||
|
|
||||||
|
pub struct TransformOptions {
|
||||||
|
pub from: Transform,
|
||||||
|
pub to: Transform,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NumfmtOptions {
|
||||||
|
pub transform: TransformOptions,
|
||||||
|
pub padding: isize,
|
||||||
|
pub header: usize,
|
||||||
|
pub fields: Vec<Range>,
|
||||||
|
pub delimiter: Option<String>,
|
||||||
|
}
|
67
src/uu/numfmt/src/units.rs
Normal file
67
src/uu/numfmt/src/units.rs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
pub const SI_BASES: [f64; 10] = [1., 1e3, 1e6, 1e9, 1e12, 1e15, 1e18, 1e21, 1e24, 1e27];
|
||||||
|
|
||||||
|
pub const IEC_BASES: [f64; 10] = [
|
||||||
|
1.,
|
||||||
|
1_024.,
|
||||||
|
1_048_576.,
|
||||||
|
1_073_741_824.,
|
||||||
|
1_099_511_627_776.,
|
||||||
|
1_125_899_906_842_624.,
|
||||||
|
1_152_921_504_606_846_976.,
|
||||||
|
1_180_591_620_717_411_303_424.,
|
||||||
|
1_208_925_819_614_629_174_706_176.,
|
||||||
|
1_237_940_039_285_380_274_899_124_224.,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub type WithI = bool;
|
||||||
|
|
||||||
|
pub enum Unit {
|
||||||
|
Auto,
|
||||||
|
Si,
|
||||||
|
Iec(WithI),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Transform {
|
||||||
|
pub unit: Unit,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, String>;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum RawSuffix {
|
||||||
|
K,
|
||||||
|
M,
|
||||||
|
G,
|
||||||
|
T,
|
||||||
|
P,
|
||||||
|
E,
|
||||||
|
Z,
|
||||||
|
Y,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Suffix = (RawSuffix, WithI);
|
||||||
|
|
||||||
|
pub struct DisplayableSuffix(pub Suffix);
|
||||||
|
|
||||||
|
impl fmt::Display for DisplayableSuffix {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let DisplayableSuffix((ref raw_suffix, ref with_i)) = *self;
|
||||||
|
match raw_suffix {
|
||||||
|
RawSuffix::K => write!(f, "K"),
|
||||||
|
RawSuffix::M => write!(f, "M"),
|
||||||
|
RawSuffix::G => write!(f, "G"),
|
||||||
|
RawSuffix::T => write!(f, "T"),
|
||||||
|
RawSuffix::P => write!(f, "P"),
|
||||||
|
RawSuffix::E => write!(f, "E"),
|
||||||
|
RawSuffix::Z => write!(f, "Z"),
|
||||||
|
RawSuffix::Y => write!(f, "Y"),
|
||||||
|
}
|
||||||
|
.and_then(|()| match with_i {
|
||||||
|
true => write!(f, "i"),
|
||||||
|
false => Ok(()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -383,3 +383,98 @@ fn test_field_df_example() {
|
||||||
.succeeds()
|
.succeeds()
|
||||||
.stdout_is_fixture("df_expected.txt");
|
.stdout_is_fixture("df_expected.txt");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delimiter_must_not_be_empty() {
|
||||||
|
new_ucmd!().args(&["-d"]).fails();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delimiter_must_not_be_more_than_one_character() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["--delimiter", "sad"])
|
||||||
|
.fails()
|
||||||
|
.stderr_is("numfmt: the delimiter must be a single character");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delimiter_only() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["-d", ","])
|
||||||
|
.pipe_in("1234,56")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_only("1234,56\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_line_is_field_with_no_delimiter() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["-d,", "--to=iec"])
|
||||||
|
.pipe_in("123456")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_only("121K\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delimiter_to_si() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["-d=,", "--to=si"])
|
||||||
|
.pipe_in("1234,56")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_only("1.3K,56\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delimiter_skips_leading_whitespace() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["-d=,", "--to=si"])
|
||||||
|
.pipe_in(" \t 1234,56")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_only("1.3K,56\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delimiter_preserves_leading_whitespace_in_unselected_fields() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["-d=|", "--to=si"])
|
||||||
|
.pipe_in(" 1000| 2000")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_only("1.0K| 2000\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delimiter_from_si() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["-d=,", "--from=si"])
|
||||||
|
.pipe_in("1.2K,56")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_only("1200,56\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delimiter_overrides_whitespace_separator() {
|
||||||
|
// GNU numfmt reports this as “invalid suffix”
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["-d,"])
|
||||||
|
.pipe_in("1 234,56")
|
||||||
|
.fails()
|
||||||
|
.stderr_is("numfmt: invalid number: ‘1 234’\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delimiter_with_padding() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["-d=|", "--to=si", "--padding=5"])
|
||||||
|
.pipe_in("1000|2000")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_only(" 1.0K|2000\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delimiter_with_padding_and_fields() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["-d=|", "--to=si", "--padding=5", "--field=-"])
|
||||||
|
.pipe_in("1000|2000")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_only(" 1.0K| 2.0K\n");
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue