canboat 9.1.0

NMEA 2000 decoding, encoding and live-bus access: the CANboat PGN database as a library, plus the canboat CLI (convert / interface / server / tui).
Documentation
// (C) 2009-2026, Kees Verruijt, Harlingen, The Netherlands.

//! The `canboat` command-line tool: the subcommand modules, [`app`] (the
//! clap tree and dispatch that the binary's `main` forwards to), and the
//! plumbing they share. Compiled only with the `cli` feature; none of it
//! is part of the library's public surface.
//!
//! [`canboat_argv`] is an argv pre-processor that lets clap accept
//! canboat's single-dash long options (`-json`, `-rx`, `-fixtime`, …).
//! canboat's C tools use that form throughout; clap defaults to
//! requiring a double-dash for any option longer than one character.
//! Each entry point calls `canboat_argv()` to normalise its argv before
//! handing it to `Cli::parse_from`.
//!
//! [`shape`] holds the decoded-output flags every command that emits
//! analyzer JSON shares — `--id`, `--units`, `--wrap` — so `convert`,
//! the `analyzer` shim and `server` can't drift apart.

use std::env;
use std::ffi::OsString;

pub mod app;
pub mod protocol;
pub mod quirk;
pub mod shape;

mod convert;
pub mod device;
mod format_message;
pub mod help;
mod interface;
mod legacy;
mod replay;
mod stop_signal;
mod tui;

pub use quirk::QuirkArgs;
pub use shape::ShapeArgs;

/// The canboat copyright line and schema version, generated by `keel`
/// from `database/`. Every command shows them in `--help` (via clap
/// `after_help`) and logs them at startup when verbose logging is on.
pub use crate::engine::{CANBOAT_JSON_VERSION, COPYRIGHT_ID};

/// `--help` footer shared by all binaries: embedded schema version +
/// the canboat copyright line.
pub fn help_footer() -> String {
    format!("canboat.json v{CANBOAT_JSON_VERSION}\n{COPYRIGHT_ID}")
}

/// Log the standard startup banner at info level: binary name and
/// version, embedded canboat.json version, copyright. Callers pass
/// `env!("CARGO_PKG_NAME")` / `env!("CARGO_PKG_VERSION")` so the
/// values come from the caller.
pub fn log_startup(name: &str, version: &str) {
    log::info!("{name} v{version} (canboat.json v{CANBOAT_JSON_VERSION}) {COPYRIGHT_ID}");
}

/// Where a command writes its output. Line by line when the input is
/// live (a gateway, or a pipe or a terminal on stdin), as the C programs
/// flush every message: a reader down the pipe gets each one as it
/// comes, not once a buffer fills, and a stop by signal loses none.
/// Buffered for a file.
pub(crate) fn stdout(live: bool) -> Box<dyn std::io::Write> {
    let out = std::io::stdout().lock();
    if live {
        // `Stdout` itself is line-buffered.
        Box::new(out)
    } else {
        Box::new(std::io::BufWriter::new(out))
    }
}

/// Whether stdin is live: anything but a regular file.
pub(crate) fn stdin_is_live() -> bool {
    #[cfg(unix)]
    {
        use std::os::fd::AsFd;
        if let Ok(fd) = std::io::stdin().as_fd().try_clone_to_owned() {
            return !std::fs::File::from(fd)
                .metadata()
                .is_ok_and(|m| m.is_file());
        }
    }
    true
}

/// Read the process argv and translate any single-dash long options
/// to double-dash form. This lets users invoke a canboat-rs tool the
/// same way they'd invoke its C counterpart:
///
/// ```text
///   analyzer -json -nv -fixtime pgn-test < frames.in
///   ikonvert-serial -rx 127250,127251 -reset 30 /dev/ttyUSB0
/// ```
///
/// Rules:
///   - `-` (lone dash) is left alone — it's our stdin-as-device marker.
///   - `--` is left alone (clap uses it to stop option parsing).
///   - `-X` (a single char after the dash) is a short option, left alone.
///   - Anything longer that starts with a single dash and an alphabetic
///     character is rewritten to start with `--`. `-fixtime=foo` and
///     `-fixtime` are both translated.
///   - Bare negative numbers (`-42`) are left alone.
///
/// Returns an iterator yielding `OsString`s, suitable for
/// `Cli::parse_from`.
pub fn canboat_argv() -> Vec<OsString> {
    normalize(env::args_os())
}

/// Public for unit tests; identical to [`canboat_argv`] but takes an
/// arbitrary input iterator instead of `env::args_os()`.
pub fn normalize<I: IntoIterator<Item = OsString>>(args: I) -> Vec<OsString> {
    args.into_iter().map(rewrite_one).collect()
}

fn rewrite_one(arg: OsString) -> OsString {
    let Some(s) = arg.to_str() else {
        return arg;
    };
    // `--anything` or `-` or `--` or `-X` (X = one char): leave alone.
    if !s.starts_with('-') || s.starts_with("--") {
        return arg;
    }
    if s.len() <= 2 {
        return arg;
    }
    // First char after the dash must be alphabetic for this to be a
    // long-form option. Skip `-42`, `-0.5`, etc.
    let mut chars = s[1..].chars();
    let Some(c) = chars.next() else {
        return arg;
    };
    if !c.is_ascii_alphabetic() {
        return arg;
    }
    OsString::from(format!("-{s}"))
}

#[cfg(test)]
mod tests {
    use super::*;

    fn norm(input: &[&str]) -> Vec<String> {
        normalize(input.iter().map(|s| OsString::from(*s)))
            .into_iter()
            .map(|s| s.to_string_lossy().into_owned())
            .collect()
    }

    #[test]
    fn translates_canboat_long_options() {
        assert_eq!(
            norm(&["analyzer", "-json", "-nv", "-fixtime", "pgn-test"]),
            vec!["analyzer", "--json", "--nv", "--fixtime", "pgn-test"]
        );
    }

    #[test]
    fn leaves_short_options_alone() {
        assert_eq!(
            norm(&["bin", "-r", "-w", "-d"]),
            vec!["bin", "-r", "-w", "-d"]
        );
    }

    #[test]
    fn leaves_double_dash_alone() {
        assert_eq!(
            norm(&["bin", "--rx", "12,34", "--"]),
            vec!["bin", "--rx", "12,34", "--"]
        );
    }

    #[test]
    fn leaves_single_dash_alone() {
        // `-` is our stdin marker.
        assert_eq!(norm(&["bin", "-"]), vec!["bin", "-"]);
    }

    #[test]
    fn leaves_negative_numbers_alone() {
        assert_eq!(norm(&["bin", "-42", "-0.5"]), vec!["bin", "-42", "-0.5"]);
    }

    #[test]
    fn translates_with_equals() {
        assert_eq!(norm(&["bin", "-fixtime=foo"]), vec!["bin", "--fixtime=foo"]);
    }
}