use std::ffi::OsString;
use std::io::{self, Write};
use std::path::PathBuf;
use anyhow::{Context, Result};
use clap::Parser;
use crate::engine::{
format::InputFormat,
output::{GeoFormat, JsonOptions, TextOptions, write_json, write_text},
};
use crate::io::analyze::{self, Config};
#[derive(Debug, Parser)]
#[command(
name = "analyzer",
about = "Decode canboat PLAIN/FAST lines from stdin into text or JSON",
version,
after_help = crate::cli::help_footer()
)]
struct Cli {
#[arg(long, value_name = "PATH")]
file: Option<PathBuf>,
#[arg(long)]
json: bool,
#[arg(long)]
empty: bool,
#[arg(long)]
nv: bool,
#[arg(long)]
debug: bool,
#[arg(long, value_name = "FMT", default_value = "dd")]
geo: String,
#[command(flatten)]
shape: crate::cli::ShapeArgs,
#[command(flatten)]
quirk: crate::cli::QuirkArgs,
#[arg(long, value_name = "STRING")]
fixtime: Option<String>,
#[arg(long, value_name = "N")]
src: Option<u8>,
#[arg(long, value_name = "N")]
dst: Option<u8>,
#[arg(long, value_name = "NAME")]
format: Option<String>,
#[arg(long)]
ignore_checksum: bool,
#[command(flatten)]
protocol: crate::cli::protocol::ProtocolArgs,
#[arg(value_name = "PGN")]
pgn: Option<u32>,
}
fn parse_format_flag(name: &str) -> Result<InputFormat> {
Ok(match name.to_ascii_lowercase().as_str() {
"plain" | "fast" | "plain_or_fast" => InputFormat::Plain,
"plain_mix_fast" | "plain-mix-fast" => InputFormat::PlainMixFast,
"actisense" | "actisense-ascii" | "actisense_n2k_ascii" => InputFormat::ActisenseAscii,
"ydwg02" | "yden" => InputFormat::Ydwg02,
"ikonvert" => InputFormat::Ikonvert,
"airmar" => InputFormat::Airmar,
"seasmart" | "chetco" => InputFormat::SeaSmart,
"mxpgn" => InputFormat::Mxpgn,
"actisense_mxpgn" | "actisense-mxpgn" => InputFormat::ActisenseMxpgn,
"garmin" | "garmin-csv" | "garmin_csv1" => InputFormat::GarminCsv,
"garmin-csv2" | "garmin_csv2" => InputFormat::GarminCsv2,
"candump" => InputFormat::Candump,
other => anyhow::bail!("unknown --format {other:?}"),
})
}
pub fn run(argv: Vec<OsString>) -> Result<()> {
run_cli(Cli::parse_from(argv))
}
fn run_cli(cli: Cli) -> Result<()> {
cli.shape.warn_deprecated();
cli.quirk.apply();
let units = cli.shape.units();
let protocol = cli.protocol.resolve()?;
let json_opts = JsonOptions {
include_empty: cli.empty,
name_value: cli.nv,
debug: cli.debug,
camel_case: cli.shape.camel_case(),
wrap: cli.shape.wrap(),
protocol,
};
let geo = match cli.geo.as_str() {
"dd" => GeoFormat::Dd,
"dm" => GeoFormat::Dm,
"dms" => GeoFormat::Dms,
other => anyhow::bail!("--geo must be one of dd, dm, dms (got {other:?})"),
};
let text_opts = TextOptions {
show_unavailable: cli.empty,
debug: cli.debug,
geo,
};
let mut out = crate::cli::stdout(cli.file.is_none() && crate::cli::stdin_is_live());
let mut line_buf = String::with_capacity(512);
let suppress_banner = cli.fixtime.as_deref().is_some_and(|s| !s.contains("n2kd"));
if cli.json && !suppress_banner {
writeln!(
out,
"{}",
crate::build_info::version_banner(cli.shape.is_si(), cli.nv, protocol)
)
.context("writing JSON banner")?;
}
let forced_format = cli.format.as_deref().map(parse_format_flag).transpose()?;
let cfg = Config {
forced_format,
pgn_filter: cli.pgn,
src_filter: cli.src,
dst_filter: cli.dst,
suppress_startup_record: cli.fixtime.as_deref().is_some_and(|s| !s.contains("n2kd")),
units,
protocol,
fixed_time: cli.fixtime.as_deref(),
ignore_checksum: cli.ignore_checksum,
};
let mut sink_err: Option<anyhow::Error> = None;
let sink = |decoded: &crate::engine::DecodedPgn| {
if sink_err.is_some() {
return;
}
line_buf.clear();
if cli.json {
write_json(&mut line_buf, decoded, &json_opts).expect("write to String");
} else {
write_text(&mut line_buf, decoded, &text_opts).expect("write to String");
}
if let Err(e) = out
.write_all(line_buf.as_bytes())
.and_then(|()| out.write_all(b"\n"))
{
sink_err = Some(anyhow::Error::new(e).context("writing line"));
}
};
let result = if let Some(path) = cli.file.as_deref() {
analyze::decode_file(path, &cfg, sink)
} else {
let stdin = io::stdin();
analyze::decode_stream(stdin.lock(), &cfg, sink)
};
if let Some(e) = sink_err {
return Err(e);
}
result.map_err(Into::into)
}