diff --git a/src/uu/lscpu/src/lscpu.rs b/src/uu/lscpu/src/lscpu.rs index 5dd313b..3150e53 100644 --- a/src/uu/lscpu/src/lscpu.rs +++ b/src/uu/lscpu/src/lscpu.rs @@ -6,7 +6,7 @@ use clap::{crate_version, Arg, ArgAction, Command}; use regex::RegexBuilder; use serde::Serialize; -use std::{fs, str::FromStr}; +use std::{cmp, fs}; use sysinfo::System; use uucore::{error::UResult, format_usage, help_about, help_usage}; @@ -27,6 +27,22 @@ struct CpuInfos { struct CpuInfo { field: 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 { @@ -36,11 +52,7 @@ impl CpuInfos { } } - fn push(&mut self, field: &str, data: &str) { - let cpu_info = CpuInfo { - field: String::from_str(field).unwrap(), - data: String::from_str(data).unwrap(), - }; + fn push(&mut self, cpu_info: CpuInfo) { self.lscpu.push(cpu_info); } @@ -49,41 +61,145 @@ impl CpuInfos { } } +struct OutputOptions { + json: bool, + _hex: bool, +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches: clap::ArgMatches = uu_app().try_get_matches_from(args)?; let system = System::new_all(); - let _hex = matches.get_flag(options::HEX); - let json = matches.get_flag(options::JSON); + let output_opts = OutputOptions { + _hex: matches.get_flag(options::HEX), + json: matches.get_flag(options::JSON), + }; 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 re = RegexBuilder::new(r"^model name\s+:\s+(.*)$") - .multi_line(true) - .build() - .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]); - }; + let mut arch_info = CpuInfo::new("Architecture", &get_architecture(), None); + + // 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 json { - println!("{}", cpu_infos.to_json()); - } else { - for elt in cpu_infos.lscpu { - println!("{}: {}", elt.field, elt.data); + 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) + .build() + .unwrap(); + + let value = re + .captures_iter(contents) + .next() + .map(|cap| cap[1].to_string()); + value +} + fn get_architecture() -> String { if cfg!(target_arch = "x86") { "x86".to_string() @@ -113,6 +229,7 @@ pub fn uu_app() -> Command { ) .arg( Arg::new(options::JSON) + .short('J') .long("json") .help( "Use JSON output format for the default summary or extended output \ diff --git a/tests/by-util/test_lscpu.rs b/tests/by-util/test_lscpu.rs index ca4c92e..474001d 100644 --- a/tests/by-util/test_lscpu.rs +++ b/tests/by-util/test_lscpu.rs @@ -17,12 +17,33 @@ fn test_hex() { } #[test] +#[cfg(target_os = "linux")] fn test_json() { - new_ucmd!() - .arg("--json") - .succeeds() - // ensure some fields are there, non-exhausting - .stdout_contains("\"lscpu\": [") + let res = new_ucmd!().arg("--json").succeeds(); + + let stdout = res.no_stderr().stdout_str(); + assert!(stdout.starts_with("{")); + assert!(stdout.ends_with("}\n")); + + res.stdout_contains("\"lscpu\": [") .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:")); }