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