lsmem: implement --output column printing

This commit is contained in:
Foorack 2025-02-08 21:37:05 +01:00
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<_>>()