Merge pull request #218 from alxndrv/lscpu-children
`lscpu`: Support extracting and outputting nested fields
This commit is contained in:
@ -6,7 +6,7 @@
|
|||||||
use clap::{crate_version, Arg, ArgAction, Command};
|
use clap::{crate_version, Arg, ArgAction, Command};
|
||||||
use regex::RegexBuilder;
|
use regex::RegexBuilder;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::{fs, str::FromStr};
|
use std::{cmp, fs};
|
||||||
use sysinfo::System;
|
use sysinfo::System;
|
||||||
use uucore::{error::UResult, format_usage, help_about, help_usage};
|
use uucore::{error::UResult, format_usage, help_about, help_usage};
|
||||||
|
|
||||||
@ -27,6 +27,22 @@ struct CpuInfos {
|
|||||||
struct CpuInfo {
|
struct CpuInfo {
|
||||||
field: String,
|
field: String,
|
||||||
data: String,
|
data: String,
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
children: Vec<CpuInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CpuInfo {
|
||||||
|
fn new(field: &str, data: &str, children: Option<Vec<CpuInfo>>) -> Self {
|
||||||
|
Self {
|
||||||
|
field: field.to_string(),
|
||||||
|
data: data.to_string(),
|
||||||
|
children: children.unwrap_or_default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_child(&mut self, child: Self) {
|
||||||
|
self.children.push(child);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CpuInfos {
|
impl CpuInfos {
|
||||||
@ -36,11 +52,7 @@ impl CpuInfos {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push(&mut self, field: &str, data: &str) {
|
fn push(&mut self, cpu_info: CpuInfo) {
|
||||||
let cpu_info = CpuInfo {
|
|
||||||
field: String::from_str(field).unwrap(),
|
|
||||||
data: String::from_str(data).unwrap(),
|
|
||||||
};
|
|
||||||
self.lscpu.push(cpu_info);
|
self.lscpu.push(cpu_info);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,39 +61,143 @@ impl CpuInfos {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct OutputOptions {
|
||||||
|
json: bool,
|
||||||
|
_hex: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[uucore::main]
|
#[uucore::main]
|
||||||
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
let matches: clap::ArgMatches = uu_app().try_get_matches_from(args)?;
|
let matches: clap::ArgMatches = uu_app().try_get_matches_from(args)?;
|
||||||
|
|
||||||
let system = System::new_all();
|
let system = System::new_all();
|
||||||
|
|
||||||
let _hex = matches.get_flag(options::HEX);
|
let output_opts = OutputOptions {
|
||||||
let json = matches.get_flag(options::JSON);
|
_hex: matches.get_flag(options::HEX),
|
||||||
|
json: matches.get_flag(options::JSON),
|
||||||
|
};
|
||||||
|
|
||||||
let mut cpu_infos = CpuInfos::new();
|
let mut cpu_infos = CpuInfos::new();
|
||||||
cpu_infos.push("Architecture", &get_architecture());
|
|
||||||
cpu_infos.push("CPU(s)", &format!("{}", system.cpus().len()));
|
|
||||||
// Add more CPU information here...
|
|
||||||
|
|
||||||
if let Ok(contents) = fs::read_to_string("/proc/cpuinfo") {
|
let mut arch_info = CpuInfo::new("Architecture", &get_architecture(), None);
|
||||||
let re = RegexBuilder::new(r"^model name\s+:\s+(.*)$")
|
|
||||||
|
// TODO: We just silently ignore failures to read `/proc/cpuinfo` currently and treat it as empty
|
||||||
|
// Perhaps a better solution should be put in place, but what?
|
||||||
|
let contents = fs::read_to_string("/proc/cpuinfo").unwrap_or_default();
|
||||||
|
|
||||||
|
if let Some(addr_sizes) = find_cpuinfo_value(&contents, "address sizes") {
|
||||||
|
arch_info.add_child(CpuInfo::new("Address sizes", &addr_sizes, None))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(byte_order) = fs::read_to_string("/sys/kernel/cpu_byteorder") {
|
||||||
|
match byte_order.trim() {
|
||||||
|
"big" => arch_info.add_child(CpuInfo::new("Byte Order", "Big Endian", None)),
|
||||||
|
"little" => arch_info.add_child(CpuInfo::new("Byte Order", "Little Endian", None)),
|
||||||
|
_ => eprintln!("Unrecognised Byte Order: {}", byte_order),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu_infos.push(arch_info);
|
||||||
|
cpu_infos.push(CpuInfo::new(
|
||||||
|
"CPU(s)",
|
||||||
|
&format!("{}", system.cpus().len()),
|
||||||
|
None,
|
||||||
|
));
|
||||||
|
|
||||||
|
// TODO: This is currently quite verbose and doesn't strictly respect the hierarchy of `/proc/cpuinfo` contents
|
||||||
|
// ie. the file might contain multiple sections, each with their own vendor_id/model name etc. but right now
|
||||||
|
// we're just taking whatever our regex matches first and using that
|
||||||
|
if let Some(vendor) = find_cpuinfo_value(&contents, "vendor_id") {
|
||||||
|
let mut vendor_info = CpuInfo::new("Vendor ID", &vendor, None);
|
||||||
|
|
||||||
|
if let Some(model_name) = find_cpuinfo_value(&contents, "model name") {
|
||||||
|
let mut model_name_info = CpuInfo::new("Model name", &model_name, None);
|
||||||
|
|
||||||
|
if let Some(family) = find_cpuinfo_value(&contents, "cpu family") {
|
||||||
|
model_name_info.add_child(CpuInfo::new("CPU Family", &family, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(model) = find_cpuinfo_value(&contents, "model") {
|
||||||
|
model_name_info.add_child(CpuInfo::new("Model", &model, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
vendor_info.add_child(model_name_info);
|
||||||
|
}
|
||||||
|
cpu_infos.push(vendor_info);
|
||||||
|
}
|
||||||
|
|
||||||
|
print_output(cpu_infos, output_opts);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_output(infos: CpuInfos, out_opts: OutputOptions) {
|
||||||
|
if out_opts.json {
|
||||||
|
println!("{}", infos.to_json());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn indentation(depth: usize) -> usize {
|
||||||
|
// Indentation is 2 spaces per level, used in a few places, hence its own helper function
|
||||||
|
depth * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurses down the tree of entries and find the one with the "widest" field name (taking into account tree depth)
|
||||||
|
fn get_max_field_width(info: &CpuInfo, depth: usize) -> usize {
|
||||||
|
let max_child_width = info
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.map(|entry| get_max_field_width(entry, depth + 1))
|
||||||
|
.max()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let own_width = indentation(depth) + info.field.len();
|
||||||
|
cmp::max(own_width, max_child_width)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_entries(
|
||||||
|
entries: &Vec<CpuInfo>,
|
||||||
|
depth: usize,
|
||||||
|
max_field_width: usize,
|
||||||
|
_out_opts: &OutputOptions,
|
||||||
|
) {
|
||||||
|
for entry in entries {
|
||||||
|
let margin = indentation(depth);
|
||||||
|
let padding = cmp::max(max_field_width - margin - entry.field.len(), 0);
|
||||||
|
println!(
|
||||||
|
"{}{}:{} {}",
|
||||||
|
" ".repeat(margin),
|
||||||
|
entry.field,
|
||||||
|
" ".repeat(padding),
|
||||||
|
entry.data
|
||||||
|
);
|
||||||
|
print_entries(&entry.children, depth + 1, max_field_width, _out_opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to align all values to the same column
|
||||||
|
let max_field_width = infos
|
||||||
|
.lscpu
|
||||||
|
.iter()
|
||||||
|
.map(|info| get_max_field_width(info, 0))
|
||||||
|
.max()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
print_entries(&infos.lscpu, 0, max_field_width, &out_opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_cpuinfo_value(contents: &str, key: &str) -> Option<String> {
|
||||||
|
let pattern = format!(r"^{}\s+:\s+(.*)$", key);
|
||||||
|
let re = RegexBuilder::new(pattern.as_str())
|
||||||
.multi_line(true)
|
.multi_line(true)
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// Assuming all CPUs have the same model name
|
|
||||||
if let Some(cap) = re.captures_iter(&contents).next() {
|
|
||||||
cpu_infos.push("Model name", &cap[1]);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if json {
|
let value = re
|
||||||
println!("{}", cpu_infos.to_json());
|
.captures_iter(contents)
|
||||||
} else {
|
.next()
|
||||||
for elt in cpu_infos.lscpu {
|
.map(|cap| cap[1].to_string());
|
||||||
println!("{}: {}", elt.field, elt.data);
|
value
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_architecture() -> String {
|
fn get_architecture() -> String {
|
||||||
@ -113,6 +229,7 @@ pub fn uu_app() -> Command {
|
|||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(options::JSON)
|
Arg::new(options::JSON)
|
||||||
|
.short('J')
|
||||||
.long("json")
|
.long("json")
|
||||||
.help(
|
.help(
|
||||||
"Use JSON output format for the default summary or extended output \
|
"Use JSON output format for the default summary or extended output \
|
||||||
|
@ -17,12 +17,33 @@ fn test_hex() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
fn test_json() {
|
fn test_json() {
|
||||||
new_ucmd!()
|
let res = new_ucmd!().arg("--json").succeeds();
|
||||||
.arg("--json")
|
|
||||||
.succeeds()
|
let stdout = res.no_stderr().stdout_str();
|
||||||
// ensure some fields are there, non-exhausting
|
assert!(stdout.starts_with("{"));
|
||||||
.stdout_contains("\"lscpu\": [")
|
assert!(stdout.ends_with("}\n"));
|
||||||
|
|
||||||
|
res.stdout_contains("\"lscpu\": [")
|
||||||
.stdout_contains("\"field\": \"Architecture\"")
|
.stdout_contains("\"field\": \"Architecture\"")
|
||||||
.stdout_contains("\"field\": \"CPU(s)\"");
|
.stdout_contains("\"field\": \"CPU(s)\"")
|
||||||
|
.stdout_contains("\"children\": [");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn test_output() {
|
||||||
|
let res = new_ucmd!().succeeds();
|
||||||
|
let stdout = res.no_stderr().stdout_str();
|
||||||
|
|
||||||
|
// Non-exhaustive list of fields we expect
|
||||||
|
// This also checks that fields which should be indented, are indeed indented as excepted
|
||||||
|
assert!(stdout.contains("Architecture:"));
|
||||||
|
assert!(stdout.contains("\n Address sizes:"));
|
||||||
|
assert!(stdout.contains("\n Byte Order:"));
|
||||||
|
assert!(stdout.contains("\nCPU(s):"));
|
||||||
|
assert!(stdout.contains("\nVendor ID:"));
|
||||||
|
assert!(stdout.contains("\n Model name:"));
|
||||||
|
assert!(stdout.contains("\n CPU Family:"));
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user