jaarg 0.2.0

It can parse your arguments you should use it it's called jaarg
Documentation
/* bin2c - jaarg example application
 * SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
 * SPDX-License-Identifier: MIT OR Apache-2.0
 */

use jaarg::{Opt, Opts, ParseControl, ParseResult};
use std::fs::File;
use std::io::Write;
use std::io::{BufRead, BufReader, Seek, SeekFrom};
use std::path::PathBuf;
use std::process::ExitCode;

/// Strip disallowed characters from a C preprocessor label
fn sanitise_label(ident: &str) -> String {
  let mut out = String::new();
  out.reserve(ident.len());
  // Prevent leading underscore
  let mut last = '_';
  for mut i in ident.chars() {
    if !out.is_empty() || !i.is_ascii_digit() {
      if !i.is_alphanumeric() {
        i = '_';
      }
      if i != '_' || last != '_' {
        out.push(i);
      }
      last = i;
    }
  }
  // Prevent trailing underscore
  if last == '_' {
    out.pop();
  }
  out
}

/// Turn filename into an include guard label
fn guard_name(name: &str) -> String {
  let mut out = "BIN2H_".to_owned();
  out.reserve(name.len());
  out.extend(sanitise_label(name).chars().flat_map(|c| c.to_uppercase()));
  // Ensure guard ends with _H
  if !out.ends_with("_H") {
    out += "_H";
  }
  out
}

/// If the job is for a plain text file or a binary file
enum JobType {
  Binary,
  Text,
}

/// Structure for reading jobs, containing the path and type of job
struct Job {
  job_type: JobType,
  path: PathBuf
}

struct Arguments {
  out: PathBuf,
  whitespace: String,
}

impl Default for Arguments {
  fn default() -> Self {
    Self {
      out: PathBuf::new(),
      whitespace: "\t".into(),
    }
  }
}

/// Write an array from a binary file
fn bin2h(name: &str, mut file: File, out: &mut File, whitespace: &str) -> std::io::Result<()> {
  let ident = sanitise_label(name);

  // Write length
  let length = file.seek(SeekFrom::End(0))?;
  file.seek(SeekFrom::Start(0))?;
  writeln!(out, "#define {}_SIZE {}", ident.to_uppercase(), length)?;

  // Write signature
  writeln!(out, "static const unsigned char {ident}[{length}] = {{")?;

  // Write values
  let mut reader = BufReader::with_capacity(16, file);
  let mut first_line = true;
  loop {
    // Get the next row of bytes
    let bytes = reader.fill_buf()?;
    let bytes_len = bytes.len();
    if bytes.is_empty() {
      writeln!(out)?;
      break;
    }

    // Terminate the previous row
    if first_line {
      first_line = false;
    } else {
      writeln!(out, ",")?;
    }

    // Write row as hex bytes
    for (col, byte) in bytes.iter().enumerate() {
      let prefix = if col == 0 { whitespace } else { ", " };
      write!(out, "{prefix}0x{byte:02X}")?;
    }
    reader.consume(bytes_len);
  }

  // Write array terminator
  writeln!(out, "}};")?;
  Ok(())
}

/// Write a C-string from a plain text file
fn txt2h(name: &str, file: File, out: &mut File, whitespace: &str) -> std::io::Result<()> {
  let ident = sanitise_label(name);

  // Write signature
  writeln!(out, "static const char* const {ident} =")?;

  // Write lines
  let mut reader = BufReader::new(file);
  let mut line = String::new();
  let mut first_line = true;
  loop {
    if reader.read_line(&mut line)? == 0 {
      // End of file
      writeln!(out, ";")?;
      break;
    }

    // Separate lines
    if first_line {
      first_line = false;
    } else {
      writeln!(out)?;
    }

    // Write line
    write!(out, "{whitespace}\"")?;
    for c in line.chars() {
      match c {
        // Escape backslash and double-quotes
        '\\' => write!(out, "\\\\")?,
        '"' => write!(out, "\\\"")?,
        // Write control codes as character escapes
        '\x07' => write!(out, "\\a")?,
        '\x08' => write!(out, "\\b")?,
        '\x0C' => write!(out, "\\f")?,
        '\n' => write!(out, "\\n")?,
        '\r' => write!(out, "\\r")?,
        '\t' => write!(out, "\\t")?,
        '\x0B' => write!(out, "\\v")?,
        // Write ASCII control codes that don't have C character escapes as hex codes
        _ if c.is_ascii_control() => write!(out, "\\x{:02X}", c as u32)?,
        // Write remaining ASCII characters verbatim
        _ if c.is_ascii() => write!(out, "{c}")?,
        // Write non-ASCII characters as unicode escapes
        ..'\u{10000}' => write!(out, "\\u{:04X}", c as u32)?,
        _ => write!(out, "\\U{:08X}", c as u32)?,
      }
    }
    write!(out, "\"")?;
    line.clear();
  }
  Ok(())
}

