Skip to main content

config_disassembler/
cli.rs

1//! Top-level command-line dispatcher.
2//!
3//! ```text
4//! config-disassembler <subcommand> [args...]
5//!
6//! Subcommands:
7//!   xml      Forward to the bundled xml-disassembler CLI.
8//!   json     Disassemble or reassemble a JSON file.
9//!   json5    Disassemble or reassemble a JSON5 file.
10//!   yaml     Disassemble or reassemble a YAML file.
11//!   help     Show this help text.
12//! ```
13
14use std::path::PathBuf;
15
16use crate::disassemble::{self, DisassembleOptions};
17use crate::error::{Error, Result};
18use crate::format::Format;
19use crate::reassemble::{self, ReassembleOptions};
20use crate::xml_cmd;
21
22/// Dispatch a full argv (including program name at `args[0]`).
23pub async fn dispatch(args: Vec<String>) -> Result<()> {
24    let mut iter = args.into_iter();
25    let _program = iter.next();
26    let subcommand = match iter.next() {
27        Some(s) => s,
28        None => {
29            print_help();
30            return Ok(());
31        }
32    };
33    let rest: Vec<String> = iter.collect();
34
35    match subcommand.as_str() {
36        "help" | "-h" | "--help" => {
37            print_help();
38            Ok(())
39        }
40        "xml" => xml_cmd::run(rest).await,
41        "json" => run_format(Format::Json, rest),
42        "json5" => run_format(Format::Json5, rest),
43        "yaml" | "yml" => run_format(Format::Yaml, rest),
44        other => Err(Error::Usage(format!(
45            "unknown subcommand `{other}`. run `config-disassembler help` for usage."
46        ))),
47    }
48}
49
50fn run_format(default_format: Format, args: Vec<String>) -> Result<()> {
51    let mut iter = args.into_iter();
52    let action = iter.next().ok_or_else(|| {
53        Error::Usage(format!(
54            "{default_format} subcommand requires `disassemble` or `reassemble`"
55        ))
56    })?;
57    let rest: Vec<String> = iter.collect();
58
59    match action.as_str() {
60        "disassemble" => run_disassemble(default_format, rest),
61        "reassemble" => run_reassemble(default_format, rest),
62        "help" | "-h" | "--help" => {
63            print_format_help(default_format);
64            Ok(())
65        }
66        other => Err(Error::Usage(format!(
67            "unknown action `{other}` for `{default_format}`; expected `disassemble` or `reassemble`"
68        ))),
69    }
70}
71
72fn run_disassemble(default_format: Format, args: Vec<String>) -> Result<()> {
73    let mut input: Option<PathBuf> = None;
74    let mut output_dir: Option<PathBuf> = None;
75    let mut output_format: Option<Format> = None;
76    let mut input_format: Option<Format> = None;
77    let mut unique_id: Option<String> = None;
78    let mut pre_purge = false;
79    let mut post_purge = false;
80
81    let mut iter = args.into_iter();
82    while let Some(arg) = iter.next() {
83        match arg.as_str() {
84            "--output-dir" | "-o" => {
85                output_dir = Some(PathBuf::from(expect_value(&mut iter, "--output-dir")?));
86            }
87            "--output-format" => {
88                output_format = Some(expect_value(&mut iter, "--output-format")?.parse()?);
89            }
90            "--input-format" => {
91                input_format = Some(expect_value(&mut iter, "--input-format")?.parse()?);
92            }
93            "--unique-id" => {
94                unique_id = Some(expect_value(&mut iter, "--unique-id")?);
95            }
96            "--pre-purge" => pre_purge = true,
97            "--post-purge" => post_purge = true,
98            "-h" | "--help" => {
99                print_format_help(default_format);
100                return Ok(());
101            }
102            other if other.starts_with('-') => {
103                return Err(Error::Usage(format!("unknown option `{other}`")));
104            }
105            _ if input.is_none() => input = Some(PathBuf::from(arg)),
106            _ => {
107                return Err(Error::Usage(format!("unexpected argument `{arg}`")));
108            }
109        }
110    }
111
112    let input = input.ok_or_else(|| Error::Usage("missing <input> file path".into()))?;
113    let opts = DisassembleOptions {
114        input,
115        input_format: input_format.or(Some(default_format)),
116        output_dir,
117        output_format,
118        unique_id,
119        pre_purge,
120        post_purge,
121    };
122    let dir = disassemble::disassemble(opts)?;
123    println!("disassembled into {}", dir.display());
124    Ok(())
125}
126
127fn run_reassemble(default_format: Format, args: Vec<String>) -> Result<()> {
128    let mut input_dir: Option<PathBuf> = None;
129    let mut output: Option<PathBuf> = None;
130    let mut output_format: Option<Format> = None;
131    let mut post_purge = false;
132
133    let mut iter = args.into_iter();
134    while let Some(arg) = iter.next() {
135        match arg.as_str() {
136            "--output" | "-o" => {
137                output = Some(PathBuf::from(expect_value(&mut iter, "--output")?));
138            }
139            "--output-format" => {
140                output_format = Some(expect_value(&mut iter, "--output-format")?.parse()?);
141            }
142            "--post-purge" => post_purge = true,
143            "-h" | "--help" => {
144                print_format_help(default_format);
145                return Ok(());
146            }
147            other if other.starts_with('-') => {
148                return Err(Error::Usage(format!("unknown option `{other}`")));
149            }
150            _ if input_dir.is_none() => input_dir = Some(PathBuf::from(arg)),
151            _ => {
152                return Err(Error::Usage(format!("unexpected argument `{arg}`")));
153            }
154        }
155    }
156
157    let input_dir = input_dir.ok_or_else(|| Error::Usage("missing <input-dir> path".into()))?;
158    let opts = ReassembleOptions {
159        input_dir,
160        output,
161        output_format: output_format.or(Some(default_format)),
162        post_purge,
163    };
164    let path = reassemble::reassemble(opts)?;
165    println!("reassembled to {}", path.display());
166    Ok(())
167}
168
169fn expect_value<I: Iterator<Item = String>>(iter: &mut I, flag: &str) -> Result<String> {
170    iter.next()
171        .ok_or_else(|| Error::Usage(format!("`{flag}` expects a value")))
172}
173
174fn print_help() {
175    eprintln!(
176        "config-disassembler {ver}\n\
177\n\
178Disassemble configuration files (XML, JSON, JSON5, YAML) into smaller files\n\
179and reassemble the original on demand.\n\
180\n\
181USAGE:\n\
182    config-disassembler <subcommand> [args...]\n\
183\n\
184SUBCOMMANDS:\n\
185    xml      Forward to the bundled xml-disassembler CLI.\n\
186    json     Disassemble or reassemble a JSON file.\n\
187    json5    Disassemble or reassemble a JSON5 file.\n\
188    yaml     Disassemble or reassemble a YAML file.\n\
189    help     Show this help text.\n\
190\n\
191Run `config-disassembler <subcommand> --help` for subcommand details.\n",
192        ver = env!("CARGO_PKG_VERSION")
193    );
194}
195
196fn print_format_help(format: Format) {
197    eprintln!(
198        "config-disassembler {format} <action> [options]\n\
199\n\
200ACTIONS:\n\
201    disassemble <input>   Split <input> into a directory of smaller files.\n\
202    reassemble  <dir>     Rebuild the original file from <dir>.\n\
203\n\
204DISASSEMBLE OPTIONS:\n\
205    -o, --output-dir <dir>      Output directory (default: <input-stem> next to input).\n\
206    --input-format <fmt>        Override the input format (default: inferred from extension or `{format}`).\n\
207    --output-format <fmt>       Format used for the split files (default: same as input).\n\
208    --unique-id <field>         For array roots, name files by this field on each element.\n\
209    --pre-purge                 Remove the output directory before writing.\n\
210    --post-purge                Delete the input file after disassembly.\n\
211\n\
212REASSEMBLE OPTIONS:\n\
213    -o, --output <file>         Output file (default: derived from metadata next to input dir).\n\
214    --output-format <fmt>       Format to write the reassembled file in (default: original source format).\n\
215    --post-purge                Remove the input directory after reassembly.\n\
216\n\
217<fmt> is one of: json, json5, yaml.\n"
218    );
219}