lsmem: implement --output column printing
This commit is contained in:
parent
6fbd0cd072
commit
8b8728e1df
@ -5,7 +5,8 @@
|
|||||||
|
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use clap::{crate_version, Command};
|
use clap::builder::{EnumValueParser, PossibleValue, PossibleValuesParser};
|
||||||
|
use clap::{crate_version, Command, ValueEnum};
|
||||||
use clap::{Arg, ArgAction};
|
use clap::{Arg, ArgAction};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::borrow::Borrow;
|
use std::borrow::Borrow;
|
||||||
@ -21,10 +22,13 @@ const USAGE: &str = help_usage!("lsmem.md");
|
|||||||
mod options {
|
mod options {
|
||||||
pub const ALL: &str = "all";
|
pub const ALL: &str = "all";
|
||||||
pub const BYTES: &str = "bytes";
|
pub const BYTES: &str = "bytes";
|
||||||
pub const NOHEADINGS: &str = "noheadings";
|
|
||||||
pub const JSON: &str = "json";
|
pub const JSON: &str = "json";
|
||||||
|
pub const NOHEADINGS: &str = "noheadings";
|
||||||
|
pub const OUTPUT: &str = "output";
|
||||||
|
pub const OUTPUT_ALL: &str = "output-all";
|
||||||
pub const PAIRS: &str = "pairs";
|
pub const PAIRS: &str = "pairs";
|
||||||
pub const RAW: &str = "raw";
|
pub const RAW: &str = "raw";
|
||||||
|
pub const SPLIT: &str = "split";
|
||||||
}
|
}
|
||||||
|
|
||||||
// const BUFSIZ: usize = 1024;
|
// const BUFSIZ: usize = 1024;
|
||||||
@ -36,8 +40,8 @@ const PATH_SUB_REMOVABLE: &str = "removable";
|
|||||||
const PATH_SUB_STATE: &str = "state";
|
const PATH_SUB_STATE: &str = "state";
|
||||||
const NAME_MEMORY: &str = "memory";
|
const NAME_MEMORY: &str = "memory";
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
enum Columns {
|
enum Column {
|
||||||
#[serde(rename = "RANGE")]
|
#[serde(rename = "RANGE")]
|
||||||
Range,
|
Range,
|
||||||
#[serde(rename = "SIZE")]
|
#[serde(rename = "SIZE")]
|
||||||
@ -54,55 +58,84 @@ enum Columns {
|
|||||||
Zones,
|
Zones,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ALL_COLUMNS: &[Columns] = &[
|
impl ValueEnum for Column {
|
||||||
Columns::Range,
|
fn value_variants<'a>() -> &'a [Self] {
|
||||||
Columns::Size,
|
&[
|
||||||
Columns::State,
|
Column::Range,
|
||||||
Columns::Removable,
|
Column::Size,
|
||||||
Columns::Block,
|
Column::State,
|
||||||
Columns::Node,
|
Column::Removable,
|
||||||
Columns::Zones,
|
Column::Block,
|
||||||
];
|
Column::Node,
|
||||||
|
Column::Zones,
|
||||||
impl Columns {
|
]
|
||||||
fn get_name(&self) -> String {
|
|
||||||
serde_json::to_string(self)
|
|
||||||
.unwrap()
|
|
||||||
.trim_matches('"')
|
|
||||||
.to_string()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
fn to_possible_value(&self) -> Option<PossibleValue> {
|
||||||
fn get_float_direction(&self) -> &'static str {
|
Some(PossibleValue::new(self.get_name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default columns to display if none are explicitly specified.
|
||||||
|
const DEFAULT_COLUMNS: &[Column] = &[
|
||||||
|
Column::Range,
|
||||||
|
Column::Size,
|
||||||
|
Column::State,
|
||||||
|
Column::Removable,
|
||||||
|
Column::Block,
|
||||||
|
];
|
||||||
|
/// Which columns (attributes) are possible to split memory blocks to ranges on.
|
||||||
|
const SPLIT_COLUMNS: &[Column] = &[
|
||||||
|
Column::State,
|
||||||
|
Column::Removable,
|
||||||
|
Column::Node,
|
||||||
|
Column::Zones,
|
||||||
|
];
|
||||||
|
|
||||||
|
impl Column {
|
||||||
|
fn get_name(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Columns::Range => "<",
|
Column::Range => "RANGE",
|
||||||
Columns::Size => ">",
|
Column::Size => "SIZE",
|
||||||
Columns::State => ">",
|
Column::State => "STATE",
|
||||||
Columns::Removable => ">",
|
Column::Removable => "REMOVABLE",
|
||||||
Columns::Block => ">",
|
Column::Block => "BLOCK",
|
||||||
Columns::Node => ">",
|
Column::Node => "NODE",
|
||||||
Columns::Zones => ">",
|
Column::Zones => "ZONES",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn get_width_hint(&self) -> i8 {
|
fn get_float_right(&self) -> bool {
|
||||||
if self == &Columns::Size {
|
match self {
|
||||||
|
Column::Range => false,
|
||||||
|
Column::Size => true,
|
||||||
|
Column::State => true,
|
||||||
|
Column::Removable => true,
|
||||||
|
Column::Block => true,
|
||||||
|
Column::Node => true,
|
||||||
|
Column::Zones => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn get_width_hint(&self) -> usize {
|
||||||
|
if self == &Column::Size {
|
||||||
5
|
5
|
||||||
} else {
|
} else {
|
||||||
self.get_name().len() as i8
|
self.get_name().len()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_help(&self) -> &'static str {
|
fn get_help(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Columns::Range => "start and end address of the memory range",
|
Column::Range => "start and end address of the memory range",
|
||||||
Columns::Size => "size of the memory range",
|
Column::Size => "size of the memory range",
|
||||||
Columns::State => "online status of the memory range",
|
Column::State => "online status of the memory range",
|
||||||
Columns::Removable => "memory is removable",
|
Column::Removable => "memory is removable",
|
||||||
Columns::Block => "memory block number or blocks range",
|
Column::Block => "memory block number or blocks range",
|
||||||
Columns::Node => "numa node of memory",
|
Column::Node => "numa node of memory",
|
||||||
Columns::Zones => "valid zones for the memory range",
|
Column::Zones => "valid zones for the memory range",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -200,6 +233,17 @@ struct TableRow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TableRow {
|
impl TableRow {
|
||||||
|
fn get_value(&self, column: &Column) -> String {
|
||||||
|
match column {
|
||||||
|
Column::Range => self.range.clone(),
|
||||||
|
Column::Size => self.size.clone(),
|
||||||
|
Column::State => self.state.clone(),
|
||||||
|
Column::Removable => self.removable.clone(),
|
||||||
|
Column::Block => self.block.clone(),
|
||||||
|
Column::Node => self.node.clone(),
|
||||||
|
Column::Zones => self.zones.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
fn to_pairs_string(&self) -> String {
|
fn to_pairs_string(&self) -> String {
|
||||||
format!(
|
format!(
|
||||||
r#"RANGE="{}" SIZE="{}" STATE="{}" REMOVABLE="{}" BLOCK="{}""#,
|
r#"RANGE="{}" SIZE="{}" STATE="{}" REMOVABLE="{}" BLOCK="{}""#,
|
||||||
@ -220,19 +264,24 @@ struct TableRowJson {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct Options {
|
struct Options {
|
||||||
|
// Set by command-line arguments
|
||||||
all: bool,
|
all: bool,
|
||||||
bytes: bool,
|
bytes: bool,
|
||||||
export: bool,
|
columns: Vec<Column>,
|
||||||
have_nodes: bool,
|
|
||||||
have_zones: bool,
|
|
||||||
json: bool,
|
|
||||||
list_all: bool,
|
|
||||||
noheadings: bool,
|
noheadings: bool,
|
||||||
|
json: bool,
|
||||||
|
pairs: bool,
|
||||||
raw: bool,
|
raw: bool,
|
||||||
split_by_node: bool,
|
split_by_node: bool,
|
||||||
split_by_removable: bool,
|
split_by_removable: bool,
|
||||||
split_by_state: bool,
|
split_by_state: bool,
|
||||||
split_by_zones: bool,
|
split_by_zones: bool,
|
||||||
|
|
||||||
|
// Set by read_info
|
||||||
|
have_nodes: bool,
|
||||||
|
have_zones: bool,
|
||||||
|
|
||||||
|
// Computed from flags above
|
||||||
want_summary: bool,
|
want_summary: bool,
|
||||||
want_table: bool,
|
want_table: bool,
|
||||||
}
|
}
|
||||||
@ -265,20 +314,22 @@ impl Options {
|
|||||||
fn new() -> Options {
|
fn new() -> Options {
|
||||||
Options {
|
Options {
|
||||||
all: false,
|
all: false,
|
||||||
have_nodes: false,
|
|
||||||
raw: false,
|
|
||||||
export: false,
|
|
||||||
json: false,
|
|
||||||
noheadings: false,
|
|
||||||
list_all: false,
|
|
||||||
bytes: false,
|
bytes: false,
|
||||||
|
columns: Vec::default(),
|
||||||
|
noheadings: false,
|
||||||
|
json: false,
|
||||||
|
pairs: false,
|
||||||
|
raw: false,
|
||||||
|
split_by_node: false,
|
||||||
|
split_by_removable: false,
|
||||||
|
split_by_state: false,
|
||||||
|
split_by_zones: false,
|
||||||
|
|
||||||
|
have_nodes: false,
|
||||||
|
have_zones: false,
|
||||||
|
|
||||||
want_summary: true, // default true
|
want_summary: true, // default true
|
||||||
want_table: true, // default true
|
want_table: true, // default true
|
||||||
split_by_node: false,
|
|
||||||
split_by_state: false,
|
|
||||||
split_by_removable: false,
|
|
||||||
split_by_zones: false,
|
|
||||||
have_zones: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -321,7 +372,7 @@ fn read_info(lsmem: &mut Lsmem, opts: &mut Options) {
|
|||||||
} else {
|
} else {
|
||||||
lsmem.mem_offline += lsmem.block_size;
|
lsmem.mem_offline += lsmem.block_size;
|
||||||
}
|
}
|
||||||
if !opts.all && is_mergeable(lsmem, opts, &blk) {
|
if is_mergeable(lsmem, opts, &blk) {
|
||||||
lsmem.blocks[lsmem.nblocks - 1].count += 1;
|
lsmem.blocks[lsmem.nblocks - 1].count += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -349,7 +400,7 @@ fn is_mergeable(lsmem: &Lsmem, opts: &Options, blk: &MemoryBlock) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let curr_block = &lsmem.blocks[lsmem.nblocks - 1];
|
let curr_block = &lsmem.blocks[lsmem.nblocks - 1];
|
||||||
if opts.list_all {
|
if opts.all {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if curr_block.index + curr_block.count != blk.index {
|
if curr_block.index + curr_block.count != blk.index {
|
||||||
@ -482,44 +533,53 @@ fn create_table_rows(lsmem: &Lsmem, opts: &Options) -> Vec<TableRow> {
|
|||||||
|
|
||||||
fn print_table(lsmem: &Lsmem, opts: &Options) {
|
fn print_table(lsmem: &Lsmem, opts: &Options) {
|
||||||
let table_rows = create_table_rows(lsmem, opts);
|
let table_rows = create_table_rows(lsmem, opts);
|
||||||
let mut col_widths = vec![5, 5, 6, 9, 5]; // Initialize with default minimum widths
|
let mut col_widths = vec![0; opts.columns.len()];
|
||||||
|
|
||||||
|
// Initialize column widths based on column names
|
||||||
|
for (i, column) in opts.columns.iter().enumerate() {
|
||||||
|
col_widths[i] = column.get_width_hint();
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate minimum column widths based on the actual data
|
// Calculate minimum column widths based on the actual data
|
||||||
for row in &table_rows {
|
for row in &table_rows {
|
||||||
col_widths[0] = col_widths[0].max(row.range.len());
|
for (i, column) in opts.columns.iter().enumerate() {
|
||||||
col_widths[1] = col_widths[1].max(row.size.len());
|
let width = match column {
|
||||||
col_widths[2] = col_widths[2].max(row.state.len());
|
Column::Range => row.range.len(),
|
||||||
col_widths[3] = col_widths[3].max(row.removable.len());
|
Column::Size => row.size.len(),
|
||||||
col_widths[4] = col_widths[4].max(row.block.len());
|
Column::State => row.state.len(),
|
||||||
|
Column::Removable => row.removable.len(),
|
||||||
|
Column::Block => row.block.len(),
|
||||||
|
Column::Node => row.node.len(),
|
||||||
|
Column::Zones => row.zones.len(),
|
||||||
|
};
|
||||||
|
col_widths[i] = col_widths[i].max(width);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !opts.noheadings {
|
if !opts.noheadings {
|
||||||
println!(
|
let mut output = vec![];
|
||||||
"{:<col0$} {:>col1$} {:>col2$} {:>col3$} {:>col4$}",
|
for (i, column) in opts.columns.iter().enumerate() {
|
||||||
"RANGE",
|
let formatted = if column.get_float_right() {
|
||||||
"SIZE",
|
format!("{:>width$}", column.get_name(), width = col_widths[i])
|
||||||
"STATE",
|
} else {
|
||||||
"REMOVABLE",
|
format!("{:<width$}", column.get_name(), width = col_widths[i])
|
||||||
"BLOCK",
|
};
|
||||||
col0 = col_widths[0],
|
output.push(formatted);
|
||||||
col1 = col_widths[1],
|
}
|
||||||
col2 = col_widths[2],
|
println!("{}", output.join(" "));
|
||||||
col3 = col_widths[3],
|
|
||||||
col4 = col_widths[4],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for row in table_rows {
|
for row in table_rows {
|
||||||
let mut columns = vec![];
|
let mut output = vec![];
|
||||||
columns.push(format!("{:<col0$}", row.range, col0 = col_widths[0]));
|
for (i, column) in opts.columns.iter().enumerate() {
|
||||||
columns.push(format!("{:>col1$}", row.size, col1 = col_widths[1]));
|
let formatted = if column.get_float_right() {
|
||||||
columns.push(format!("{:>col2$}", row.state, col2 = col_widths[2]));
|
format!("{:>width$}", row.get_value(column), width = col_widths[i])
|
||||||
columns.push(format!("{:>col3$}", row.removable, col3 = col_widths[3]));
|
} else {
|
||||||
columns.push(format!("{:>col4$}", row.block, col4 = col_widths[4]));
|
format!("{:<width$}", row.get_value(column), width = col_widths[i])
|
||||||
// Default version skips NODE and ZONES
|
};
|
||||||
// columns.push(format!("{:>col5$}", row.node, col5 = col_widths[5]));
|
output.push(formatted);
|
||||||
// columns.push(format!("{:>col6$}", row.zones, col6 = col_widths[6]));
|
}
|
||||||
println!("{}", columns.join(" "));
|
println!("{}", output.join(" "));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -597,10 +657,35 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
|||||||
opts.bytes = matches.get_flag(options::BYTES);
|
opts.bytes = matches.get_flag(options::BYTES);
|
||||||
opts.noheadings = matches.get_flag(options::NOHEADINGS);
|
opts.noheadings = matches.get_flag(options::NOHEADINGS);
|
||||||
opts.json = matches.get_flag(options::JSON);
|
opts.json = matches.get_flag(options::JSON);
|
||||||
opts.export = matches.get_flag(options::PAIRS);
|
opts.pairs = matches.get_flag(options::PAIRS);
|
||||||
opts.raw = matches.get_flag(options::RAW);
|
opts.raw = matches.get_flag(options::RAW);
|
||||||
|
opts.columns = matches
|
||||||
|
.get_many::<Column>(options::OUTPUT)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.map(|c| c.to_owned())
|
||||||
|
.collect::<Vec<Column>>();
|
||||||
|
|
||||||
if opts.json || opts.export || opts.raw {
|
// Only respect --output-all if no column list were provided.
|
||||||
|
// --output takes priority over --output-all.
|
||||||
|
if opts.columns.is_empty() {
|
||||||
|
if matches.get_flag(options::OUTPUT_ALL) {
|
||||||
|
opts.columns = Column::value_variants().to_vec();
|
||||||
|
} else {
|
||||||
|
opts.columns = DEFAULT_COLUMNS.to_vec();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let split_columns = matches
|
||||||
|
.get_many::<String>(options::SPLIT)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.map(|c| c.to_owned())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
opts.split_by_node = split_columns.contains(&Column::Node.get_name().to_string());
|
||||||
|
opts.split_by_removable = split_columns.contains(&Column::Removable.get_name().to_string());
|
||||||
|
opts.split_by_state = split_columns.contains(&Column::State.get_name().to_string());
|
||||||
|
opts.split_by_zones = split_columns.contains(&Column::Zones.get_name().to_string());
|
||||||
|
|
||||||
|
if opts.json || opts.pairs || opts.raw {
|
||||||
opts.want_summary = false;
|
opts.want_summary = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -609,7 +694,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
|||||||
if opts.want_table {
|
if opts.want_table {
|
||||||
if opts.json {
|
if opts.json {
|
||||||
print_json(&lsmem, &opts);
|
print_json(&lsmem, &opts);
|
||||||
} else if opts.export {
|
} else if opts.pairs {
|
||||||
print_pairs(&lsmem, &opts);
|
print_pairs(&lsmem, &opts);
|
||||||
} else if opts.raw {
|
} else if opts.raw {
|
||||||
print_raw(&lsmem, &opts);
|
print_raw(&lsmem, &opts);
|
||||||
@ -636,27 +721,6 @@ pub fn uu_app() -> Command {
|
|||||||
.about(ABOUT)
|
.about(ABOUT)
|
||||||
.override_usage(format_usage(USAGE))
|
.override_usage(format_usage(USAGE))
|
||||||
.infer_long_args(true)
|
.infer_long_args(true)
|
||||||
.arg(
|
|
||||||
Arg::new(options::ALL)
|
|
||||||
.short('a')
|
|
||||||
.long("all")
|
|
||||||
.help("list each individual memory block")
|
|
||||||
.action(ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new(options::BYTES)
|
|
||||||
.short('b')
|
|
||||||
.long("bytes")
|
|
||||||
.help("print SIZE in bytes rather than in human readable format")
|
|
||||||
.action(ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new(options::NOHEADINGS)
|
|
||||||
.short('n')
|
|
||||||
.long("noheadings")
|
|
||||||
.help("don't print headings")
|
|
||||||
.action(ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(options::JSON)
|
Arg::new(options::JSON)
|
||||||
.short('J')
|
.short('J')
|
||||||
@ -673,6 +737,44 @@ pub fn uu_app() -> Command {
|
|||||||
.action(ArgAction::SetTrue)
|
.action(ArgAction::SetTrue)
|
||||||
.conflicts_with_all([options::JSON, options::RAW]),
|
.conflicts_with_all([options::JSON, options::RAW]),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new(options::ALL)
|
||||||
|
.short('a')
|
||||||
|
.long("all")
|
||||||
|
.help("list each individual memory block")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
|
.conflicts_with(options::SPLIT),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new(options::BYTES)
|
||||||
|
.short('b')
|
||||||
|
.long("bytes")
|
||||||
|
.help("print SIZE in bytes rather than in human readable format")
|
||||||
|
.action(ArgAction::SetTrue),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new(options::NOHEADINGS)
|
||||||
|
.short('n')
|
||||||
|
.long("noheadings")
|
||||||
|
.help("don't print headings")
|
||||||
|
.action(ArgAction::SetTrue),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new(options::OUTPUT)
|
||||||
|
.short('o')
|
||||||
|
.long("output")
|
||||||
|
.help("output columns")
|
||||||
|
.ignore_case(true)
|
||||||
|
.action(ArgAction::Set)
|
||||||
|
.value_delimiter(',')
|
||||||
|
.value_parser(EnumValueParser::<Column>::new()),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new(options::OUTPUT_ALL)
|
||||||
|
.long("output-all")
|
||||||
|
.help("output all columns")
|
||||||
|
.action(ArgAction::SetTrue),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(options::RAW)
|
Arg::new(options::RAW)
|
||||||
.short('r')
|
.short('r')
|
||||||
@ -681,9 +783,25 @@ pub fn uu_app() -> Command {
|
|||||||
.action(ArgAction::SetTrue)
|
.action(ArgAction::SetTrue)
|
||||||
.conflicts_with_all([options::JSON, options::PAIRS]),
|
.conflicts_with_all([options::JSON, options::PAIRS]),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new(options::SPLIT)
|
||||||
|
.short('S')
|
||||||
|
.long("split")
|
||||||
|
.help("split ranges by specified columns")
|
||||||
|
.conflicts_with(options::ALL)
|
||||||
|
.ignore_case(true)
|
||||||
|
.action(ArgAction::Set)
|
||||||
|
.value_delimiter(',')
|
||||||
|
.value_parser(PossibleValuesParser::new(
|
||||||
|
SPLIT_COLUMNS
|
||||||
|
.iter()
|
||||||
|
.map(|col| col.to_possible_value().unwrap())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)),
|
||||||
|
)
|
||||||
.after_help(&format!(
|
.after_help(&format!(
|
||||||
"Available output columns:\n{}",
|
"Available output columns:\n{}",
|
||||||
ALL_COLUMNS
|
Column::value_variants()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|col| format!("{:>11} {}", col.get_name(), col.get_help()))
|
.map(|col| format!("{:>11} {}", col.get_name(), col.get_help()))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user