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