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      Disassemble or reassemble an XML file (in-tree port of xml-disassembler).
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//!   toml     Disassemble or reassemble a TOML file (TOML <-> TOML only).
12//!   help     Show this help text.
13//! ```
14
15use std::path::PathBuf;
16
17use crate::disassemble::{self, DisassembleOptions};
18use crate::error::{Error, Result};
19use crate::format::Format;
20use crate::reassemble::{self, ReassembleOptions};
21use crate::xml_cmd;
22
23/// Dispatch a full argv (including program name at `args[0]`).
24pub async fn dispatch(args: Vec<String>) -> Result<()> {
25    let mut iter = args.into_iter();
26    let _program = iter.next();
27    let subcommand = match iter.next() {
28        Some(s) => s,
29        None => {
30            print_help();
31            return Ok(());
32        }
33    };
34    let rest: Vec<String> = iter.collect();
35
36    match subcommand.as_str() {
37        "help" | "-h" | "--help" => {
38            print_help();
39            Ok(())
40        }
41        "xml" => xml_cmd::run(rest).await,
42        "json" => run_format(Format::Json, rest),
43        "json5" => run_format(Format::Json5, rest),
44        "yaml" | "yml" => run_format(Format::Yaml, rest),
45        "toml" => run_format(Format::Toml, rest),
46        other => Err(Error::Usage(format!(
47            "unknown subcommand `{other}`. run `config-disassembler help` for usage."
48        ))),
49    }
50}
51
52fn run_format(default_format: Format, args: Vec<String>) -> Result<()> {
53    let mut iter = args.into_iter();
54    let action = iter.next().ok_or_else(|| {
55        Error::Usage(format!(
56            "{default_format} subcommand requires `disassemble` or `reassemble`"
57        ))
58    })?;
59    let rest: Vec<String> = iter.collect();
60
61    match action.as_str() {
62        "disassemble" => run_disassemble(default_format, rest),
63        "reassemble" => run_reassemble(default_format, rest),
64        "help" | "-h" | "--help" => {
65            print_format_help(default_format);
66            Ok(())
67        }
68        other => Err(Error::Usage(format!(
69            "unknown action `{other}` for `{default_format}`; expected `disassemble` or `reassemble`"
70        ))),
71    }
72}
73
74fn run_disassemble(default_format: Format, args: Vec<String>) -> Result<()> {
75    let toml_only = default_format == Format::Toml;
76    let mut input: Option<PathBuf> = None;
77    let mut output_dir: Option<PathBuf> = None;
78    let mut output_format: Option<Format> = None;
79    let mut input_format: Option<Format> = None;
80    let mut unique_id: Option<String> = None;
81    let mut pre_purge = false;
82    let mut post_purge = false;
83    let mut ignore_path: Option<PathBuf> = None;
84
85    let mut iter = args.into_iter();
86    while let Some(arg) = iter.next() {
87        match arg.as_str() {
88            "--output-dir" | "-o" => {
89                output_dir = Some(PathBuf::from(expect_value(&mut iter, "--output-dir")?));
90            }
91            "--output-format" if toml_only => {
92                return Err(Error::Usage(
93                    "--output-format is not supported for `toml`; TOML can only be converted to TOML"
94                        .into(),
95                ));
96            }
97            "--output-format" => {
98                output_format = Some(expect_value(&mut iter, "--output-format")?.parse()?);
99            }
100            "--input-format" if toml_only => {
101                return Err(Error::Usage(
102                    "--input-format is not supported for `toml`; TOML can only be converted from TOML"
103                        .into(),
104                ));
105            }
106            "--input-format" => {
107                input_format = Some(expect_value(&mut iter, "--input-format")?.parse()?);
108            }
109            "--unique-id" => {
110                unique_id = Some(expect_value(&mut iter, "--unique-id")?);
111            }
112            "--ignore-path" => {
113                ignore_path = Some(PathBuf::from(expect_value(&mut iter, "--ignore-path")?));
114            }
115            "--pre-purge" => pre_purge = true,
116            "--post-purge" => post_purge = true,
117            "-h" | "--help" => {
118                print_format_help(default_format);
119                return Ok(());
120            }
121            other if other.starts_with('-') => {
122                return Err(Error::Usage(format!("unknown option `{other}`")));
123            }
124            _ if input.is_none() => input = Some(PathBuf::from(arg)),
125            _ => {
126                return Err(Error::Usage(format!("unexpected argument `{arg}`")));
127            }
128        }
129    }
130
131    let input = input.ok_or_else(|| Error::Usage("missing <input> file path".into()))?;
132    let opts = DisassembleOptions {
133        input,
134        input_format: input_format.or(Some(default_format)),
135        output_dir,
136        output_format,
137        unique_id,
138        pre_purge,
139        post_purge,
140        ignore_path,
141    };
142    let dir = disassemble::disassemble(opts)?;
143    println!("disassembled into {}", dir.display());
144    Ok(())
145}
146
147fn run_reassemble(default_format: Format, args: Vec<String>) -> Result<()> {
148    let toml_only = default_format == Format::Toml;
149    let mut input_dir: Option<PathBuf> = None;
150    let mut output: Option<PathBuf> = None;
151    let mut output_format: Option<Format> = None;
152    let mut post_purge = false;
153
154    let mut iter = args.into_iter();
155    while let Some(arg) = iter.next() {
156        match arg.as_str() {
157            "--output" | "-o" => {
158                output = Some(PathBuf::from(expect_value(&mut iter, "--output")?));
159            }
160            "--output-format" if toml_only => {
161                return Err(Error::Usage(
162                    "--output-format is not supported for `toml`; TOML can only be reassembled to TOML"
163                        .into(),
164                ));
165            }
166            "--output-format" => {
167                output_format = Some(expect_value(&mut iter, "--output-format")?.parse()?);
168            }
169            "--post-purge" => post_purge = true,
170            "-h" | "--help" => {
171                print_format_help(default_format);
172                return Ok(());
173            }
174            other if other.starts_with('-') => {
175                return Err(Error::Usage(format!("unknown option `{other}`")));
176            }
177            _ if input_dir.is_none() => input_dir = Some(PathBuf::from(arg)),
178            _ => {
179                return Err(Error::Usage(format!("unexpected argument `{arg}`")));
180            }
181        }
182    }
183
184    let input_dir = input_dir.ok_or_else(|| Error::Usage("missing <input-dir> path".into()))?;
185    let opts = ReassembleOptions {
186        input_dir,
187        output,
188        output_format: output_format.or(Some(default_format)),
189        post_purge,
190    };
191    let path = reassemble::reassemble(opts)?;
192    println!("reassembled to {}", path.display());
193    Ok(())
194}
195
196fn expect_value<I: Iterator<Item = String>>(iter: &mut I, flag: &str) -> Result<String> {
197    iter.next()
198        .ok_or_else(|| Error::Usage(format!("`{flag}` expects a value")))
199}
200
201fn print_help() {
202    eprintln!(
203        "config-disassembler {ver}\n\
204\n\
205Disassemble configuration files (XML, JSON, JSON5, YAML, TOML) into smaller\n\
206files and reassemble the original on demand.\n\
207\n\
208USAGE:\n\
209    config-disassembler <subcommand> [args...]\n\
210\n\
211SUBCOMMANDS:\n\
212    xml      Disassemble or reassemble an XML file.\n\
213    json     Disassemble or reassemble a JSON file.\n\
214    json5    Disassemble or reassemble a JSON5 file.\n\
215    yaml     Disassemble or reassemble a YAML file.\n\
216    toml     Disassemble or reassemble a TOML file (TOML <-> TOML only).\n\
217    help     Show this help text.\n\
218\n\
219Run `config-disassembler <subcommand> --help` for subcommand details.\n",
220        ver = env!("CARGO_PKG_VERSION")
221    );
222}
223
224fn print_format_help(format: Format) {
225    if format == Format::Toml {
226        eprintln!(
227            "config-disassembler toml <action> [options]\n\
228\n\
229TOML can only be converted to and from TOML. Cross-format conversion with\n\
230JSON, JSON5, or YAML is rejected because TOML cannot represent `null`,\n\
231forbids array roots, and forces bare keys to precede tables (which would\n\
232reorder values on round-trip).\n\
233\n\
234ACTIONS:\n\
235    disassemble <input>   Split <input>.toml into a directory of TOML files.\n\
236                          <input> may also be a directory; every .toml file\n\
237                          beneath it is disassembled in place.\n\
238    reassemble  <dir>     Rebuild the original TOML file from <dir>.\n\
239\n\
240DISASSEMBLE OPTIONS:\n\
241    -o, --output-dir <dir>      Output directory (default: <input-stem> next to input).\n\
242                                Not allowed when <input> is a directory.\n\
243    --unique-id <field>         For array roots, name files by this field on each element.\n\
244                                (TOML disallows array roots, so this only applies to nested arrays.)\n\
245    --ignore-path <path>        Path to a .gitignore-style file used when <input> is a\n\
246                                directory (default: .cdignore in the input directory).\n\
247    --pre-purge                 Remove the output directory before writing.\n\
248    --post-purge                Delete the input file after disassembly.\n\
249\n\
250REASSEMBLE OPTIONS:\n\
251    -o, --output <file>         Output file (default: derived from metadata next to input dir).\n\
252    --post-purge                Remove the input directory after reassembly.\n"
253        );
254        return;
255    }
256    eprintln!(
257        "config-disassembler {format} <action> [options]\n\
258\n\
259ACTIONS:\n\
260    disassemble <input>   Split <input> into a directory of smaller files.\n\
261                          <input> may also be a directory; every matching\n\
262                          file beneath it is disassembled in place.\n\
263    reassemble  <dir>     Rebuild the original file from <dir>.\n\
264\n\
265DISASSEMBLE OPTIONS:\n\
266    -o, --output-dir <dir>      Output directory (default: <input-stem> next to input).\n\
267                                Not allowed when <input> is a directory.\n\
268    --input-format <fmt>        Override the input format (default: inferred from extension or `{format}`).\n\
269    --output-format <fmt>       Format used for the split files (default: same as input).\n\
270    --unique-id <field>         For array roots, name files by this field on each element.\n\
271    --ignore-path <path>        Path to a .gitignore-style file used when <input> is a\n\
272                                directory (default: .cdignore in the input directory).\n\
273    --pre-purge                 Remove the output directory before writing.\n\
274    --post-purge                Delete the input file after disassembly.\n\
275\n\
276REASSEMBLE OPTIONS:\n\
277    -o, --output <file>         Output file (default: derived from metadata next to input dir).\n\
278    --output-format <fmt>       Format to write the reassembled file in (default: original source format).\n\
279    --post-purge                Remove the input directory after reassembly.\n\
280\n\
281<fmt> is one of: json, json5, yaml. (TOML is excluded -- use the `toml` subcommand.)\n"
282    );
283}