#[cfg(feature = "watch")]
use clap::{Parser, Subcommand, ValueEnum};
#[cfg(not(feature = "watch"))]
use clap::{Parser, ValueEnum};
use colored::*;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use simple_logger::SimpleLogger;
use std::collections::HashSet;
use std::env;
use std::path::{Path, PathBuf};
use terminal_size::terminal_size;
use cyme::config::Config;
use cyme::display::{self, Block, DeviceBlocks};
use cyme::error::{Error, ErrorKind, Result};
use cyme::lsusb;
use cyme::profiler;
use cyme::types::VidPid;
use cyme::usb::BaseClass;
use std::str::FromStr;
#[cfg(feature = "watch")]
mod watch;
const MAX_VERBOSITY: u8 = 4;
#[derive(Parser, Debug, Default, Serialize, Deserialize)]
#[skip_serializing_none]
#[command(author, version, about, long_about = None, max_term_width=80)]
struct Args {
#[arg(short, long, default_value_t = false)]
lsusb: bool,
#[arg(short, long, default_value_t = false)]
tree: bool,
#[arg(short = 'd', long, action = clap::ArgAction::Append, value_name = "VID:[PID]", aliases = &["filter-vidpid"])]
vidpid: Vec<VidPid>,
#[arg(short, long)]
show: Option<String>,
#[arg(short = 'D', long)]
device: Option<String>,
#[arg(long, action = clap::ArgAction::Append)]
filter_name: Vec<String>,
#[arg(long, action = clap::ArgAction::Append)]
filter_serial: Vec<String>,
#[arg(long, action = clap::ArgAction::Append)]
filter_class: Vec<BaseClass>,
#[arg(long, action = clap::ArgAction::Append, value_name = "KEY=VALUE")]
filter_exclude: Vec<String>,
#[arg(short = 'v', long, default_value_t = 0, action = clap::ArgAction::Count)]
verbose: u8,
#[arg(short, long, value_enum, value_delimiter = ',', num_args = 1..)]
blocks: Option<Vec<display::DeviceBlocks>>,
#[arg(long, value_enum, value_delimiter = ',', num_args = 1..)]
bus_blocks: Option<Vec<display::BusBlocks>>,
#[arg(long, value_enum, value_delimiter = ',', num_args = 1..)]
config_blocks: Option<Vec<display::ConfigurationBlocks>>,
#[arg(long, value_enum, value_delimiter = ',', num_args = 1..)]
interface_blocks: Option<Vec<display::InterfaceBlocks>>,
#[arg(long, value_enum, value_delimiter = ',', num_args = 1..)]
endpoint_blocks: Option<Vec<display::EndpointBlocks>>,
#[arg(long, value_enum)]
block_operation: Option<display::BlockOperation>,
#[arg(short, long, default_value_t = false)]
more: bool,
#[arg(long, value_enum)]
sort_devices: Option<display::Sort>,
#[arg(long, default_value_t = false)]
sort_buses: bool,
#[arg(long, value_enum)]
group_devices: Option<display::Group>,
#[arg(long, default_value_t = false)]
hide_buses: bool,
#[arg(long, default_value_t = false)]
hide_hubs: bool,
#[arg(long, default_value_t = false)]
list_root_hubs: bool,
#[arg(long, default_value_t = false)]
decimal: bool,
#[arg(long, default_value_t = false)]
no_padding: bool,
#[arg(long, value_enum, aliases = &["colour"])]
color: Option<display::ColorWhen>,
#[arg(long, default_value_t = false, hide = true, aliases = &["no_colour"])]
no_color: bool,
#[arg(long, value_enum)]
encoding: Option<display::Encoding>,
#[arg(long, default_value_t = false, hide = true)]
ascii: bool,
#[arg(long, default_value_t = false, hide = true)]
no_icons: bool,
#[arg(long, value_enum, aliases = &["icon_when"])]
icon: Option<display::IconWhen>,
#[arg(long, default_value_t = false)]
headings: bool,
#[arg(long, default_value_t = false, overrides_with = "lsusb")]
json: bool,
#[arg(long)]
from_json: Option<PathBuf>,
#[arg(short = 'F', long, default_value_t = false)]
force_libusb: bool,
#[arg(short = 'c', long)]
config: Option<PathBuf>,
#[arg(long, default_value_t = false, hide = true)]
filter_post: bool,
#[arg(short = 'z', long, action = clap::ArgAction::Count)]
debug: u8,
#[arg(long)]
mask_serials: Option<display::MaskSerial>,
#[arg(long, default_value_t = false)]
mute_hubs: bool,
#[cfg(feature = "cli_generate")]
#[arg(long, hide = true, exclusive = true)]
gen: bool,
#[arg(long, default_value_t = false)]
system_profiler: bool,
#[cfg(feature = "watch")]
#[command(subcommand)]
command: Option<SubCommand>,
}
#[cfg(feature = "watch")]
#[derive(Subcommand, Debug, Serialize, Deserialize)]
enum SubCommand {
Watch,
}
macro_rules! eprintexit {
($error:expr) => {
eprintln!(
"{}\n{}",
"cyme encountered a runtime error:".bold().red(),
$error.to_string().bold().red()
);
std::process::exit(1);
};
}
#[allow(unused_macros)]
macro_rules! wprintln {
($error:expr) => {
println!("{}", $error.to_string().bold().yellow());
log::warn!($error)
};
}
fn merge_config(c: &mut Config, a: &Args) {
c.lsusb |= a.lsusb;
c.tree |= a.tree;
c.more |= a.more;
c.hide_buses |= a.hide_buses;
c.hide_hubs |= a.hide_hubs;
c.list_root_hubs |= a.list_root_hubs;
c.decimal |= a.decimal;
c.no_padding |= a.no_padding;
c.ascii |= a.ascii;
c.headings |= a.headings;
c.force_libusb |= a.force_libusb;
c.no_icons |= a.no_icons;
c.no_color |= a.no_color;
c.json |= a.json;
if a.group_devices.is_some() {
c.group_devices = a.group_devices;
}
if a.encoding.is_some() {
c.encoding = a.encoding;
}
if a.sort_devices.is_some() {
c.sort_devices = a.sort_devices;
}
if a.icon.is_some() {
c.icon_when = a.icon;
}
if a.color.is_some() {
c.color_when = a.color;
}
if a.mask_serials.is_some() {
c.mask_serials = a.mask_serials;
}
if a.block_operation.is_some() {
c.block_operation = a.block_operation;
}
c.mute_hubs |= a.mute_hubs;
c.sort_buses |= a.sort_buses;
c.verbose = c.verbose.max(a.verbose);
}
fn parse_show(s: &str) -> Result<(Option<u8>, Option<u8>)> {
if s.contains(':') {
let split: Vec<&str> = s.split(':').collect();
let bus: Option<u8> = split
.first()
.filter(|v| !v.is_empty())
.map_or(Ok(None), |v| {
v.parse::<u8>()
.map(Some)
.map_err(|e| Error::new(ErrorKind::Parsing, &e.to_string()))
})?;
let device = split
.last()
.filter(|v| !v.is_empty())
.map_or(Ok(None), |v| {
v.parse::<u8>()
.map(Some)
.map_err(|e| Error::new(ErrorKind::Parsing, &e.to_string()))
})?;
Ok((bus, device))
} else {
let device: Option<u8> = s
.trim()
.parse::<u8>()
.map(Some)
.map_err(|e| Error::new(ErrorKind::Parsing, &e.to_string()))?;
Ok((None, device))
}
}
fn parse_exclude(s: &str) -> Result<profiler::Filter> {
let mut f = profiler::Filter::default();
for part in s.split(',') {
let (key, value) = part.split_once('=').ok_or_else(|| {
Error::new(
ErrorKind::InvalidArg,
&format!("Expected KEY=VALUE format in '--filter-exclude {s}'"),
)
})?;
match key.trim() {
"vidpid" => {
let vp = VidPid::from_str(value.trim())?;
f.vid = vp.0;
f.pid = vp.1;
}
"name" => f.name = Some(value.to_string()),
"serial" => f.serial = Some(value.to_string()),
"class" => {
f.class = Some(
BaseClass::from_str(value.trim(), true).map_err(|e| {
Error::new(ErrorKind::Parsing, &e.to_string())
})?,
)
}
"bus" => {
f.bus = Some(value.trim().parse::<u8>().map_err(|e| {
Error::new(ErrorKind::Parsing, &e.to_string())
})?)
}
"number" => {
f.number = Some(value.trim().parse::<u8>().map_err(|e| {
Error::new(ErrorKind::Parsing, &e.to_string())
})?)
}
_ => {
return Err(Error::new(
ErrorKind::InvalidArg,
&format!("Unknown exclude key '{key}'; expected one of: vidpid, name, serial, class, bus, number"),
))
}
}
}
Ok(f)
}
fn parse_devpath(s: &str) -> Result<(Option<u8>, Option<u8>)> {
if s.contains('/') {
let split: Vec<&str> = s.split('/').collect();
let bus: Option<u8> = split.get(split.len() - 2).map_or(Ok(None), |v| {
v.parse::<u8>()
.map(Some)
.map_err(|e| Error::new(ErrorKind::Parsing, &e.to_string()))
})?;
let device = split.last().map_or(Ok(None), |v| {
v.parse::<u8>()
.map(Some)
.map_err(|e| Error::new(ErrorKind::Parsing, &e.to_string()))
})?;
Ok((bus, device))
} else {
Err(Error::new(
ErrorKind::InvalidArg,
&format!("Invalid device path {s}"),
))
}
}
#[allow(unused_variables)]
fn is_watch(args: &Args) -> bool {
#[cfg(feature = "watch")]
{
matches!(args.command, Some(SubCommand::Watch))
}
#[cfg(not(feature = "watch"))]
{
false
}
}
#[cfg(target_os = "macos")]
fn get_system_profile_macos(
config: &Config,
args: &Args,
filter: Option<profiler::DeviceFilter>,
) -> Result<profiler::SystemProfile> {
if args.system_profiler || !cfg!(feature = "nusb") {
if !config.force_libusb
&& args.device.is_none() && args.filter_class.is_empty() && !((config.tree && config.lsusb) || config.verbose > 0 || config.more)
{
profiler::macos::get_spusb().map_or_else(
|e| {
if e.kind() == ErrorKind::SystemProfiler {
eprintln!("Failed to run 'system_profiler -json SPUSBDataType', fallback to cyme profiler; Error({e})");
get_system_profile(config, args, filter)
} else {
Err(e)
}
},
Ok,
)
} else if !config.force_libusb {
if cfg!(feature = "libusb") {
log::warn!("Merging macOS system_profiler output with libusb for verbose data. Apple internal devices will not be obtained");
}
let depth = if config.verbose >= 2
|| (config.json && config.verbose >= 1)
|| (config.lsusb && (args.device.is_some() || config.tree || config.verbose > 0))
|| is_watch(args)
{
profiler::ProfileDepth::Full
} else if config.verbose == 1
|| !args.filter_class.is_empty()
|| config.json
|| config.more
{
profiler::ProfileDepth::Standard
} else {
profiler::ProfileDepth::Identity
};
let options = profiler::ProfilerOptions {
filter,
depth,
tree: config.tree,
};
profiler::macos::get_spusb_with_options(&options).map_or_else(
|e| {
if e.kind() == ErrorKind::SystemProfiler {
eprintln!("Failed to run 'system_profiler -json SPUSBDataType', fallback to cyme profiler; Error({e})");
get_system_profile(config, args, options.filter)
} else {
Err(e)
}
},
Ok,
)
} else {
get_system_profile(config, args, filter)
}
} else {
get_system_profile(config, args, filter)
}
}
fn get_system_profile(
config: &Config,
args: &Args,
filter: Option<profiler::DeviceFilter>,
) -> Result<profiler::SystemProfile> {
let depth = if config.verbose >= 2
|| (config.json && config.verbose >= 1)
|| (config.lsusb && (args.device.is_some() || config.tree || config.verbose > 0))
|| is_watch(args)
{
profiler::ProfileDepth::Full
} else if config.verbose == 1 || !args.filter_class.is_empty() || config.json || config.more {
profiler::ProfileDepth::Standard
} else {
profiler::ProfileDepth::Identity
};
let options = profiler::ProfilerOptions {
filter,
depth,
tree: config.tree,
};
profiler::get_spusb_with_options(&options)
}
fn print_lsusb(
sp_usb: &profiler::SystemProfile,
device: &Option<String>,
settings: &display::PrintSettings,
) -> Result<()> {
if settings.tree && device.is_none() {
if !cfg!(target_os = "linux") {
log::warn!("Most of the data in a lsusb style tree is applicable to Linux only!");
}
lsusb::print_tree(sp_usb, settings)
} else {
if !(cfg!(feature = "libusb") || cfg!(feature = "nusb"))
&& (settings.verbosity > 0 || device.is_some())
{
return Err(Error::new(ErrorKind::Unsupported, "nusb or libusb feature is required to do this, install with `cargo install --features nusb/libusb`"));
}
let devices = sp_usb.flattened_devices();
if let Some(dev_path) = &device {
lsusb::dump_one_device(&devices, dev_path)?
} else {
lsusb::print(&devices, settings.verbosity > 0);
}
};
Ok(())
}
#[cfg(feature = "cli_generate")]
#[cold]
fn print_man() -> Result<()> {
use clap::CommandFactory;
use clap_complete::generate_to;
use clap_complete::shells::*;
use std::fs;
use std::path::PathBuf;
let outdir = std::env::var_os("BUILD_SCRIPT_DIR")
.or_else(|| std::env::var_os("OUT_DIR"))
.unwrap_or_else(|| "./doc".into());
fs::create_dir_all(&outdir)?;
println!("Generating CLI info to {outdir:?}");
let mut app = Args::command();
let bin_name = app.get_name().to_string();
generate_to(Bash, &mut app, &bin_name, &outdir).expect("Failed to generate Bash completions");
generate_to(Fish, &mut app, &bin_name, &outdir).expect("Failed to generate Fish completions");
generate_to(Zsh, &mut app, &bin_name, &outdir).expect("Failed to generate Zsh completions");
generate_to(PowerShell, &mut app, &bin_name, &outdir)
.expect("Failed to generate PowerShell completions");
let man = clap_mangen::Man::new(app);
let mut buffer: Vec<u8> = Default::default();
man.render(&mut buffer)?;
std::fs::write(PathBuf::from(&outdir).join("cyme.1"), buffer)?;
std::fs::write(
PathBuf::from(&outdir).join("cyme_example_config.json"),
serde_json::to_string_pretty(&Config::example())?,
)?;
std::fs::write(
PathBuf::from(&outdir).join("cyme_example_filter_config.json"),
serde_json::to_string_pretty(&Config::example_with_filter())?,
)?;
Ok(())
}
fn load_config<P: AsRef<Path>>(path: Option<P>) -> Result<Config> {
if let Some(p) = path {
let config = Config::from_file(p);
log::info!("Using user config {config:?}");
config
} else {
Config::sys()
}
}
pub fn set_log_level(debug: u8) -> Result<()> {
let mut builder = SimpleLogger::new();
let mut env_levels: HashSet<(String, log::LevelFilter)> = HashSet::new();
let global_level = match debug {
0 => {
env_levels.insert(("udevrs".to_string(), log::LevelFilter::Off));
env_levels.insert(("nusb".to_string(), log::LevelFilter::Off));
log::LevelFilter::Error
}
1 => {
env_levels.insert(("udevrs".to_string(), log::LevelFilter::Warn));
env_levels.insert(("nusb".to_string(), log::LevelFilter::Warn));
env_levels.insert(("cyme".to_string(), log::LevelFilter::Info));
log::LevelFilter::Error
}
2 => {
env_levels.insert(("udevrs".to_string(), log::LevelFilter::Info));
env_levels.insert(("nusb".to_string(), log::LevelFilter::Info));
env_levels.insert(("cyme".to_string(), log::LevelFilter::Debug));
log::LevelFilter::Error
}
3 => {
env_levels.insert(("udevrs".to_string(), log::LevelFilter::Debug));
env_levels.insert(("nusb".to_string(), log::LevelFilter::Debug));
env_levels.insert(("cyme".to_string(), log::LevelFilter::Trace));
log::LevelFilter::Error
}
_ => log::LevelFilter::Trace,
};
if let Ok(rust_log) = std::env::var("RUST_LOG") {
rust_log
.split(',')
.filter(|s| !s.is_empty())
.map(|s| {
let mut split = s.split('=');
let k = split.next().unwrap();
let v = split.next().and_then(|s| s.parse().ok());
(k.to_string(), v)
})
.filter(|(_, v)| v.is_some())
.map(|(k, v)| (k, v.unwrap()))
.for_each(|(k, v)| {
env_levels.replace((k, v));
});
}
for (k, v) in env_levels {
builder = builder.with_module_level(&k, v);
}
builder
.with_utc_timestamps()
.with_level(global_level)
.env()
.init()
.map_err(|e| {
Error::new(
ErrorKind::Other("logger"),
&format!("Failed to set log level: {e}"),
)
})?;
#[cfg(feature = "libusb")]
profiler::libusb::set_log_level(debug);
Ok(())
}
fn merge_blocks(config: &Config, args: &Args, settings: &mut display::PrintSettings) -> Result<()> {
let block_op = config.block_operation.unwrap_or_default();
if let Some(blocks) = &args.blocks {
let mut device_blocks = config.blocks.to_owned().unwrap_or(if settings.more {
DeviceBlocks::default_blocks(true)
} else if settings.tree {
DeviceBlocks::default_device_tree_blocks()
} else {
DeviceBlocks::default_blocks(false)
});
block_op.run(&mut device_blocks, blocks)?;
settings.device_blocks = Some(device_blocks);
}
if let Some(blocks) = &args.bus_blocks {
settings.bus_blocks =
Some(block_op.new_or_op(config.bus_blocks.to_owned(), blocks, settings.more)?);
}
if let Some(blocks) = &args.config_blocks {
settings.config_blocks =
Some(block_op.new_or_op(settings.config_blocks.to_owned(), blocks, settings.more)?);
}
if let Some(blocks) = &args.interface_blocks {
settings.interface_blocks = Some(block_op.new_or_op(
settings.interface_blocks.to_owned(),
blocks,
settings.more,
)?);
}
if let Some(blocks) = &args.endpoint_blocks {
settings.endpoint_blocks =
Some(block_op.new_or_op(settings.endpoint_blocks.to_owned(), blocks, settings.more)?);
}
Ok(())
}
fn build_inclusion_filters(
vidpids: &[VidPid],
bus: Option<u8>,
number: Option<u8>,
names: &[String],
serials: &[String],
classes: &[BaseClass],
) -> Vec<profiler::Filter> {
let vids: Vec<Option<VidPid>> = if vidpids.is_empty() {
vec![None]
} else {
vidpids.iter().copied().map(Some).collect()
};
let names: Vec<Option<&String>> = if names.is_empty() {
vec![None]
} else {
names.iter().map(Some).collect()
};
let serials: Vec<Option<&String>> = if serials.is_empty() {
vec![None]
} else {
serials.iter().map(Some).collect()
};
let classes: Vec<Option<BaseClass>> = if classes.is_empty() {
vec![None]
} else {
classes.iter().copied().map(Some).collect()
};
let mut filters = Vec::new();
for vid in &vids {
for name in &names {
for serial in &serials {
for class in &classes {
let (vid_val, pid_val) = vid.map_or((None, None), |v| (v.0, v.1));
filters.push(profiler::Filter {
vid: vid_val,
pid: pid_val,
bus,
number,
name: name.map(|n| n.to_owned()),
serial: serial.map(|s| s.to_owned()),
class: *class,
case_sensitive: false,
});
}
}
}
}
filters
}
fn cyme() -> Result<()> {
let mut args = Args::parse();
#[cfg(feature = "cli_generate")]
if args.gen {
print_man()?;
std::process::exit(0);
}
set_log_level(args.debug)?;
let mut config = load_config(args.config.as_deref())?;
if config.print_non_critical_profiler_stderr {
std::env::set_var("CYME_PRINT_NON_CRITICAL_PROFILER_STDERR", "1");
}
if args.ascii {
args.encoding = Some(display::Encoding::Ascii);
}
if args.no_color {
args.color = Some(display::ColorWhen::Never);
}
if args.verbose >= MAX_VERBOSITY {
args.more = true;
}
merge_config(&mut config, &args);
match config.color_when {
Some(display::ColorWhen::Always) => {
env::set_var("NO_COLOR", "0");
colored::control::set_override(true);
config.no_color = false;
}
Some(display::ColorWhen::Never) => {
env::set_var("NO_COLOR", "1");
colored::control::set_override(false);
config.no_color = true;
}
_ => (),
};
let filter = {
let vidpids = &args.vidpid;
let (bus, number) = if let Some(devpath) = &args.device {
parse_devpath(devpath.as_str()).map_err(|e| {
Error::new(
ErrorKind::InvalidArg,
&format!(
"Failed to parse devpath '{devpath}', should end with 'BUS/DEVNO'; Error({e})"
),
)
})?
} else if let Some(show) = &args.show {
parse_show(show.as_str()).map_err(|e| {
Error::new(
ErrorKind::InvalidArg,
&format!("Failed to parse show parameter '{show}'; Error({e})"),
)
})?
} else {
(None, None)
};
let exclude_filters: Vec<profiler::Filter> = args
.filter_exclude
.iter()
.map(|s| {
parse_exclude(s.as_str()).map_err(|e| {
Error::new(
ErrorKind::InvalidArg,
&format!("Failed to parse filter-exclude '{s}'; Error({e})"),
)
})
})
.collect::<Result<_>>()?;
let include_root_hubs = config.lsusb
|| config.json
|| config.list_root_hubs
|| args.device.is_some()
|| args.show.is_some();
let has_inclusion_criteria = !vidpids.is_empty()
|| !args.filter_name.is_empty()
|| !args.filter_serial.is_empty()
|| !args.filter_class.is_empty()
|| bus.is_some()
|| number.is_some();
let config_include: Vec<profiler::Filter> = config
.filter_include
.iter()
.cloned()
.map(profiler::Filter::from)
.collect();
let config_exclude: Vec<profiler::Filter> = config
.filter_exclude
.iter()
.cloned()
.map(profiler::Filter::from)
.collect();
let has_any_criteria = has_inclusion_criteria
|| !exclude_filters.is_empty()
|| config.hide_hubs
|| config.hide_buses
|| !config_include.is_empty()
|| !config_exclude.is_empty();
if has_any_criteria || cfg!(target_os = "linux") {
let mut f = profiler::DeviceFilter::default();
f.filters.extend(config_include);
if has_inclusion_criteria {
f.filters.extend(build_inclusion_filters(
vidpids,
bus,
number,
&args.filter_name,
&args.filter_serial,
&args.filter_class,
));
}
f.exclude_filters.extend(config_exclude);
f.exclude_filters.extend(exclude_filters);
f.exclude_empty_bus = config.hide_buses;
f.exclude_empty_hub = config.hide_hubs;
f.include_root_hubs = include_root_hubs;
if !f.filters.is_empty() || !f.exclude_filters.is_empty() {
log::info!(
"Device filter active: {} inclusion filter(s), {} exclusion filter(s)",
f.filters.len(),
f.exclude_filters.len()
);
}
Some(f)
} else {
None
}
};
let mut spusb = if let Some(file_path) = args.from_json.clone() {
match profiler::read_json_dump(&file_path) {
Ok(s) => s,
Err(e) => {
log::warn!(
"Failed to read json dump, attempting as flattened with phony bus: Error({e})"
);
profiler::read_flat_json_to_phony_bus(&file_path)?
}
}
} else {
#[cfg(target_os = "macos")]
{
get_system_profile_macos(
&config,
&args,
if args.filter_post {
None
} else {
filter.clone()
},
)?
}
#[cfg(not(target_os = "macos"))]
{
get_system_profile(
&config,
&args,
if args.filter_post {
None
} else {
filter.clone()
},
)?
}
};
let mut settings = config.print_settings();
settings.terminal_size = terminal_size().map(|(w, h)| (w.0, h.0));
merge_blocks(&config, &args, &mut settings)?;
log::trace!("Returned system_profiler data\n\r{spusb:#?}");
#[cfg(feature = "watch")]
if matches!(args.command, Some(SubCommand::Watch)) {
if settings.json {
watch::watch_usb_devices_json(spusb, filter, settings)?;
} else {
watch::watch_usb_devices(spusb, filter, settings, config)?;
}
return Ok(());
}
display::prepare(&mut spusb, filter.as_ref(), &settings);
if config.lsusb {
print_lsusb(&spusb, &args.device, &settings)?;
} else {
#[allow(clippy::unnecessary_unwrap)]
if args.device.is_some() && spusb.is_empty() {
return Err(Error::new(
ErrorKind::NotFound,
&format!("Unable to find device at {:?}", args.device.unwrap()),
));
}
display::print(&spusb, &settings);
}
Ok(())
}
fn main() {
cyme().unwrap_or_else(|e| {
eprintexit!(e);
});
}
#[cfg(test)]
mod tests {
use super::*;
#[ignore]
#[test]
fn test_output_args() {
let mut args = Args {
..Default::default()
};
args.blocks = Some(vec![display::DeviceBlocks::BusNumber]);
println!("{}", serde_json::to_string_pretty(&args).unwrap());
}
#[test]
fn test_parse_show() {
assert_eq!(parse_show("1").unwrap(), (None, Some(1)));
assert_eq!(parse_show("1:124").unwrap(), (Some(1), Some(124)));
assert_eq!(parse_show("1:").unwrap(), (Some(1), None));
assert!(parse_show("55233:12323").is_err());
assert!(parse_show("dfg:sdfd").is_err());
}
#[test]
fn test_parse_devpath() {
assert_eq!(
parse_devpath("/dev/bus/usb/001/003").unwrap(),
(Some(1), Some(3))
);
assert_eq!(
parse_devpath("/dev/bus/usb/004/003").unwrap(),
(Some(4), Some(3))
);
assert_eq!(
parse_devpath("/dev/bus/usb/004/3").unwrap(),
(Some(4), Some(3))
);
assert_eq!(parse_devpath("004/3").unwrap(), (Some(4), Some(3)));
assert!(parse_devpath("004/").is_err());
assert!(parse_devpath("sas/ssas").is_err());
}
}