use clap::{Parser, ValueEnum};
use miplog::{autodetect, input, output, parse as miplog_parse, Solver, SolverLog};
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
#[derive(Parser, Debug)]
#[command(
name = "miplog",
version,
about = "Parse MIP/LP solver log files into a unified, serde-serializable schema.",
long_about = None,
)]
struct Cli {
input: PathBuf,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(short, long, value_enum)]
format: Option<OutputFormat>,
#[arg(long, value_enum)]
solver: Option<SolverArg>,
#[arg(long)]
no_progress: bool,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum OutputFormat {
Summary,
Json,
JsonPretty,
JsonGz,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum SolverArg {
Gurobi,
Xpress,
Scip,
Highs,
Cplex,
Cbc,
Copt,
Optverse,
Mosek,
}
impl From<SolverArg> for Solver {
fn from(a: SolverArg) -> Self {
match a {
SolverArg::Gurobi => Solver::Gurobi,
SolverArg::Xpress => Solver::Xpress,
SolverArg::Scip => Solver::Scip,
SolverArg::Highs => Solver::Highs,
SolverArg::Cplex => Solver::Cplex,
SolverArg::Cbc => Solver::Cbc,
SolverArg::Copt => Solver::Copt,
SolverArg::Optverse => Solver::Optverse,
SolverArg::Mosek => Solver::Mosek,
}
}
}
fn main() -> ExitCode {
let cli = Cli::parse();
match run(cli) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("miplog: {e}");
ExitCode::FAILURE
}
}
}
fn run(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
let text = if cli.input.as_os_str() == "-" {
let mut s = String::new();
io::stdin().read_to_string(&mut s)?;
s
} else {
input::read_file(&cli.input)?
};
let log: SolverLog = match cli.solver {
Some(s) => miplog_parse(&text, s.into())?,
None => autodetect(&text)?,
};
let format = cli
.format
.unwrap_or_else(|| infer_format(cli.output.as_deref()));
let summary_str = || -> String {
if cli.no_progress {
format!("{}", log.summary_no_table())
} else {
format!("{log}")
}
};
match (cli.output, format) {
(Some(path), OutputFormat::JsonGz) => output::write_json_gz(&path, &log)?,
(Some(path), OutputFormat::Json) => output::write_json(&path, &log)?,
(Some(path), OutputFormat::JsonPretty) => output::write_json_pretty(&path, &log)?,
(Some(path), OutputFormat::Summary) => std::fs::write(&path, summary_str())?,
(None, OutputFormat::JsonGz) => {
return Err(
"--format json-gz requires --output (gzip to stdout is rarely useful)".into(),
);
}
(None, OutputFormat::Json) => {
serde_json::to_writer(io::stdout().lock(), &log)?;
println!();
}
(None, OutputFormat::JsonPretty) => {
serde_json::to_writer_pretty(io::stdout().lock(), &log)?;
println!();
}
(None, OutputFormat::Summary) => {
let mut out = io::stdout().lock();
write!(out, "{}", summary_str())?;
}
}
Ok(())
}
fn infer_format(out: Option<&Path>) -> OutputFormat {
let Some(p) = out else {
return OutputFormat::Summary;
};
let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("");
if name.ends_with(".json.gz") {
OutputFormat::JsonGz
} else if name.ends_with(".json") {
OutputFormat::Json
} else {
OutputFormat::Summary
}
}