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