cbor-cli 0.7.0

Encode, decode, and inspect CBOR (RFC 8949) from the command line. Convert between CBOR, JSON, YAML, and TOML via serde, with streaming support for CBOR sequences.
Documentation
use std::{
  fs::File,
  io::{self, BufReader, Write},
  process,
};

use anyhow::Context;
use cbor_cli::{
  config::{self, Commands},
  export::export_from_reader,
  files::{get_format_by_file_extension, get_missing_files},
  import::import_from_reader,
  inspect::inspect_from_reader,
};

fn files_exist_or_exit(input_paths: &[std::path::PathBuf]) {
  let missing_files = get_missing_files(input_paths);
  if !missing_files.is_empty() {
    for missing_file in missing_files {
      eprintln!("File does not exist: {}", missing_file.display());
    }
    process::exit(1);
  }
}

/// Interpret `\n`, `\t`, `\r`, `\0`, `\\` escape sequences in a delimiter string.
fn unescape_delimiter(s: &str) -> String {
  let mut out = String::with_capacity(s.len());
  let mut chars = s.chars();
  while let Some(c) = chars.next() {
    if c == '\\' {
      match chars.next() {
        Some('n') => out.push('\n'),
        Some('t') => out.push('\t'),
        Some('r') => out.push('\r'),
        Some('0') => out.push('\0'),
        Some(other) => out.push(other),
        None => out.push('\\'),
      }
    } else {
      out.push(c);
    }
  }
  out
}

fn main() -> anyhow::Result<()> {
  let cli = config::get_cli();

  match &cli.command {
    Some(Commands::Inspect { input_paths }) => {
      files_exist_or_exit(input_paths);

      if input_paths.is_empty() {
        let reader = BufReader::new(io::stdin());
        inspect_from_reader(reader, io::stdout())?;
        return Ok(());
      }

      for input_path in input_paths {
        write!(io::stdout(), "{}", input_path.display())?;
        let reader = BufReader::new(File::open(input_path)
          .with_context(|| format!("Failed to open {}", input_path.display()))?);
        inspect_from_reader(reader, io::stdout())
          .with_context(|| format!("Failed to inspect {}", input_path.display()))?;
      }
    }

    Some(Commands::Import { input_paths, format }) => {
      files_exist_or_exit(input_paths);

      if input_paths.is_empty() {
        let input_format = match format {
          Some(format) => format.to_owned(),
          None => {
            eprintln!("Error: No format specified. Use --format to specify the format.");
            process::exit(1);
          }
        };

        let reader = BufReader::new(io::stdin());
        import_from_reader(&input_format, reader, &mut io::stdout())?;
        return Ok(());
      }

      for input_path in input_paths {
        let input_format = match format {
          Some(format) => format.to_owned(),
          None => match get_format_by_file_extension(input_path) {
            Some(format) => format,
            None => {
              eprintln!("Error: Could not determine format from file extension");
              process::exit(1);
            }
          },
        };

        let reader = BufReader::new(File::open(input_path)
          .with_context(|| format!("Failed to open {}", input_path.display()))?);
        import_from_reader(&input_format, reader, &mut io::stdout())
          .with_context(|| format!("Failed to import {}", input_path.display()))?;
      }
    }

    Some(Commands::Export { input_paths, format, pretty }) => {
      files_exist_or_exit(input_paths);

      let delimiter = match &cli.delimiter {
        Some(delimiter) => unescape_delimiter(delimiter),
        None => {
          if format == "yaml" {
            "\n---\n".to_string()
          } else {
            "\n".to_string()
          }
        }
      };

      if cli.verbose > 0 {
        eprintln!("Files to export: {input_paths:?}");
        eprintln!("Exporting to {} format", format);
      }

      if input_paths.is_empty() {
        let reader = BufReader::new(io::stdin());
        export_from_reader(format, &delimiter, *pretty, reader, &mut io::stdout())?;
        return Ok(());
      }

      for (i, input_path) in input_paths.iter().enumerate() {
        if i > 0 {
          io::stdout().write_all(delimiter.as_bytes())?;
        }
        let reader = BufReader::new(File::open(input_path)
          .with_context(|| format!("Failed to open {}", input_path.display()))?);
        export_from_reader(format, &delimiter, *pretty, reader, &mut io::stdout())
          .with_context(|| format!("Failed to export {}", input_path.display()))?;
      }
    }
    None => {}
  }

  Ok(())
}