1pub mod ai;
2pub mod android;
3pub mod containers;
4pub mod coreutils;
5pub mod dotnet;
6pub mod forges;
7pub mod fuzzy;
8pub mod go;
9pub mod jvm;
10pub mod magick;
11pub mod network;
12pub mod node;
13pub mod perl;
14pub mod php;
15pub mod python;
16pub mod r;
17pub mod ruby;
18pub mod rust;
19pub mod shell;
20pub mod swift;
21pub mod system;
22pub mod vcs;
23pub mod wrappers;
24pub mod xcode;
25
26use crate::parse::Token;
27use crate::verdict::Verdict;
28
29pub fn dispatch(tokens: &[Token]) -> Verdict {
30 let cmd = tokens[0].command_name();
31 None
32 .or_else(|| shell::dispatch(cmd, tokens))
33 .or_else(|| wrappers::dispatch(cmd, tokens))
34 .or_else(|| vcs::dispatch(cmd, tokens))
35 .or_else(|| forges::dispatch(cmd, tokens))
36 .or_else(|| node::dispatch(cmd, tokens))
37 .or_else(|| ruby::dispatch(cmd, tokens))
38 .or_else(|| python::dispatch(cmd, tokens))
39 .or_else(|| rust::dispatch(cmd, tokens))
40 .or_else(|| go::dispatch(cmd, tokens))
41 .or_else(|| jvm::dispatch(cmd, tokens))
42 .or_else(|| android::dispatch(cmd, tokens))
43 .or_else(|| php::dispatch(cmd, tokens))
44 .or_else(|| swift::dispatch(cmd, tokens))
45 .or_else(|| dotnet::dispatch(cmd, tokens))
46 .or_else(|| containers::dispatch(cmd, tokens))
47 .or_else(|| network::dispatch(cmd, tokens))
48 .or_else(|| ai::dispatch(cmd, tokens))
49 .or_else(|| system::dispatch(cmd, tokens))
50 .or_else(|| xcode::dispatch(cmd, tokens))
51 .or_else(|| perl::dispatch(cmd, tokens))
52 .or_else(|| r::dispatch(cmd, tokens))
53 .or_else(|| coreutils::dispatch(cmd, tokens))
54 .or_else(|| fuzzy::dispatch(cmd, tokens))
55 .or_else(|| magick::dispatch(cmd, tokens))
56 .unwrap_or(Verdict::Denied)
57}
58
59#[cfg(test)]
60const HANDLED_CMDS: &[&str] = &[
61 "sh", "bash", "xargs", "timeout", "time", "env", "nice", "ionice", "hyperfine", "dotenv",
62 "git", "jj", "gh", "glab", "jjpr", "tea",
63 "npm", "yarn", "pnpm", "bun", "deno", "npx", "bunx", "nvm", "fnm", "volta",
64 "ruby", "ri", "bundle", "gem", "importmap", "rbenv",
65 "pip", "uv", "poetry", "pyenv", "conda",
66 "cargo", "rustup",
67 "go",
68 "gradle", "mvn", "mvnw", "ktlint", "detekt",
69 "javap", "jar", "keytool", "jarsigner",
70 "adb", "apkanalyzer", "apksigner", "bundletool", "aapt2",
71 "emulator", "avdmanager", "sdkmanager", "zipalign", "lint",
72 "fastlane", "firebase",
73 "composer", "craft",
74 "swift",
75 "dotnet",
76 "curl",
77 "docker", "podman", "kubectl", "orbctl", "qemu-img",
78 "ollama", "llm", "hf", "claude", "aider", "codex", "opencode", "vibe",
79 "ddev", "dcli",
80 "brew", "mise", "asdf", "crontab", "defaults", "pmset", "sysctl", "cmake", "psql", "pg_isready",
81 "terraform", "heroku", "vercel", "flyctl",
82 "overmind", "tailscale", "tmux", "wg",
83 "networksetup", "launchctl", "diskutil", "security", "csrutil", "log",
84 "xcodebuild", "plutil", "xcode-select", "xcrun", "pkgutil", "lipo", "codesign", "spctl",
85 "xcodegen", "tuist", "pod", "swiftlint", "swiftformat", "periphery", "xcbeautify", "agvtool", "simctl",
86 "perl",
87 "R", "Rscript",
88 "grep", "rg", "ag", "ack", "zgrep", "locate",
89 "cat", "head", "tail", "wc", "cut", "tr", "uniq", "less", "more", "zcat",
90 "diff", "comm", "paste", "tac", "rev", "nl",
91 "expand", "unexpand", "fold", "fmt", "col", "column", "iconv", "nroff",
92 "echo", "printf", "seq", "test", "expr", "bc", "factor", "bat",
93 "arch", "command", "hostname",
94 "find", "sed", "shuf", "sort", "yq", "xmllint", "awk", "gawk", "mawk", "nawk",
95 "magick",
96 "fd", "eza", "ls", "delta", "colordiff",
97 "dirname", "basename", "realpath", "readlink",
98 "file", "stat", "du", "df", "tree", "cmp", "zipinfo", "tar", "unzip", "gzip",
99 "true", "false",
100 "alias", "export", "printenv", "read", "type", "wait", "whereis", "which", "whoami", "date", "pwd", "cd", "unset",
101 "uname", "nproc", "uptime", "id", "groups", "tty", "locale", "cal", "sleep",
102 "who", "w", "last", "lastlog",
103 "ps", "top", "htop", "iotop", "procs", "dust", "lsof", "pgrep", "lsblk", "free",
104 "jq", "jaq", "gojq", "fx", "jless", "htmlq", "xq", "tomlq", "mlr", "dasel",
105 "base64", "xxd", "getconf", "uuidgen",
106 "md5sum", "md5", "sha256sum", "shasum", "sha1sum", "sha512sum",
107 "cksum", "b2sum", "sum", "strings", "hexdump", "od", "size", "sips",
108 "sw_vers", "mdls", "otool", "nm", "system_profiler", "ioreg", "vm_stat", "mdfind", "man",
109 "dig", "nslookup", "host", "whois", "netstat", "ss", "ifconfig", "route", "ping",
110 "xv",
111 "fzf", "fzy", "peco", "pick", "selecta", "sk", "zf",
112 "identify", "shellcheck", "cloc", "tokei", "cucumber", "branchdiff", "workon", "safe-chains",
113];
114
115pub fn handler_docs() -> Vec<crate::docs::CommandDoc> {
116 let mut docs = Vec::new();
117 docs.extend(vcs::command_docs());
118 docs.extend(forges::command_docs());
119 docs.extend(node::command_docs());
120 docs.extend(ruby::command_docs());
121 docs.extend(python::command_docs());
122 docs.extend(rust::command_docs());
123 docs.extend(go::command_docs());
124 docs.extend(jvm::command_docs());
125 docs.extend(android::command_docs());
126 docs.extend(php::command_docs());
127 docs.extend(swift::command_docs());
128 docs.extend(dotnet::command_docs());
129 docs.extend(containers::command_docs());
130 docs.extend(ai::command_docs());
131 docs.extend(network::command_docs());
132 docs.extend(system::command_docs());
133 docs.extend(xcode::command_docs());
134 docs.extend(perl::command_docs());
135 docs.extend(r::command_docs());
136 docs.extend(coreutils::command_docs());
137 docs.extend(fuzzy::command_docs());
138 docs.extend(shell::command_docs());
139 docs.extend(wrappers::command_docs());
140 docs.extend(magick::command_docs());
141 docs
142}
143
144#[cfg(test)]
145#[derive(Debug)]
146pub(crate) enum CommandEntry {
147 Positional { cmd: &'static str },
148 Custom { cmd: &'static str, valid_prefix: Option<&'static str> },
149 Subcommand { cmd: &'static str, subs: &'static [SubEntry], bare_ok: bool },
150 Delegation { cmd: &'static str },
151}
152
153#[cfg(test)]
154#[derive(Debug)]
155pub(crate) enum SubEntry {
156 Policy { name: &'static str },
157 Nested { name: &'static str, subs: &'static [SubEntry] },
158 Custom { name: &'static str, valid_suffix: Option<&'static str> },
159 Positional,
160 Guarded { name: &'static str, valid_suffix: &'static str },
161}
162
163use crate::command::CommandDef;
164
165const COMMAND_DEFS: &[&CommandDef] = &[
166 &ai::CODEX, &ai::OLLAMA, &ai::OPENCODE, &ai::LLM, &ai::HF,
167 &containers::DOCKER, &containers::PODMAN, &containers::KUBECTL, &containers::ORBCTL, &containers::QEMU_IMG,
168 &dotnet::DOTNET,
169 &go::GO,
170 &android::APKANALYZER, &android::APKSIGNER, &android::BUNDLETOOL, &android::AAPT2,
171 &android::AVDMANAGER,
172 &jvm::GRADLE, &jvm::KEYTOOL,
173 &magick::MAGICK,
174 &node::NPM, &node::PNPM, &node::BUN, &node::DENO,
175 &node::NVM, &node::FNM, &node::VOLTA,
176 &php::COMPOSER, &php::CRAFT,
177 &python::PIP, &python::UV, &python::POETRY,
178 &python::PYENV, &python::CONDA,
179 &ruby::BUNDLE, &ruby::GEM, &ruby::IMPORTMAP, &ruby::RBENV,
180 &rust::CARGO, &rust::RUSTUP,
181 &vcs::GIT,
182 &swift::SWIFT,
183 &system::BREW, &system::MISE, &system::ASDF, &system::DDEV, &system::DCLI, &system::CMAKE,
184 &system::DEFAULTS, &system::TERRAFORM, &system::HEROKU, &system::VERCEL,
185 &system::FLYCTL, &system::FASTLANE, &system::FIREBASE,
186 &system::OVERMIND, &system::TAILSCALE, &system::WG,
187 &system::SECURITY, &system::CSRUTIL, &system::DISKUTIL,
188 &system::LAUNCHCTL, &system::LOG,
189 &xcode::XCODEBUILD, &xcode::PLUTIL, &xcode::XCODE_SELECT,
190 &xcode::XCODEGEN, &xcode::TUIST, &xcode::POD, &xcode::SWIFTLINT,
191 &xcode::PERIPHERY, &xcode::AGVTOOL, &xcode::SIMCTL,
192];
193
194pub fn all_opencode_patterns() -> Vec<String> {
195 let mut patterns = Vec::new();
196 for def in COMMAND_DEFS {
197 patterns.extend(def.opencode_patterns());
198 }
199 for def in coreutils::all_flat_defs() {
200 patterns.extend(def.opencode_patterns());
201 }
202 for def in jvm::jvm_flat_defs() {
203 patterns.extend(def.opencode_patterns());
204 }
205 for def in android::android_flat_defs() {
206 patterns.extend(def.opencode_patterns());
207 }
208 for def in ai::ai_flat_defs() {
209 patterns.extend(def.opencode_patterns());
210 }
211 for def in ruby::ruby_flat_defs() {
212 patterns.extend(def.opencode_patterns());
213 }
214 for def in system::system_flat_defs() {
215 patterns.extend(def.opencode_patterns());
216 }
217 for def in xcode::xcbeautify_flat_defs() {
218 patterns.extend(def.opencode_patterns());
219 }
220 for def in fuzzy::fuzzy_flat_defs() {
221 patterns.extend(def.opencode_patterns());
222 }
223 patterns.sort();
224 patterns.dedup();
225 patterns
226}
227
228#[cfg(test)]
229fn full_registry() -> Vec<&'static CommandEntry> {
230 let mut entries = Vec::new();
231 entries.extend(shell::REGISTRY);
232 entries.extend(wrappers::REGISTRY);
233 entries.extend(vcs::full_registry());
234 entries.extend(forges::full_registry());
235 entries.extend(node::full_registry());
236 entries.extend(jvm::full_registry());
237 entries.extend(android::full_registry());
238 entries.extend(network::REGISTRY);
239 entries.extend(system::full_registry());
240 entries.extend(xcode::full_registry());
241 entries.extend(perl::REGISTRY);
242 entries.extend(r::REGISTRY);
243 entries.extend(coreutils::full_registry());
244 entries.extend(fuzzy::full_registry());
245 entries
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use std::collections::HashSet;
252
253 const UNKNOWN_FLAG: &str = "--xyzzy-unknown-42";
254 const UNKNOWN_SUB: &str = "xyzzy-unknown-42";
255
256 fn check_entry(entry: &CommandEntry, failures: &mut Vec<String>) {
257 match entry {
258 CommandEntry::Positional { .. } | CommandEntry::Delegation { .. } => {}
259 CommandEntry::Custom { cmd, valid_prefix } => {
260 let base = valid_prefix.unwrap_or(cmd);
261 let test = format!("{base} {UNKNOWN_FLAG}");
262 if crate::is_safe_command(&test) {
263 failures.push(format!("{cmd}: accepted unknown flag: {test}"));
264 }
265 }
266 CommandEntry::Subcommand { cmd, subs, bare_ok } => {
267 if !bare_ok && crate::is_safe_command(cmd) {
268 failures.push(format!("{cmd}: accepted bare invocation"));
269 }
270 let test = format!("{cmd} {UNKNOWN_SUB}");
271 if crate::is_safe_command(&test) {
272 failures.push(format!("{cmd}: accepted unknown subcommand: {test}"));
273 }
274 for sub in *subs {
275 check_sub(cmd, sub, failures);
276 }
277 }
278 }
279 }
280
281 fn check_sub(prefix: &str, entry: &SubEntry, failures: &mut Vec<String>) {
282 match entry {
283 SubEntry::Policy { name } => {
284 let test = format!("{prefix} {name} {UNKNOWN_FLAG}");
285 if crate::is_safe_command(&test) {
286 failures.push(format!("{prefix} {name}: accepted unknown flag: {test}"));
287 }
288 }
289 SubEntry::Nested { name, subs } => {
290 let path = format!("{prefix} {name}");
291 let test = format!("{path} {UNKNOWN_SUB}");
292 if crate::is_safe_command(&test) {
293 failures.push(format!("{path}: accepted unknown subcommand: {test}"));
294 }
295 for sub in *subs {
296 check_sub(&path, sub, failures);
297 }
298 }
299 SubEntry::Custom { name, valid_suffix } => {
300 let base = match valid_suffix {
301 Some(s) => format!("{prefix} {name} {s}"),
302 None => format!("{prefix} {name}"),
303 };
304 let test = format!("{base} {UNKNOWN_FLAG}");
305 if crate::is_safe_command(&test) {
306 failures.push(format!("{prefix} {name}: accepted unknown flag: {test}"));
307 }
308 }
309 SubEntry::Positional => {}
310 SubEntry::Guarded { name, valid_suffix } => {
311 let test = format!("{prefix} {name} {valid_suffix} {UNKNOWN_FLAG}");
312 if crate::is_safe_command(&test) {
313 failures.push(format!("{prefix} {name}: accepted unknown flag: {test}"));
314 }
315 }
316 }
317 }
318
319 #[test]
320 fn all_commands_reject_unknown() {
321 let registry = full_registry();
322 let mut failures = Vec::new();
323 for entry in ®istry {
324 check_entry(entry, &mut failures);
325 }
326 assert!(
327 failures.is_empty(),
328 "unknown flags/subcommands accepted:\n{}",
329 failures.join("\n")
330 );
331 }
332
333 #[test]
334 fn command_defs_reject_unknown() {
335 for def in COMMAND_DEFS {
336 def.auto_test_reject_unknown();
337 }
338 }
339
340 #[test]
341 fn flat_defs_reject_unknown() {
342 for def in coreutils::all_flat_defs() {
343 def.auto_test_reject_unknown();
344 }
345 for def in xcode::xcbeautify_flat_defs() {
346 def.auto_test_reject_unknown();
347 }
348 for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()).chain(fuzzy::fuzzy_flat_defs()) {
349 def.auto_test_reject_unknown();
350 }
351 }
352
353
354 #[test]
355 fn bare_false_rejects_bare_invocation() {
356 let check_def = |def: &crate::command::FlatDef| {
357 if !def.policy.bare {
358 assert!(
359 !crate::is_safe_command(def.name),
360 "{}: bare=false but bare invocation accepted",
361 def.name,
362 );
363 }
364 };
365 for def in coreutils::all_flat_defs()
366 .into_iter()
367 .chain(xcode::xcbeautify_flat_defs())
368 {
369 check_def(def);
370 }
371 for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()).chain(fuzzy::fuzzy_flat_defs()) {
372 check_def(def);
373 }
374 }
375
376 fn visit_subs(prefix: &str, subs: &[crate::command::SubDef], visitor: &mut dyn FnMut(&str, &crate::command::SubDef)) {
377 for sub in subs {
378 visitor(prefix, sub);
379 if let crate::command::SubDef::Nested { name, subs: inner } = sub {
380 visit_subs(&format!("{prefix} {name}"), inner, visitor);
381 }
382 }
383 }
384
385 #[test]
386 fn guarded_subs_require_guard() {
387 let mut failures = Vec::new();
388 for def in COMMAND_DEFS {
389 visit_subs(def.name, def.subs, &mut |prefix, sub| {
390 if let crate::command::SubDef::Guarded { name, guard_long, .. } = sub {
391 let without = format!("{prefix} {name}");
392 if crate::is_safe_command(&without) {
393 failures.push(format!("{without}: accepted without guard {guard_long}"));
394 }
395 let with = format!("{prefix} {name} {guard_long}");
396 if !crate::is_safe_command(&with) {
397 failures.push(format!("{with}: rejected with guard {guard_long}"));
398 }
399 }
400 });
401 }
402 assert!(failures.is_empty(), "guarded sub issues:\n{}", failures.join("\n"));
403 }
404
405 #[test]
406 fn guarded_subs_accept_guard_short() {
407 let mut failures = Vec::new();
408 for def in COMMAND_DEFS {
409 visit_subs(def.name, def.subs, &mut |prefix, sub| {
410 if let crate::command::SubDef::Guarded { name, guard_short: Some(short), .. } = sub {
411 let with_short = format!("{prefix} {name} {short}");
412 if !crate::is_safe_command(&with_short) {
413 failures.push(format!("{with_short}: rejected with guard_short"));
414 }
415 }
416 });
417 }
418 assert!(failures.is_empty(), "guard_short issues:\n{}", failures.join("\n"));
419 }
420
421 #[test]
422 fn nested_subs_reject_bare() {
423 let mut failures = Vec::new();
424 for def in COMMAND_DEFS {
425 visit_subs(def.name, def.subs, &mut |prefix, sub| {
426 if let crate::command::SubDef::Nested { name, .. } = sub {
427 let bare = format!("{prefix} {name}");
428 if crate::is_safe_command(&bare) {
429 failures.push(format!("{bare}: nested sub accepted bare invocation"));
430 }
431 }
432 });
433 }
434 assert!(failures.is_empty(), "nested bare issues:\n{}", failures.join("\n"));
435 }
436
437 #[test]
438 fn process_substitution_blocked() {
439 let cmds = ["echo <(cat /etc/passwd)", "echo >(rm -rf /)", "grep pattern <(ls)"];
440 for cmd in &cmds {
441 assert!(
442 !crate::is_safe_command(cmd),
443 "process substitution not blocked: {cmd}",
444 );
445 }
446 }
447
448 #[test]
449 fn positional_style_accepts_unknown_args() {
450 use crate::policy::FlagStyle;
451 for def in coreutils::all_flat_defs() {
452 if def.policy.flag_style == FlagStyle::Positional {
453 let test = format!("{} --unknown-xyz", def.name);
454 assert!(
455 crate::is_safe_command(&test),
456 "{}: FlagStyle::Positional but rejected unknown arg",
457 def.name,
458 );
459 }
460 }
461 }
462
463 fn visit_policies(prefix: &str, subs: &[crate::command::SubDef], visitor: &mut dyn FnMut(&str, &crate::policy::FlagPolicy)) {
464 for sub in subs {
465 match sub {
466 crate::command::SubDef::Policy { name, policy, .. } => {
467 visitor(&format!("{prefix} {name}"), policy);
468 }
469 crate::command::SubDef::Guarded { name, guard_long, policy, .. } => {
470 visitor(&format!("{prefix} {name} {guard_long}"), policy);
471 }
472 crate::command::SubDef::Nested { name, subs: inner } => {
473 visit_policies(&format!("{prefix} {name}"), inner, visitor);
474 }
475 _ => {}
476 }
477 }
478 }
479
480 #[test]
481 fn valued_flags_accept_eq_syntax() {
482 let mut failures = Vec::new();
483
484 let check_flat = |def: &crate::command::FlatDef, failures: &mut Vec<String>| {
485 for flag in def.policy.valued.iter() {
486 let cmd = format!("{} {flag}=test_value", def.name);
487 if !crate::is_safe_command(&cmd) {
488 failures.push(format!("{cmd}: valued flag rejected with = syntax"));
489 }
490 }
491 };
492 for def in coreutils::all_flat_defs()
493 .into_iter()
494 .chain(xcode::xcbeautify_flat_defs())
495 {
496 check_flat(def, &mut failures);
497 }
498 for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()).chain(fuzzy::fuzzy_flat_defs()) {
499 check_flat(def, &mut failures);
500 }
501
502 for def in COMMAND_DEFS {
503 visit_policies(def.name, def.subs, &mut |prefix, policy| {
504 for flag in policy.valued.iter() {
505 let cmd = format!("{prefix} {flag}=test_value");
506 if !crate::is_safe_command(&cmd) {
507 failures.push(format!("{cmd}: valued flag rejected with = syntax"));
508 }
509 }
510 });
511 }
512
513 assert!(failures.is_empty(), "valued = syntax issues:\n{}", failures.join("\n"));
514 }
515
516 #[test]
517 fn max_positional_enforced() {
518 let mut failures = Vec::new();
519
520 let check_flat = |def: &crate::command::FlatDef, failures: &mut Vec<String>| {
521 if let Some(max) = def.policy.max_positional {
522 let args: Vec<&str> = (0..=max).map(|_| "testarg").collect();
523 let cmd = format!("{} {}", def.name, args.join(" "));
524 if crate::is_safe_command(&cmd) {
525 failures.push(format!(
526 "{}: max_positional={max} but accepted {} positional args",
527 def.name,
528 max + 1,
529 ));
530 }
531 }
532 };
533 for def in coreutils::all_flat_defs()
534 .into_iter()
535 .chain(xcode::xcbeautify_flat_defs())
536 {
537 check_flat(def, &mut failures);
538 }
539 for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()).chain(fuzzy::fuzzy_flat_defs()) {
540 check_flat(def, &mut failures);
541 }
542
543 for def in COMMAND_DEFS {
544 visit_policies(def.name, def.subs, &mut |prefix, policy| {
545 if let Some(max) = policy.max_positional {
546 let args: Vec<&str> = (0..=max).map(|_| "testarg").collect();
547 let cmd = format!("{prefix} {}", args.join(" "));
548 if crate::is_safe_command(&cmd) {
549 failures.push(format!(
550 "{prefix}: max_positional={max} but accepted {} positional args",
551 max + 1,
552 ));
553 }
554 }
555 });
556 }
557
558 assert!(failures.is_empty(), "max_positional issues:\n{}", failures.join("\n"));
559 }
560
561 #[test]
562 fn doc_generation_non_empty() {
563 let mut failures = Vec::new();
564
565 for def in COMMAND_DEFS {
566 let doc = def.to_doc();
567 if doc.description.trim().is_empty() {
568 failures.push(format!("{}: CommandDef produced empty doc", def.name));
569 }
570 if doc.url.is_empty() {
571 failures.push(format!("{}: CommandDef has empty URL", def.name));
572 }
573 }
574
575 let check_flat = |def: &crate::command::FlatDef, failures: &mut Vec<String>| {
576 let doc = def.to_doc();
577 if doc.description.trim().is_empty() && !def.policy.bare {
578 failures.push(format!("{}: FlatDef produced empty doc", def.name));
579 }
580 if doc.url.is_empty() {
581 failures.push(format!("{}: FlatDef has empty URL", def.name));
582 }
583 };
584 for def in coreutils::all_flat_defs()
585 .into_iter()
586 .chain(xcode::xcbeautify_flat_defs())
587 {
588 check_flat(def, &mut failures);
589 }
590 for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()).chain(fuzzy::fuzzy_flat_defs()) {
591 check_flat(def, &mut failures);
592 }
593
594 assert!(failures.is_empty(), "doc generation issues:\n{}", failures.join("\n"));
595 }
596
597 #[test]
598 fn registry_covers_handled_commands() {
599 let registry = full_registry();
600 let mut all_cmds: HashSet<&str> = registry
601 .iter()
602 .map(|e| match e {
603 CommandEntry::Positional { cmd }
604 | CommandEntry::Custom { cmd, .. }
605 | CommandEntry::Subcommand { cmd, .. }
606 | CommandEntry::Delegation { cmd } => *cmd,
607 })
608 .collect();
609 for def in COMMAND_DEFS {
610 all_cmds.insert(def.name);
611 }
612 for def in coreutils::all_flat_defs() {
613 all_cmds.insert(def.name);
614 }
615 for def in xcode::xcbeautify_flat_defs() {
616 all_cmds.insert(def.name);
617 }
618 for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()).chain(fuzzy::fuzzy_flat_defs()) {
619 all_cmds.insert(def.name);
620 }
621 let handled: HashSet<&str> = HANDLED_CMDS.iter().copied().collect();
622
623 let missing: Vec<_> = handled.difference(&all_cmds).collect();
624 assert!(missing.is_empty(), "not in registry or COMMAND_DEFS: {missing:?}");
625
626 let extra: Vec<_> = all_cmds.difference(&handled).collect();
627 assert!(extra.is_empty(), "not in HANDLED_CMDS: {extra:?}");
628 }
629
630}