config_disassembler/
cli.rs1use 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
23pub 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}