/// Generates and writes out a header file
fn write_h<'a, I: Iterator<Item = &'a Job>>(opt: &Arguments, jobs: I) -> std::io::Result<()> {
  let mut out = File::create(&opt.out)?;
  let guard = guard_name(&opt.out.file_name().unwrap().to_string_lossy());
  writeln!(out, "/*DO NOT EDIT")?;
  writeln!(out, " * Autogenerated by bin2h")?;
  writeln!(out, " */")?;
  writeln!(out)?;
  writeln!(out, "#ifndef {guard}")?;
  writeln!(out, "#define {guard}")?;
  writeln!(out)?;
  for job in jobs {
    let name = job.path.file_stem().unwrap().to_string_lossy();
    let file = File::open(&job.path)?;
    match job.job_type {
      JobType::Binary => bin2h(&name, file, &mut out, &opt.whitespace)?,
      JobType::Text   => txt2h(&name, file, &mut out, &opt.whitespace)?,
    }
    writeln!(out)?;
  }
  writeln!(out, "#endif/*{guard}*/")?;
  Ok(())
}

pub fn main() -> ExitCode {
  // Program arguments
  let mut arguments = Arguments::default();
  let mut jobs = vec![];

  // Read & parse arguments from the command line, store results into the above structure
  enum Arg { Out, Bin, Txt, Whitespace, Help }
  const OPTIONS: Opts<Arg> = Opts::new(&[
    Opt::help_flag(Arg::Help, &["--help", "-h"]).help_text("Show this help message and exit"),
    Opt::positional(Arg::Out, "out").help_text("Path to generated header file").required(),
    Opt::value(Arg::Bin, &["--bin", "-b"], "data.bin").help_text("Add a binary file"),
    Opt::value(Arg::Txt, &["--txt", "-t"], "text.txt").help_text("Add a text file"),
    Opt::value(Arg::Whitespace, &["--whitespace"], "\"  \"").help_text("Emitted indentation (Default: \"\\t\")"),
  ]).with_description("Convert one or more binary and text file(s) to a C header file,\n\
                       as arrays and C strings respectively.");
  match OPTIONS.parse_easy(|program_name, id, _opt, _name, arg| {
    match id {
      Arg::Out => { arguments.out = arg.into(); }
      Arg::Bin => { jobs.push(Job { job_type: JobType::Binary, path: arg.into() }); }
      Arg::Txt => { jobs.push(Job { job_type: JobType::Text, path: arg.into() }); }
      Arg::Whitespace => { arguments.whitespace = arg.into(); }
      Arg::Help => {
        OPTIONS.print_full_help(program_name);
        return Ok(ParseControl::Quit);
      }
    }
    Ok(ParseControl::Continue)
  }) {
    ParseResult::ContinueSuccess => {
      // Generate header
      match write_h(&arguments, jobs.iter()) {
        Ok(_) => ExitCode::SUCCESS,
        Err(err) => {
          eprintln!("error: {err}");
          ExitCode::FAILURE
        }
      }
    },
    ParseResult::ExitSuccess => { ExitCode::SUCCESS }
    ParseResult::ExitError   => { ExitCode::FAILURE }
  }
}