mod cli;
use std::fs;
use std::io::{self, Stdout, Write};
use std::path::PathBuf;
use clap::Parser;
#[cfg(feature = "diff")]
use cli::DiffArgs;
use cli::{AnalyzeArgs, Cli, Commands, ConvertArgs, ConvertFormat, InfoArgs, OutputFormat, ParseArgs};
#[cfg(feature = "lp-solvers")]
use cli::{SolveArgs, Solver};
use lp_parser_rs::analysis::AnalysisConfig;
use lp_parser_rs::model::{Constraint, VariableType};
use lp_parser_rs::parser::parse_file;
use lp_parser_rs::problem::LpProblem;
type BoxError = Box<dyn std::error::Error>;
fn cmd_parse(args: ParseArgs, verbose: u8, quiet: bool) -> Result<(), BoxError> {
let content = parse_file(&args.file)?;
let problem = LpProblem::parse(&content)?;
if !quiet && verbose > 0 {
eprintln!("Parsed file: {}", args.file.display());
eprintln!(
"Problem: {} objectives, {} constraints, {} variables",
problem.objective_count(),
problem.constraint_count(),
problem.variable_count()
);
}
let mut writer = OutputWriter::new(args.output)?;
match args.format {
OutputFormat::Text => {
writeln!(writer, "{problem}")?;
}
#[cfg(feature = "serde")]
OutputFormat::Json => {
if args.pretty {
serde_json::to_writer_pretty(&mut writer, &problem)?;
} else {
serde_json::to_writer(&mut writer, &problem)?;
}
writeln!(writer)?;
}
#[cfg(feature = "serde")]
OutputFormat::Yaml => {
serde_yaml::to_writer(&mut writer, &problem)?;
}
}
Ok(())
}
fn cmd_info(args: &InfoArgs, verbose: u8, quiet: bool) -> Result<(), BoxError> {
let content = parse_file(&args.file)?;
let problem = LpProblem::parse(&content)?;
if !quiet && verbose > 0 {
eprintln!("Analyzing file: {}", args.file.display());
}
let mut writer = OutputWriter::new(args.output.clone())?;
match args.format {
OutputFormat::Text => {
write_info_text(&mut writer, &problem, args)?;
}
#[cfg(feature = "serde")]
OutputFormat::Json => {
let info = build_info_struct(&problem, args);
if args.pretty {
serde_json::to_writer_pretty(&mut writer, &info)?;
} else {
serde_json::to_writer(&mut writer, &info)?;
}
writeln!(writer)?;
}
#[cfg(feature = "serde")]
OutputFormat::Yaml => {
let info = build_info_struct(&problem, args);
serde_yaml::to_writer(&mut writer, &info)?;
}
}
Ok(())
}
fn cmd_analyze(args: AnalyzeArgs, verbose: u8, quiet: bool) -> Result<(), BoxError> {
let content = parse_file(&args.file)?;
let problem = LpProblem::parse(&content)?;
if !quiet && verbose > 0 {
eprintln!("Analyzing file: {}", args.file.display());
}
let config = AnalysisConfig {
large_coefficient_threshold: args.large_coeff_threshold,
small_coefficient_threshold: args.small_coeff_threshold,
large_rhs_threshold: args.large_coeff_threshold,
coefficient_ratio_threshold: args.ratio_threshold,
};
let analysis = problem.analyze_with_config(&config);
let mut writer = OutputWriter::new(args.output)?;
match args.format {
OutputFormat::Text => {
if args.issues_only {
if analysis.issues.is_empty() {
writeln!(writer, "No issues detected.")?;
} else {
writeln!(writer, "Issues Found: {}", analysis.issues.len())?;
for issue in &analysis.issues {
writeln!(writer, " {issue}")?;
}
}
} else {
write!(writer, "{analysis}")?;
}
}
#[cfg(feature = "serde")]
OutputFormat::Json => {
if args.issues_only {
if args.pretty {
serde_json::to_writer_pretty(&mut writer, &analysis.issues)?;
} else {
serde_json::to_writer(&mut writer, &analysis.issues)?;
}
} else if args.pretty {
serde_json::to_writer_pretty(&mut writer, &analysis)?;
} else {
serde_json::to_writer(&mut writer, &analysis)?;
}
writeln!(writer)?;
}
#[cfg(feature = "serde")]
OutputFormat::Yaml => {
if args.issues_only {
serde_yaml::to_writer(&mut writer, &analysis.issues)?;
} else {
serde_yaml::to_writer(&mut writer, &analysis)?;
}
}
}
Ok(())
}
fn write_info_text<W: Write>(writer: &mut W, problem: &LpProblem, args: &InfoArgs) -> Result<(), BoxError> {
writeln!(writer, "=== LP Problem Info ===")?;
if let Some(name) = problem.name() {
writeln!(writer, "Name: {name}")?;
}
writeln!(writer, "Sense: {}", problem.sense)?;
writeln!(writer, "Objectives: {}", problem.objective_count())?;
writeln!(writer, "Constraints: {}", problem.constraint_count())?;
writeln!(writer, "Variables: {}", problem.variable_count())?;
let mut binary_count = 0;
let mut integer_count = 0;
let mut continuous_count = 0;
for var in problem.variables.values() {
match var.var_type {
VariableType::Binary => binary_count += 1,
VariableType::Integer => integer_count += 1,
_ => continuous_count += 1,
}
}
writeln!(writer)?;
writeln!(writer, "Variable Types:")?;
writeln!(writer, " Continuous: {continuous_count}")?;
writeln!(writer, " Integer: {integer_count}")?;
writeln!(writer, " Binary: {binary_count}")?;
if args.objectives {
writeln!(writer)?;
writeln!(writer, "Objectives:")?;
for (name_id, obj) in &problem.objectives {
let name = problem.resolve(*name_id);
writeln!(writer, " {name}: {} terms", obj.coefficients.len())?;
}
}
if args.constraints {
writeln!(writer)?;
writeln!(writer, "Constraints:")?;
for (name_id, constr) in &problem.constraints {
let name = problem.resolve(*name_id);
match constr {
Constraint::Standard { coefficients, operator, rhs, .. } => {
writeln!(writer, " {name}: {} terms {operator} {rhs}", coefficients.len())?;
}
Constraint::SOS { sos_type, weights, .. } => {
writeln!(writer, " {name}: {sos_type} with {} variables", weights.len())?;
}
}
}
}
if args.variables {
writeln!(writer)?;
writeln!(writer, "Variables:")?;
for (name_id, var) in &problem.variables {
let name = problem.resolve(*name_id);
writeln!(writer, " {name}: {:?}", var.var_type)?;
}
}
Ok(())
}
#[cfg(feature = "serde")]
#[derive(serde::Serialize)]
struct ProblemInfo {
name: Option<String>,
sense: String,
objective_count: usize,
constraint_count: usize,
variable_count: usize,
variable_types: VariableTypeCounts,
#[serde(skip_serializing_if = "Option::is_none")]
objectives: Option<Vec<ObjectiveInfo>>,
#[serde(skip_serializing_if = "Option::is_none")]
constraints: Option<Vec<ConstraintInfo>>,
#[serde(skip_serializing_if = "Option::is_none")]
variables: Option<Vec<VariableInfo>>,
}
#[cfg(feature = "serde")]
#[derive(serde::Serialize)]
struct VariableTypeCounts {
continuous: usize,
integer: usize,
binary: usize,
}
#[cfg(feature = "serde")]
#[derive(serde::Serialize)]
struct ObjectiveInfo {
name: String,
term_count: usize,
}
#[cfg(feature = "serde")]
#[derive(serde::Serialize)]
struct ConstraintInfo {
name: String,
constraint_type: String,
details: String,
}
#[cfg(feature = "serde")]
#[derive(serde::Serialize)]
struct VariableInfo {
name: String,
var_type: String,
}
#[cfg(feature = "serde")]
fn build_info_struct(problem: &LpProblem, args: &InfoArgs) -> ProblemInfo {
let mut binary_count = 0;
let mut integer_count = 0;
let mut continuous_count = 0;
for var in problem.variables.values() {
match var.var_type {
VariableType::Binary => binary_count += 1,
VariableType::Integer => integer_count += 1,
_ => continuous_count += 1,
}
}
let objectives = if args.objectives {
Some(
problem
.objectives
.iter()
.map(|(name_id, obj)| ObjectiveInfo { name: problem.resolve(*name_id).to_string(), term_count: obj.coefficients.len() })
.collect(),
)
} else {
None
};
let constraints = if args.constraints {
Some(
problem
.constraints
.iter()
.map(|(name_id, constr)| {
let name = problem.resolve(*name_id).to_string();
match constr {
Constraint::Standard { coefficients, operator, rhs, .. } => ConstraintInfo {
name,
constraint_type: "standard".to_string(),
details: format!("{} terms {} {}", coefficients.len(), operator, rhs),
},
Constraint::SOS { sos_type, weights, .. } => ConstraintInfo {
name,
constraint_type: "sos".to_string(),
details: format!("{} with {} variables", sos_type, weights.len()),
},
}
})
.collect(),
)
} else {
None
};
let variables = if args.variables {
Some(
problem
.variables
.iter()
.map(|(name_id, var)| VariableInfo { name: problem.resolve(*name_id).to_string(), var_type: format!("{:?}", var.var_type) })
.collect(),
)
} else {
None
};
ProblemInfo {
name: problem.name().map(String::from),
sense: format!("{}", problem.sense),
objective_count: problem.objective_count(),
constraint_count: problem.constraint_count(),
variable_count: problem.variable_count(),
variable_types: VariableTypeCounts { continuous: continuous_count, integer: integer_count, binary: binary_count },
objectives,
constraints,
variables,
}
}
#[cfg(feature = "diff")]
fn cmd_diff(args: DiffArgs, verbose: u8, quiet: bool) -> Result<(), BoxError> {
use std::collections::{BTreeMap, BTreeSet, HashMap};
use lp_parser_rs::interner::NameId;
use lp_parser_rs::model::{Coefficient, ComparisonOp, Constraint};
if args.rename.len() % 2 != 0 {
return Err("--rename requires pairs of PATTERN REPLACEMENT".into());
}
let rules: Vec<(regex::Regex, String)> =
args.rename.chunks_exact(2).map(|c| Ok::<_, BoxError>((regex::Regex::new(&c[0])?, c[1].clone()))).collect::<Result<_, _>>()?;
let rewrite = |name: &str| -> String {
let mut s = name.to_string();
for (re, rep) in &rules {
s = re.replace_all(&s, rep.as_str()).into_owned();
}
s
};
let abs_tol = args.abs_tol;
let rel_tol = args.rel_tol;
let differs = |a: f64, b: f64| -> bool {
let diff = (a - b).abs();
if diff == 0.0 {
return false;
}
let scale = a.abs().max(b.abs());
diff > abs_tol && diff > rel_tol * scale
};
if !quiet && verbose > 0 {
eprintln!("Diffing {} vs {}", args.file1.display(), args.file2.display());
eprintln!("abs_tol={abs_tol} rel_tol={rel_tol} rename_rules={}", rules.len());
}
let content1 = parse_file(&args.file1)?;
let content2 = parse_file(&args.file2)?;
let p1 = LpProblem::parse(&content1)?;
let p2 = LpProblem::parse(&content2)?;
let canon_ids = |problem: &LpProblem, ids: &[NameId]| -> HashMap<String, NameId> {
ids.iter().map(|id| (rewrite(problem.resolve(*id)), *id)).collect()
};
let cvars1 = canon_ids(&p1, &p1.variables.keys().copied().collect::<Vec<_>>());
let cvars2 = canon_ids(&p2, &p2.variables.keys().copied().collect::<Vec<_>>());
let ccons1: HashMap<String, NameId> = p1.constraints.values().map(|c| (rewrite(p1.resolve(c.name())), c.name())).collect();
let ccons2: HashMap<String, NameId> = p2.constraints.values().map(|c| (rewrite(p2.resolve(c.name())), c.name())).collect();
let cobjs1 = canon_ids(&p1, &p1.objectives.keys().copied().collect::<Vec<_>>());
let cobjs2 = canon_ids(&p2, &p2.objectives.keys().copied().collect::<Vec<_>>());
let set_of = |m: &HashMap<String, NameId>| -> BTreeSet<String> { m.keys().cloned().collect() };
let vars1 = set_of(&cvars1);
let vars2 = set_of(&cvars2);
let cons1 = set_of(&ccons1);
let cons2 = set_of(&ccons2);
let objs1 = set_of(&cobjs1);
let objs2 = set_of(&cobjs2);
let vars_added: Vec<String> = vars2.difference(&vars1).cloned().collect();
let vars_removed: Vec<String> = vars1.difference(&vars2).cloned().collect();
let cons_added: Vec<String> = cons2.difference(&cons1).cloned().collect();
let cons_removed: Vec<String> = cons1.difference(&cons2).cloned().collect();
let objs_added: Vec<String> = objs2.difference(&objs1).cloned().collect();
let objs_removed: Vec<String> = objs1.difference(&objs2).cloned().collect();
let coeff_map = |problem: &LpProblem, coeffs: &[Coefficient]| -> BTreeMap<String, f64> {
coeffs.iter().map(|c| (rewrite(problem.resolve(c.name)), c.value)).collect()
};
let op_str = |op: ComparisonOp| -> &'static str {
match op {
ComparisonOp::LTE => "<=",
ComparisonOp::GTE => ">=",
ComparisonOp::LT => "<",
ComparisonOp::GT => ">",
ComparisonOp::EQ => "=",
}
};
let mut cons_modified: Vec<(String, Vec<String>)> = Vec::new();
for name in cons1.intersection(&cons2) {
let c1 = &p1.constraints[&ccons1[name]];
let c2 = &p2.constraints[&ccons2[name]];
let mut changes = Vec::new();
match (c1, c2) {
(
Constraint::Standard { coefficients: cf1, operator: op1, rhs: r1, .. },
Constraint::Standard { coefficients: cf2, operator: op2, rhs: r2, .. },
) => {
if op1 != op2 {
changes.push(format!("operator {} -> {}", op_str(*op1), op_str(*op2)));
}
if differs(*r1, *r2) {
changes.push(format!("rhs {r1} -> {r2}"));
}
let m1 = coeff_map(&p1, cf1);
let m2 = coeff_map(&p2, cf2);
let mut coef_diffs = 0usize;
for (k, v1) in &m1 {
match m2.get(k) {
Some(v2) if differs(*v1, *v2) => coef_diffs += 1,
None => coef_diffs += 1,
_ => {}
}
}
for k in m2.keys() {
if !m1.contains_key(k) {
coef_diffs += 1;
}
}
if coef_diffs > 0 {
changes.push(format!("{coef_diffs} coefficient change(s)"));
}
}
(Constraint::SOS { .. }, Constraint::SOS { .. }) => {
if c1 != c2 {
changes.push("SOS definition changed".to_string());
}
}
_ => changes.push("constraint kind changed (Standard <-> SOS)".to_string()),
}
if !changes.is_empty() {
cons_modified.push((name.clone(), changes));
}
}
let mut objs_modified: Vec<(String, Vec<String>)> = Vec::new();
for name in objs1.intersection(&objs2) {
let o1 = &p1.objectives[&cobjs1[name]];
let o2 = &p2.objectives[&cobjs2[name]];
let m1 = coeff_map(&p1, &o1.coefficients);
let m2 = coeff_map(&p2, &o2.coefficients);
let mut coef_diffs = 0usize;
for (k, v1) in &m1 {
match m2.get(k) {
Some(v2) if differs(*v1, *v2) => coef_diffs += 1,
None => coef_diffs += 1,
_ => {}
}
}
for k in m2.keys() {
if !m1.contains_key(k) {
coef_diffs += 1;
}
}
if coef_diffs > 0 {
objs_modified.push((name.clone(), vec![format!("{coef_diffs} coefficient change(s)")]));
}
}
let mut vars_type_changed: Vec<(String, String, String)> = Vec::new();
for name in vars1.intersection(&vars2) {
let t1 = &p1.variables[&cvars1[name]].var_type;
let t2 = &p2.variables[&cvars2[name]].var_type;
if t1 != t2 {
vars_type_changed.push((name.clone(), format!("{t1:?}"), format!("{t2:?}")));
}
}
let mut writer = OutputWriter::new(args.output)?;
match args.format {
OutputFormat::Text => {
writeln!(writer, "=== LP Diff ===")?;
writeln!(writer, "file1: {}", args.file1.display())?;
writeln!(writer, "file2: {}", args.file2.display())?;
writeln!(writer, "abs_tol: {abs_tol} rel_tol: {rel_tol} rename_rules: {}", rules.len())?;
writeln!(writer)?;
writeln!(writer, "Sense: {:?} -> {:?}", p1.sense, p2.sense)?;
writeln!(
writer,
"Counts: objectives {} -> {}, constraints {} -> {}, variables {} -> {}",
p1.objective_count(),
p2.objective_count(),
p1.constraint_count(),
p2.constraint_count(),
p1.variable_count(),
p2.variable_count()
)?;
writeln!(writer)?;
let limit = 50usize;
let fmt_list = |w: &mut OutputWriter, label: &str, items: &[String]| -> io::Result<()> {
writeln!(w, "{label} ({}):", items.len())?;
for name in items.iter().take(limit) {
writeln!(w, " {name}")?;
}
if items.len() > limit {
writeln!(w, " ... ({} more)", items.len() - limit)?;
}
Ok(())
};
fmt_list(&mut writer, "Variables added", &vars_added)?;
fmt_list(&mut writer, "Variables removed", &vars_removed)?;
writeln!(writer, "Variables with changed type ({}):", vars_type_changed.len())?;
for (n, a, b) in vars_type_changed.iter().take(limit) {
writeln!(writer, " {n}: {a} -> {b}")?;
}
fmt_list(&mut writer, "Constraints added", &cons_added)?;
fmt_list(&mut writer, "Constraints removed", &cons_removed)?;
writeln!(writer, "Constraints modified ({}):", cons_modified.len())?;
for (n, changes) in cons_modified.iter().take(limit) {
writeln!(writer, " {n}: {}", changes.join("; "))?;
}
if cons_modified.len() > limit {
writeln!(writer, " ... ({} more)", cons_modified.len() - limit)?;
}
fmt_list(&mut writer, "Objectives added", &objs_added)?;
fmt_list(&mut writer, "Objectives removed", &objs_removed)?;
writeln!(writer, "Objectives modified ({}):", objs_modified.len())?;
for (n, changes) in objs_modified.iter().take(limit) {
writeln!(writer, " {n}: {}", changes.join("; "))?;
}
}
#[cfg(feature = "serde")]
OutputFormat::Json | OutputFormat::Yaml => {
let summary = serde_json::json!({
"file1": args.file1.display().to_string(),
"file2": args.file2.display().to_string(),
"abs_tol": abs_tol,
"rel_tol": rel_tol,
"rename_rule_count": rules.len(),
"counts": {
"objectives": [p1.objective_count(), p2.objective_count()],
"constraints": [p1.constraint_count(), p2.constraint_count()],
"variables": [p1.variable_count(), p2.variable_count()],
},
"variables_added": vars_added,
"variables_removed": vars_removed,
"variables_type_changed": vars_type_changed,
"constraints_added": cons_added,
"constraints_removed": cons_removed,
"constraints_modified": cons_modified,
"objectives_added": objs_added,
"objectives_removed": objs_removed,
"objectives_modified": objs_modified,
});
match args.format {
OutputFormat::Json => {
if args.pretty {
serde_json::to_writer_pretty(&mut writer, &summary)?;
} else {
serde_json::to_writer(&mut writer, &summary)?;
}
writeln!(writer)?;
}
OutputFormat::Yaml => {
serde_yaml::to_writer(&mut writer, &summary)?;
}
OutputFormat::Text => unreachable!(),
}
}
}
Ok(())
}
fn cmd_convert(args: ConvertArgs, verbose: u8, quiet: bool) -> Result<(), BoxError> {
use lp_parser_rs::writer::{LpWriterOptions, write_lp_string_with_options};
let content = parse_file(&args.file)?;
let problem = LpProblem::parse(&content)?;
if !quiet && verbose > 0 {
eprintln!("Converting file: {}", args.file.display());
}
match args.format {
ConvertFormat::Lp => {
let options = LpWriterOptions {
include_problem_name: !args.no_problem_name,
max_line_length: args.max_line_length,
decimal_precision: args.precision,
include_section_spacing: !args.compact,
};
let output = write_lp_string_with_options(&problem, &options)?;
let mut writer = OutputWriter::new(args.output)?;
write!(writer, "{output}")?;
}
#[cfg(feature = "csv")]
ConvertFormat::Csv => {
use lp_parser_rs::csv::LpCsvWriter;
let dir = args.output.ok_or("CSV output requires --output directory")?;
if !dir.exists() {
fs::create_dir_all(&dir)?;
}
problem.to_csv(&dir)?;
if !quiet {
eprintln!("CSV files written to: {}", dir.display());
}
}
#[cfg(feature = "serde")]
ConvertFormat::Json => {
let mut writer = OutputWriter::new(args.output)?;
if args.pretty {
serde_json::to_writer_pretty(&mut writer, &problem)?;
} else {
serde_json::to_writer(&mut writer, &problem)?;
}
writeln!(writer)?;
}
#[cfg(feature = "serde")]
ConvertFormat::Yaml => {
let mut writer = OutputWriter::new(args.output)?;
serde_yaml::to_writer(&mut writer, &problem)?;
}
}
Ok(())
}
#[cfg(feature = "lp-solvers")]
fn cmd_solve(args: SolveArgs, verbose: u8, quiet: bool) -> Result<(), BoxError> {
use lp_parser_rs::compat::lp_solvers::ToLpSolvers;
use lp_solvers::solvers::{CbcSolver, GlpkSolver, SolverTrait, Status};
let content = parse_file(&args.file)?;
let problem = LpProblem::parse(&content)?;
if !quiet && verbose > 0 {
eprintln!("Loading problem: {}", args.file.display());
}
let compat = problem.to_lp_solvers()?;
for warning in compat.warnings() {
if !quiet {
eprintln!("Warning: {warning}");
}
}
if !quiet && verbose > 0 {
eprintln!("Solving with {:?}...", args.solver);
}
let solution = match args.solver {
Solver::Cbc => {
let solver = CbcSolver::new();
solver.run(&compat)?
}
Solver::Glpk => {
let solver = GlpkSolver::new();
solver.run(&compat)?
}
Solver::Gurobi | Solver::Cplex => {
return Err(
format!("{:?} solver requires commercial license - use 'cbc' or 'glpk' for open-source alternatives", args.solver).into()
);
}
};
let mut writer = OutputWriter::new(args.output)?;
match args.format {
OutputFormat::Text => {
writeln!(writer, "=== Solution ===")?;
writeln!(writer, "Status: {:?}", solution.status)?;
match solution.status {
Status::Optimal | Status::SubOptimal => {
writeln!(writer)?;
writeln!(writer, "Variables:")?;
for (name, value) in &solution.results {
writeln!(writer, " {name} = {value}")?;
}
}
Status::TimeLimit | Status::MipGap | Status::Infeasible | Status::Unbounded | Status::NotSolved => {}
}
}
#[cfg(feature = "serde")]
OutputFormat::Json => {
let status_str = match solution.status {
Status::Optimal => "optimal",
Status::SubOptimal => "suboptimal",
Status::Infeasible => "infeasible",
Status::Unbounded => "unbounded",
Status::NotSolved => "not_solved",
Status::TimeLimit => "time_limit",
Status::MipGap => "mip_gap",
};
let solution_json = serde_json::json!({
"status": status_str,
"variables": solution.results
});
if args.pretty {
serde_json::to_writer_pretty(&mut writer, &solution_json)?;
} else {
serde_json::to_writer(&mut writer, &solution_json)?;
}
writeln!(writer)?;
}
#[cfg(feature = "serde")]
OutputFormat::Yaml => {
let status_str = match solution.status {
Status::Optimal => "optimal",
Status::SubOptimal => "suboptimal",
Status::Infeasible => "infeasible",
Status::Unbounded => "unbounded",
Status::NotSolved => "not_solved",
Status::TimeLimit => "time_limit",
Status::MipGap => "mip_gap",
};
let solution_yaml = serde_json::json!({
"status": status_str,
"variables": solution.results
});
serde_yaml::to_writer(&mut writer, &solution_yaml)?;
}
}
Ok(())
}
enum OutputWriter {
Stdout(Stdout),
File(fs::File),
}
impl OutputWriter {
fn new(path: Option<PathBuf>) -> io::Result<Self> {
match path {
Some(p) => Ok(Self::File(fs::File::create(p)?)),
None => Ok(Self::Stdout(io::stdout())),
}
}
}
impl Write for OutputWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
Self::Stdout(s) => s.write(buf),
Self::File(f) => f.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
Self::Stdout(s) => s.flush(),
Self::File(f) => f.flush(),
}
}
}
fn main() -> Result<(), BoxError> {
let cli = Cli::parse();
match cli.command {
Commands::Parse(args) => cmd_parse(args, cli.verbose, cli.quiet),
Commands::Info(args) => cmd_info(&args, cli.verbose, cli.quiet),
Commands::Analyze(args) => cmd_analyze(args, cli.verbose, cli.quiet),
#[cfg(feature = "diff")]
Commands::Diff(args) => cmd_diff(args, cli.verbose, cli.quiet),
Commands::Convert(args) => cmd_convert(args, cli.verbose, cli.quiet),
#[cfg(feature = "lp-solvers")]
Commands::Solve(args) => cmd_solve(args, cli.verbose, cli.quiet),
}
}