//! `h5rs ls`: list a file's objects like h5ls. use std::collections::HashMap; use clawhdf5_format::attribute::AttributeMessage; use clawhdf5_format::dataspace::DataspaceType; use clawhdf5_format::object_header::ObjectHeader; use crate::cli::{Args, Out}; use crate::h5::{H5, Kind, Link, LinkKind, split_file_arg}; use crate::info::{self, DsInfo}; use crate::value::{self, Decoder}; pub const USAGE: &str = "\ usage: h5rs ls [-r] [-v] [--max-bytes N] FILE[/OBJECT] List the objects in FILE (or in the group OBJECT), one per line, like h5ls: name, kind, shape and, for datasets, the datatype. -r, --recursive list every object below, with full paths -v, --verbose also print address, link count, layout, chunking, storage, filters, datatype and attributes --max-bytes N largest attribute value decoded for -v (default 1 GiB) Exit status: 0 listed, 1 some object could not be described, 2 error."; struct Ls<'a> { h5: &'a H5, verbose: bool, kinds: HashMap, problems: usize, } pub fn run(args: &mut Args, out: &mut Out) -> std::io::Result { let mut recursive = false; let mut verbose = false; let mut max_bytes = None; let mut target = None; while let Some(a) = args.next() { match a.as_str() { "-r" | "--recursive" => recursive = true, "-v" | "--verbose" => verbose = true, "-rv" | "-vr" => { recursive = true; verbose = true; } "--max-bytes" => match args.number() { Some(n) => max_bytes = Some(n), None => return args.usage_error(out, "--max-bytes needs a number", USAGE), }, "-h" | "--help" => { writeln!(out.o, "{USAGE}")?; return Ok(0); } s if s.starts_with('-') && s.len() > 1 => { return args.usage_error(out, &format!("unknown option {a}"), USAGE); } _ if target.is_none() => target = Some(a), _ => return args.usage_error(out, &format!("unexpected argument {a}"), USAGE), } } let Some(target) = target else { return args.usage_error(out, "missing FILE", USAGE); }; let (file, obj) = split_file_arg(&target); let mut h5 = match H5::open(std::path::Path::new(&file)) { Ok(h) => h, Err(e) => { writeln!(out.e, "h5rs ls: {e}")?; return Ok(2); } }; if let Some(m) = max_bytes { h5.max_bytes = m; } let mut ls = Ls { h5: &h5, verbose, kinds: HashMap::new(), problems: 0, }; let obj_path = obj.unwrap_or_else(|| "/".into()); let addr = match h5.resolve(&obj_path) { Ok(a) => a, Err(e) => { writeln!(out.e, "h5rs ls: {obj_path}: not found: {e}")?; return Ok(2); } }; let header = match h5.header(addr) { Ok(h) => h, Err(e) => { writeln!(out.e, "h5rs ls: {obj_path}: {e}")?; return Ok(2); } }; let is_root = obj_path.trim_matches('/').is_empty(); if Kind::of(&header) != Kind::Group && !is_root { // h5ls names a single object by its base name, or with -r by its // path as given (without the leading slash). let name = if recursive { obj_path.trim_start_matches('/').to_string() } else { obj_path.rsplit('/').next().unwrap_or(&obj_path).to_string() }; ls.object_line(out, &name, &obj_path, addr, &Ok(header))?; } else if recursive { let start = if is_root { "/" } else { "" }; let mut io_err = None; let res = h5.walk_from(addr, start, &mut |item| { if io_err.is_some() || (item.link.is_none() && !is_root) { return; // listing a group's contents, not the group } let path = if item.path.is_empty() { "/" } else { item.path }; let full = if is_root { path.to_string() } else { format!("{}{path}", obj_path.trim_end_matches('/')) }; if let Err(e) = ls.item( out, path, &full, item.link, item.addr, item.first_path, item.header, ) { io_err = Some(e); } }); if let Some(e) = io_err { return Err(e); } if let Err(e) = res { writeln!(out.e, "h5rs ls: {e}")?; ls.problems += 1; } } else { match h5.links(&header) { Ok(links) => { for l in &links { let (a, hdr) = match l.kind { LinkKind::Hard(a) => (Some(a), Some(h5.header(a))), _ => (None, None), }; let full = format!("{}/{}", obj_path.trim_end_matches('/'), l.name); ls.item(out, &l.name, &full, Some(l), a, None, hdr.as_ref())?; } } Err(e) => { writeln!(out.e, "h5rs ls: {obj_path}: cannot list group: {e}")?; ls.problems += 1; } } } Ok(if ls.problems > 0 { 1 } else { 0 }) } impl Ls<'_> { #[allow(clippy::too_many_arguments)] fn item( &mut self, out: &mut Out, name: &str, full: &str, link: Option<&Link>, addr: Option, first: Option<&str>, header: Option<&crate::h5::Result>, ) -> std::io::Result<()> { if let Some(l) = link { match &l.kind { LinkKind::Soft(t) => return writeln!(out.o, "{name:<24} Soft Link {{{t}}}"), LinkKind::External { file, path } => { return writeln!(out.o, "{name:<24} External Link {{{file}/{path}}}"); } LinkKind::UserDefined(t) => { return writeln!(out.o, "{name:<24} User-defined link (type {t})"); } LinkKind::Hard(_) => {} } } let Some(addr) = addr else { return Ok(()) }; if let Some(first) = first { let k = self.kinds.get(&addr).copied().unwrap_or(Kind::Unknown); return writeln!(out.o, "{name:<24} {}, same as {first}", k.name()); } match header { Some(h) => self.object_line(out, name, full, addr, h), None => Ok(()), } } fn object_line( &mut self, out: &mut Out, name: &str, full: &str, addr: u64, header: &crate::h5::Result, ) -> std::io::Result<()> { let h = match header { Ok(h) => h, Err(e) => { self.problems += 1; writeln!(out.o, "{name:<24} ** error **")?; return writeln!(out.e, "h5rs ls: {name}: {e}"); } }; let kind = Kind::of(h); self.kinds.insert(addr, kind); match kind { Kind::Dataset => { let info = DsInfo::read(self.h5, full, h); let shape = match &info.ds { Ok(ds) => info::shape_text(ds, self.verbose), Err(_) => "{?}".into(), }; if self.verbose { writeln!(out.o, "{name:<24} Dataset {shape}")?; } else { let t = match &info.dt { Ok(dt) => crate::dtype::short(dt), Err(_) => "?".into(), }; writeln!(out.o, "{name:<24} Dataset {shape} {t}")?; } for e in [ info.dt.as_ref().err(), info.ds.as_ref().err(), info.layout.as_ref().err(), info.filters.as_ref().err(), ] .into_iter() .flatten() { self.problems += 1; writeln!(out.e, "h5rs ls: {name}: {e}")?; } if self.verbose { self.dataset_details(out, addr, h, &info)?; } } k => { writeln!(out.o, "{name:<24} {}", k.name())?; if self.verbose { writeln!(out.o, " Address: {addr}")?; writeln!(out.o, " Links: {}", info::link_count(h))?; if k == Kind::Datatype && let Ok(dt) = self.h5.datatype(h) { writeln!(out.o, " Type: {}", crate::dtype::long(&dt))?; } self.attributes(out, name, h)?; } } } Ok(()) } fn dataset_details( &mut self, out: &mut Out, addr: u64, h: &ObjectHeader, info: &DsInfo, ) -> std::io::Result<()> { writeln!(out.o, " Address: {addr}")?; writeln!(out.o, " Links: {}", info::link_count(h))?; if let Ok(l) = &info.layout { let idx = info::chunk_index_name(l); if idx.is_empty() { writeln!(out.o, " Layout: {}", info::layout_name(l))?; } else { writeln!( out.o, " Layout: {} ({idx} index)", info::layout_name(l) )?; } if let clawhdf5_format::data_layout::DataLayout::Chunked { chunk_dimensions, .. } = l { let rank = chunk_dimensions.len().saturating_sub(1); let dims = &chunk_dimensions[..rank]; let esize = info .dt .as_ref() .map(|d| u64::from(d.type_size())) .unwrap_or(0); let bytes = dims .iter() .try_fold(esize, |a, &d| a.checked_mul(u64::from(d))) .map(|b| b.to_string()) .unwrap_or_else(|| "?".into()); writeln!( out.o, " Chunks: {{{}}} {bytes} bytes", dims.iter() .map(|d| d.to_string()) .collect::>() .join(", ") )?; } } if info.external { writeln!(out.o, " External: raw data in external files")?; } let logical = info.logical_bytes(); match (logical, info::allocated_bytes(self.h5, info)) { (Some(l), Ok(a)) => { if a > 0 { writeln!( out.o, " Storage: {l} logical bytes, {a} allocated bytes, {:.2}% utilization", l as f64 * 100.0 / a as f64 )?; } else { writeln!(out.o, " Storage: {l} logical bytes, 0 allocated bytes")?; } } (_, Err(e)) => { self.problems += 1; writeln!(out.e, "h5rs ls: {e}")?; } _ => {} } if let Ok(Some(p)) = &info.filters { for (i, f) in p.filters.iter().enumerate() { writeln!(out.o, " Filter-{i}: {}", info::filter_text(f))?; } } if let Ok(dt) = &info.dt { writeln!(out.o, " Type: {}", crate::dtype::long(dt))?; } self.attributes(out, "", h) } fn attributes(&mut self, out: &mut Out, name: &str, h: &ObjectHeader) -> std::io::Result<()> { let (attrs, errs) = match self.h5.attributes(h) { Ok(x) => x, Err(e) => { self.problems += 1; return writeln!(out.e, "h5rs ls: {name}: {e}"); } }; for e in errs { self.problems += 1; writeln!(out.e, "h5rs ls: {name}: attribute: {e}")?; } for a in &attrs { self.attribute(out, a)?; } Ok(()) } fn attribute(&mut self, out: &mut Out, a: &AttributeMessage) -> std::io::Result<()> { let shape = match a.dataspace.space_type { DataspaceType::Scalar => "scalar".to_string(), DataspaceType::Null => "null".to_string(), DataspaceType::Simple => info::shape_text(&a.dataspace, false), }; writeln!(out.o, " Attribute: {} {shape}", a.name)?; writeln!( out.o, " Type: {}", crate::dtype::long(&a.datatype) )?; let n = crate::h5::num_elements(&a.dataspace).unwrap_or(0); if n == 0 { return Ok(()); } let dec = Decoder::new(self.h5); let shown = n.min(8) as usize; let mut vals = Vec::with_capacity(shown); for i in 0..shown { let v = dec.element(&a.datatype, &a.raw_data, i); vals.push(value::text(&v, &|_| None)); } let more = if n > 8 { ", ..." } else { "" }; writeln!(out.o, " Data: {}{more}", vals.join(", ")) } }