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