1use std::collections::HashMap;
2
3use crate::policy::{FlagTolerance, UnknownTolerance};
4use crate::verdict::SafetyLevel;
5
6use super::types::*;
7
8pub(super) fn build_policy(
9 standalone: Vec<String>,
10 valued: Vec<String>,
11 bare: Option<bool>,
12 max_positional: Option<usize>,
13 tolerate_unknown_short: Option<bool>,
14 tolerate_unknown_long: Option<bool>,
15 numeric_dash: Option<bool>,
16) -> OwnedPolicy {
17 let unknown = match (
18 tolerate_unknown_short.unwrap_or(false),
19 tolerate_unknown_long.unwrap_or(false),
20 ) {
21 (false, false) => UnknownTolerance::Strict,
22 (true, false) => UnknownTolerance::Short,
23 (false, true) => UnknownTolerance::Long,
24 (true, true) => UnknownTolerance::Both,
25 };
26 OwnedPolicy {
27 standalone,
28 valued,
29 bare: bare.unwrap_or(true),
30 max_positional,
31 tolerance: FlagTolerance {
32 unknown,
33 numeric_dash: numeric_dash.unwrap_or(false),
34 },
35 }
36}
37
38fn build_matrix(toml: TomlMatrix) -> MatrixSpec {
39 let actions = toml
40 .actions
41 .into_iter()
42 .map(|(name, action)| {
43 let built = match action {
44 TomlMatrixAction::Policy(policy_key) => MatrixAction {
45 policy_key,
46 guard: None,
47 guard_short: None,
48 },
49 TomlMatrixAction::Detailed(d) => {
50 MatrixAction {
51 policy_key: d.policy,
52 guard: d.guard,
53 guard_short: d.guard_short,
54 }
55 }
56 };
57 (name, built)
58 })
59 .collect();
60 MatrixSpec {
61 parents: toml.parents,
62 level: toml.level.into(),
63 actions,
64 }
65}
66
67fn build_handler_policy(toml: TomlHandlerPolicy) -> OwnedPolicy {
68 build_policy(
69 toml.standalone,
70 toml.valued,
71 toml.bare,
72 toml.max_positional,
73 toml.tolerate_unknown_short,
74 toml.tolerate_unknown_long,
75 toml.numeric_dash,
76 )
77}
78
79fn build_fallback(parent: &str, toml: TomlFallback) -> FallbackSpec {
80 let policy = build_policy(
81 toml.standalone,
82 toml.valued,
83 toml.bare,
84 toml.max_positional,
85 toml.tolerate_unknown_short,
86 toml.tolerate_unknown_long,
87 toml.numeric_dash,
88 );
89 let level: SafetyLevel = toml.level.unwrap_or(TomlLevel::Inert).into();
90 let positional_shape = toml.positional_shape.as_deref().map(|name| {
91 crate::policy::PositionalShape::from_name(name).unwrap_or_else(|| {
92 panic!(
93 "{}: unknown fallback positional_shape `{}` (known: path)",
94 parent, name
95 )
96 })
97 });
98 FallbackSpec {
99 policy,
100 level,
101 positional_shape,
102 }
103}
104
105fn allow_all_policy() -> OwnedPolicy {
106 OwnedPolicy {
107 standalone: Vec::new(),
108 valued: Vec::new(),
109 bare: true,
110 max_positional: None,
111 tolerance: FlagTolerance { unknown: UnknownTolerance::Both, numeric_dash: false },
112 }
113}
114
115fn check_no_legacy_positional_style(name: &str, ps: Option<bool>) {
116 if ps.is_some() {
117 panic!(
118 "command '{name}': `positional_style` was removed. Use \
119 `tolerate_unknown_short = true` for tools with single-dash \
120 flags (pdftotext -help, sample -mayDie). Use \
121 `tolerate_unknown_long = true` ONLY for tools whose \
122 double-dash flag surface is genuinely unbounded (AWS CLI \
123 style); double-dash unknowns silently pass when this is \
124 on, which has caused safety bugs. Most tools need neither."
125 );
126 }
127}
128
129fn filter_candidates(subs: Vec<TomlSub>) -> impl Iterator<Item = TomlSub> {
130 subs.into_iter().filter(|s| !s.candidate.unwrap_or(false))
131}
132
133pub(super) fn build_subs(
138 parent: &str,
139 toml: TomlSub,
140 handler_policies: &std::collections::HashMap<String, OwnedPolicy>,
141) -> Vec<SubSpec> {
142 let aliases = toml.aliases.clone();
143 let canonical = build_sub(parent, toml, handler_policies);
144 let mut out = Vec::with_capacity(1 + aliases.len());
145 for alias in aliases {
146 out.push(SubSpec {
147 name: alias,
148 kind: canonical.kind.clone(),
149 policy_ref: canonical.policy_ref.clone(),
150 eval_safe: canonical.eval_safe,
151 eval_safe_flags: canonical.eval_safe_flags.clone(),
152 eval_safe_flag_values: canonical.eval_safe_flag_values.clone(),
153 eval_safe_required_flags: canonical.eval_safe_required_flags.clone(),
154 });
155 }
156 out.push(canonical);
157 out
158}
159
160pub(super) fn build_sub(
161 parent: &str,
162 mut toml: TomlSub,
163 handler_policies: &std::collections::HashMap<String, OwnedPolicy>,
164) -> SubSpec {
165 check_no_legacy_positional_style(&toml.name, toml.positional_style);
166 let name = toml.name.clone();
167 let policy_ref = toml.policy.clone();
168 let eval_safe = toml.eval_safe.unwrap_or(false);
169 let eval_safe_flags = std::mem::take(&mut toml.eval_safe_flags);
170 let eval_safe_flag_values = std::mem::take(&mut toml.eval_safe_flag_values);
171 let eval_safe_required_flags = std::mem::take(&mut toml.eval_safe_required_flags);
172 let valued_for_check = toml.valued.clone();
173 assert_eval_safe_flags_require_tag(parent, &name, eval_safe, &eval_safe_flags);
174 assert_eval_safe_flag_values_consistent(parent, &name, &eval_safe_flags, &eval_safe_flag_values);
175 assert_eval_safe_valued_flags_declared(parent, &name, &eval_safe_flags, &valued_for_check, &eval_safe_flag_values);
176 assert_eval_safe_required_flags_consistent(parent, &name, &eval_safe_flags, &eval_safe_required_flags);
177 assert_sub_eval_safe_only_on_leaf(parent, &toml);
178 SubSpec {
179 name,
180 kind: build_sub_kind(parent, toml, handler_policies),
181 policy_ref,
182 eval_safe,
183 eval_safe_flags,
184 eval_safe_flag_values,
185 eval_safe_required_flags,
186 }
187}
188
189fn assert_eval_safe_flag_values_consistent(
190 parent: &str,
191 name: &str,
192 eval_safe_flags: &[String],
193 eval_safe_flag_values: &std::collections::HashMap<String, Vec<String>>,
194) {
195 for (flag, values) in eval_safe_flag_values {
196 if !eval_safe_flags.iter().any(|f| f == flag) {
197 panic!(
198 "command '{parent}' sub `{name}` lists `{flag}` in \
199 `eval_safe_flag_values` but not in `eval_safe_flags`. \
200 A value allowlist only takes effect when the flag itself \
201 is allowed. Add `{flag}` to `eval_safe_flags` or remove \
202 the value entry."
203 );
204 }
205 for value in values {
206 if value.is_empty() || !value.chars().all(is_bare_literal_char) {
207 panic!(
208 "command '{parent}' sub `{name}` has eval_safe_flag_values \
209 for `{flag}` containing value `{value:?}` with characters \
210 outside `[a-zA-Z0-9_./=-]`. The allowed-value set must \
211 itself be bare-literal so it can never embed a shell-\
212 expansion trigger into the substituted invocation."
213 );
214 }
215 }
216 }
217}
218
219fn is_bare_literal_char(c: char) -> bool {
220 c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.' | '/' | '=')
221}
222
223fn assert_eval_safe_required_flags_consistent(
224 parent: &str,
225 name: &str,
226 eval_safe_flags: &[String],
227 eval_safe_required_flags: &[String],
228) {
229 for flag in eval_safe_required_flags {
230 if !eval_safe_flags.iter().any(|f| f == flag) {
231 panic!(
232 "command '{parent}' sub `{name}` lists `{flag}` in \
233 `eval_safe_required_flags` but not in `eval_safe_flags`. \
234 A required-flag constraint must be a subset of the \
235 allowed-flag set — otherwise the flag is required AND \
236 immediately denied. Add `{flag}` to `eval_safe_flags` or \
237 remove it from `eval_safe_required_flags`."
238 );
239 }
240 }
241}
242
243fn assert_eval_safe_valued_flags_declared(
251 parent: &str,
252 name: &str,
253 eval_safe_flags: &[String],
254 valued: &[String],
255 eval_safe_flag_values: &std::collections::HashMap<String, Vec<String>>,
256) {
257 for flag in eval_safe_flags {
258 if !valued.iter().any(|v| v == flag) {
259 continue;
260 }
261 if !eval_safe_flag_values.contains_key(flag) {
262 panic!(
263 "command '{parent}' sub `{name}` lists `{flag}` in \
264 `eval_safe_flags` AND in `valued`, but `{flag}` has no \
265 entry in `eval_safe_flag_values`. Every valued flag \
266 tagged eval-safe must declare its value posture: \
267 either a concrete allowlist of safe values \
268 (`{flag} = [\"value-a\", \"value-b\"]`) or the \
269 explicit-unrestricted form (`{flag} = []`) signaling \
270 the contributor vetted that any value preserves shell-\
271 init output. Omitting an entry means the walker can't \
272 tell whether the value-following-flag is supposed to \
273 be checked, and a future short alias of `{flag}` \
274 could silently widen the eval-safe surface."
275 );
276 }
277 }
278}
279
280fn assert_eval_safe_flags_require_tag(parent: &str, name: &str, eval_safe: bool, flags: &[String]) {
281 if !flags.is_empty() && !eval_safe {
282 panic!(
283 "command '{parent}' sub `{name}` declares `eval_safe_flags` without \
284 `eval_safe = true`. The flag allowlist only takes effect when the \
285 sub is tagged eval-safe. Add `eval_safe = true` or drop \
286 `eval_safe_flags`."
287 );
288 }
289}
290
291fn assert_sub_eval_safe_only_on_leaf(parent: &str, toml: &TomlSub) {
292 if toml.eval_safe != Some(true) {
293 return;
294 }
295 if !toml.sub.is_empty() {
296 panic!(
297 "command '{parent}' sub `{}` sets `eval_safe = true` AND has \
298 nested [[command.sub.sub]] blocks. eval_safe must be tagged on \
299 a leaf node — move the tag onto the specific sub-sub that emits \
300 shell-init code. Otherwise the walker accepts any unmatched \
301 sub-sub name as a positional, which is counter-intuitive.",
302 toml.name,
303 );
304 }
305 if toml.handler.is_some() {
306 panic!(
307 "command '{parent}' sub `{}` sets `eval_safe = true` AND \
308 `handler = \"...\"`. Handler-based subs run Rust dispatch logic \
309 whose shape the eval walker cannot introspect — eval-safety \
310 requires a declarative leaf the registry can reason about.",
311 toml.name,
312 );
313 }
314 if toml.delegate_after.is_some() || toml.delegate_skip.is_some() {
315 panic!(
316 "command '{parent}' sub `{}` sets `eval_safe = true` AND \
317 delegates to an inner command (delegate_after / delegate_skip). \
318 The inner command's output is unrelated to this sub's vetting \
319 — drop `eval_safe`.",
320 toml.name,
321 );
322 }
323}
324
325fn assert_eval_safe_tagged_command_has_researched_version(toml: &TomlCommand) {
326 let command_tagged = toml.eval_safe == Some(true);
327 let any_sub_tagged = toml_has_any_eval_safe_sub(&toml.sub);
328 if !command_tagged && !any_sub_tagged {
329 return;
330 }
331 if toml.researched_version.is_none() {
332 panic!(
333 "command '{}' has `eval_safe = true` (on the command or a sub) \
334 but no `researched_version`. eval-safe tags pin a per-tag trust \
335 claim against a specific upstream snapshot — add the version \
336 you researched (e.g. `researched_version = \"v2026.5.3\"`) so \
337 the next contributor knows what to diff against.",
338 toml.name,
339 );
340 }
341}
342
343fn toml_has_any_eval_safe_sub(subs: &[TomlSub]) -> bool {
344 subs.iter().any(|s| s.eval_safe == Some(true) || toml_has_any_eval_safe_sub(&s.sub))
345}
346
347fn assert_command_eval_safe_only_on_leaf(toml: &TomlCommand) {
348 if toml.eval_safe != Some(true) {
349 return;
350 }
351 if !toml.sub.is_empty() {
352 panic!(
353 "command '{}' sets `eval_safe = true` at the command level AND \
354 has [[command.sub]] blocks. Move the tag onto the specific \
355 sub that emits shell-init code (e.g. `mise activate`) — \
356 command-level tagging on a structured command is counter-\
357 intuitive.",
358 toml.name,
359 );
360 }
361 if toml.wrapper.is_some() {
370 panic!(
371 "command '{}' sets `eval_safe = true` AND `[command.wrapper]`. \
372 Wrappers forward to an inner command — tagging the wrapper \
373 would tag every wrapped invocation. Drop `eval_safe`.",
374 toml.name,
375 );
376 }
377 if toml.deny.unwrap_or(false) {
378 panic!(
379 "command '{}' sets both `deny = true` and `eval_safe = true`. \
380 These are contradictory — deny silently dominates. Drop one.",
381 toml.name,
382 );
383 }
384}
385
386fn build_sub_kind(
387 parent: &str,
388 toml: TomlSub,
389 handler_policies: &std::collections::HashMap<String, OwnedPolicy>,
390) -> DispatchKind {
391 if let Some(handler_name) = toml.handler {
392 return DispatchKind::Custom {
393 handler_name,
394 doc_body: toml.doc_body,
395 subs: Vec::new(),
396 fallback: None,
397 handler_policies: std::collections::HashMap::new(),
398 matrices: Vec::new(),
399 };
400 }
401 if toml.allow_all.unwrap_or(false) {
402 return DispatchKind::Policy {
403 policy: allow_all_policy(),
404 level: toml.level.unwrap_or(TomlLevel::Inert).into(),
405 };
406 }
407 if let Some(sep) = toml.delegate_after {
408 return DispatchKind::DelegateAfterSeparator { separator: sep };
409 }
410 if let Some(skip) = toml.delegate_skip {
411 return DispatchKind::DelegateSkip { skip };
412 }
413 if !toml.sub.is_empty() {
414 return DispatchKind::Branching {
415 subs: filter_candidates(toml.sub)
416 .flat_map(|s| build_subs(parent, s, handler_policies))
417 .collect(),
418 bare_flags: Vec::new(),
419 bare_ok: toml.nested_bare.unwrap_or(false),
420 pre_standalone: toml.standalone,
421 pre_valued: toml.valued,
422 first_arg: Vec::new(),
423 first_arg_level: SafetyLevel::Inert,
424 };
425 }
426 build_policy_sub_kind(parent, toml, handler_policies)
427}
428
429fn build_policy_sub_kind(
430 parent: &str,
431 toml: TomlSub,
432 handler_policies: &std::collections::HashMap<String, OwnedPolicy>,
433) -> DispatchKind {
434 let policy = if let Some(key) = &toml.policy {
435 if !toml.standalone.is_empty() || !toml.valued.is_empty() {
436 panic!(
437 "command '{parent}' sub `{}` sets both `policy = \"{}\"` and \
438 inline standalone/valued — pick one. Either drop the inline \
439 lists (and rely on the referenced handler_policy) or drop \
440 the `policy` field.",
441 toml.name, key,
442 );
443 }
444 handler_policies.get(key).cloned().unwrap_or_else(|| {
445 panic!(
446 "command '{parent}' sub `{}` references handler_policy \
447 `{key}` which is not declared. Add a \
448 [command.handler_policy.{key}] block or fix the typo.",
449 toml.name,
450 )
451 })
452 } else {
453 build_policy(
454 toml.standalone,
455 toml.valued,
456 toml.bare,
457 toml.max_positional,
458 toml.tolerate_unknown_short,
459 toml.tolerate_unknown_long,
460 toml.numeric_dash,
461 )
462 };
463 let level: SafetyLevel = toml.level.unwrap_or(TomlLevel::Inert).into();
464 if !toml.write_flags.is_empty() {
465 return DispatchKind::WriteFlagged {
466 policy,
467 base_level: level,
468 write_flags: toml.write_flags,
469 };
470 }
471 if let Some(guard) = toml.guard {
472 let mut require_any = vec![guard];
473 if let Some(short) = toml.guard_short {
474 require_any.push(short);
475 }
476 return DispatchKind::RequireAny {
477 require_any,
478 policy,
479 level,
480 accept_bare_help: true,
481 };
482 }
483 if !toml.first_arg.is_empty() {
484 return DispatchKind::FirstArg { patterns: toml.first_arg, level };
485 }
486 if !toml.require_any.is_empty() {
487 return DispatchKind::RequireAny {
488 require_any: toml.require_any,
489 policy,
490 level,
491 accept_bare_help: false,
492 };
493 }
494 DispatchKind::Policy { policy, level }
495}
496
497fn assert_flat_or_structured(toml: &TomlCommand) {
505 if toml.sub.is_empty() {
506 return;
507 }
508 let mut conflicts = Vec::new();
509 if !toml.standalone.is_empty() {
510 conflicts.push("standalone");
511 }
512 if !toml.valued.is_empty() {
513 conflicts.push("valued");
514 }
515 if toml.max_positional.is_some() {
516 conflicts.push("max_positional");
517 }
518 if toml.tolerate_unknown_short.is_some() {
519 conflicts.push("tolerate_unknown_short");
520 }
521 if toml.tolerate_unknown_long.is_some() {
522 conflicts.push("tolerate_unknown_long");
523 }
524 if toml.numeric_dash.is_some() {
525 conflicts.push("numeric_dash");
526 }
527 if !conflicts.is_empty() {
528 panic!(
529 "command '{}' mixes flat-style top-level fields ({}) with [[command.sub]] blocks. \
530 When subs are present these fields are silently dropped. \
531 Either drop the subs (if the command is flat) or move global \
532 flags into a [command.wrapper] block.",
533 toml.name,
534 conflicts.join(", "),
535 );
536 }
537}
538
539fn assert_matrix_policy_keys_exist(toml: &TomlCommand) {
540 if toml.matrix.is_empty() {
541 return;
542 }
543 for matrix in &toml.matrix {
544 for (action_name, action) in &matrix.actions {
545 let policy_key = match action {
546 TomlMatrixAction::Policy(k) => k,
547 TomlMatrixAction::Detailed(d) => &d.policy,
548 };
549 if !toml.handler_policy.contains_key(policy_key) {
550 panic!(
551 "command '{}' matrix action `{}` references \
552 handler_policy `{}` which is not declared. \
553 Add a [command.handler_policy.{}] block or fix the typo.",
554 toml.name, action_name, policy_key, policy_key,
555 );
556 }
557 }
558 }
559}
560
561fn assert_matrix_no_duplicate_parent_action(toml: &TomlCommand) {
562 if toml.matrix.len() < 2 {
563 return;
564 }
565 let mut seen: std::collections::HashSet<(String, String)> = std::collections::HashSet::new();
566 for matrix in &toml.matrix {
567 for parent in &matrix.parents {
568 for action in matrix.actions.keys() {
569 let key = (parent.clone(), action.clone());
570 if !seen.insert(key) {
571 panic!(
572 "command '{}' matrix has duplicate (parent, action) pair \
573 (`{}`, `{}`). The first match would silently win — \
574 consolidate into one matrix block or remove the duplicate.",
575 toml.name, parent, action,
576 );
577 }
578 }
579 }
580 }
581}
582
583fn assert_fallback_requires_handler(toml: &TomlCommand) {
584 if toml.fallback.is_some() && toml.handler.is_none() {
585 panic!(
586 "command '{}' declares [command.fallback] without a handler. \
587 Fallback grammars are only consulted via \
588 registry::try_fallback_grammar() from a Rust handler — without \
589 handler = \"...\" the block is silently dropped. \
590 Either set handler or remove [command.fallback].",
591 toml.name,
592 );
593 }
594}
595
596#[allow(clippy::too_many_lines)]
597pub(super) fn build_command(toml: TomlCommand, category: &str) -> CommandSpec {
598 assert_flat_or_structured(&toml);
599 assert_fallback_requires_handler(&toml);
600 assert_matrix_policy_keys_exist(&toml);
601 assert_matrix_no_duplicate_parent_action(&toml);
602 assert_command_eval_safe_only_on_leaf(&toml);
603 assert_eval_safe_tagged_command_has_researched_version(&toml);
604 check_no_legacy_positional_style(&toml.name, toml.positional_style);
605 let cat = category.to_string();
606 let desc = toml.description.unwrap_or_default();
607 let researched_version = toml.researched_version;
608 let examples_safe = toml.examples_safe;
609 let examples_denied = toml.examples_denied;
610 let eval_safe = toml.eval_safe.unwrap_or(false);
611 let eval_safe_flags = toml.eval_safe_flags;
612 let eval_safe_flag_values = toml.eval_safe_flag_values;
613 let eval_safe_required_flags = toml.eval_safe_required_flags;
614 if !eval_safe_flags.is_empty() && !eval_safe {
615 panic!(
616 "command '{}' declares `eval_safe_flags` without `eval_safe = true`. \
617 The flag allowlist only takes effect when the command is tagged \
618 eval-safe. Add `eval_safe = true` or drop `eval_safe_flags`.",
619 toml.name,
620 );
621 }
622 assert_eval_safe_flag_values_consistent(
623 &toml.name,
624 "<command>",
625 &eval_safe_flags,
626 &eval_safe_flag_values,
627 );
628 assert_eval_safe_valued_flags_declared(
629 &toml.name,
630 "<command>",
631 &eval_safe_flags,
632 &toml.valued,
633 &eval_safe_flag_values,
634 );
635 assert_eval_safe_required_flags_consistent(
636 &toml.name,
637 "<command>",
638 &eval_safe_flags,
639 &eval_safe_required_flags,
640 );
641 if toml.deny.unwrap_or(false) {
642 return CommandSpec {
643 name: toml.name,
644 description: desc,
645 aliases: toml.aliases,
646 url: toml.url,
647 category: cat,
648 researched_version,
649 examples_safe,
650 examples_denied,
651 eval_safe,
652 eval_safe_flags: eval_safe_flags.clone(),
653 eval_safe_flag_values: eval_safe_flag_values.clone(),
654 eval_safe_required_flags: eval_safe_required_flags.clone(),
655 kind: DispatchKind::Policy {
656 policy: OwnedPolicy {
657 standalone: Vec::new(),
658 valued: Vec::new(),
659 bare: false,
660 max_positional: Some(0),
661 tolerance: FlagTolerance::default(),
662 },
663 level: SafetyLevel::Inert,
664 },
665 };
666 }
667 if let Some(handler_name) = toml.handler {
668 let handler_policies: std::collections::HashMap<String, OwnedPolicy> = toml
671 .handler_policy
672 .into_iter()
673 .map(|(k, v)| (k, build_handler_policy(v)))
674 .collect();
675 let parent_name = toml.name.clone();
676 let subs = filter_candidates(toml.sub)
677 .flat_map(|s| build_subs(&parent_name, s, &handler_policies))
678 .collect();
679 let fallback = toml.fallback.map(|f| build_fallback(&toml.name, f));
680 let matrices = toml
681 .matrix
682 .into_iter()
683 .map(build_matrix)
684 .collect();
685 return CommandSpec {
686 name: toml.name,
687 description: desc,
688 aliases: toml.aliases,
689 url: toml.url,
690 category: cat,
691 researched_version,
692 examples_safe,
693 examples_denied,
694 eval_safe,
695 eval_safe_flags: eval_safe_flags.clone(),
696 eval_safe_flag_values: eval_safe_flag_values.clone(),
697 eval_safe_required_flags: eval_safe_required_flags.clone(),
698 kind: DispatchKind::Custom {
699 handler_name,
700 doc_body: toml.doc_body,
701 subs,
702 fallback,
703 handler_policies,
704 matrices,
705 },
706 };
707 }
708
709 if let Some(w) = toml.wrapper {
710 if !toml.sub.is_empty() || !toml.bare_flags.is_empty() {
711 let first_arg_level = toml.level.unwrap_or(TomlLevel::Inert).into();
712 let parent_name = toml.name.clone();
713 return CommandSpec {
714 name: toml.name,
715 description: desc,
716 aliases: toml.aliases,
717 url: toml.url,
718 category: cat,
719 researched_version,
720 examples_safe,
721 examples_denied,
722 eval_safe,
723 eval_safe_flags: eval_safe_flags.clone(),
724 eval_safe_flag_values: eval_safe_flag_values.clone(),
725 eval_safe_required_flags: eval_safe_required_flags.clone(),
726 kind: DispatchKind::Branching {
727 bare_flags: toml.bare_flags,
728 subs: filter_candidates(toml.sub)
729 .flat_map(|s| build_subs(&parent_name, s, &std::collections::HashMap::new()))
730 .collect(),
731 pre_standalone: w.standalone,
732 pre_valued: w.valued,
733 bare_ok: toml.bare.unwrap_or(false),
734 first_arg: toml.first_arg,
735 first_arg_level,
736 },
737 };
738 }
739 return CommandSpec {
740 name: toml.name,
741 description: desc,
742 aliases: toml.aliases,
743 url: toml.url,
744 category: cat,
745 researched_version,
746 examples_safe,
747 examples_denied,
748 eval_safe,
749 eval_safe_flags: eval_safe_flags.clone(),
750 eval_safe_flag_values: eval_safe_flag_values.clone(),
751 eval_safe_required_flags: eval_safe_required_flags.clone(),
752 kind: DispatchKind::Wrapper {
753 standalone: w.standalone,
754 valued: w.valued,
755 positional_skip: w.positional_skip.unwrap_or(0),
756 separator: w.separator,
757 bare_ok: w.bare_ok.unwrap_or(false),
758 },
759 };
760 }
761
762 if !toml.sub.is_empty() || !toml.bare_flags.is_empty() {
763 let first_arg_level = toml.level.unwrap_or(TomlLevel::Inert).into();
764 let parent_name = toml.name.clone();
765 return CommandSpec {
766 name: toml.name,
767 description: desc,
768 aliases: toml.aliases,
769 url: toml.url,
770 category: cat,
771 researched_version,
772 examples_safe,
773 examples_denied,
774 eval_safe,
775 eval_safe_flags: eval_safe_flags.clone(),
776 eval_safe_flag_values: eval_safe_flag_values.clone(),
777 eval_safe_required_flags: eval_safe_required_flags.clone(),
778 kind: DispatchKind::Branching {
779 bare_flags: toml.bare_flags,
780 subs: filter_candidates(toml.sub)
781 .flat_map(|s| build_subs(&parent_name, s, &std::collections::HashMap::new()))
782 .collect(),
783 pre_standalone: Vec::new(),
784 pre_valued: Vec::new(),
785 bare_ok: toml.bare.unwrap_or(false),
786 first_arg: toml.first_arg,
787 first_arg_level,
788 },
789 };
790 }
791
792 let policy = build_policy(
793 toml.standalone,
794 toml.valued,
795 toml.bare,
796 toml.max_positional,
797 toml.tolerate_unknown_short,
798 toml.tolerate_unknown_long,
799 toml.numeric_dash,
800 );
801
802 let level = toml.level.unwrap_or(TomlLevel::Inert).into();
803
804 if !toml.first_arg.is_empty() {
805 return CommandSpec {
806 name: toml.name,
807 description: desc,
808 aliases: toml.aliases,
809 url: toml.url,
810 category: cat,
811 researched_version,
812 examples_safe,
813 examples_denied,
814 eval_safe,
815 eval_safe_flags: eval_safe_flags.clone(),
816 eval_safe_flag_values: eval_safe_flag_values.clone(),
817 eval_safe_required_flags: eval_safe_required_flags.clone(),
818 kind: DispatchKind::FirstArg {
819 patterns: toml.first_arg,
820 level,
821 },
822 };
823 }
824
825 if !toml.write_flags.is_empty() {
826 return CommandSpec {
827 name: toml.name,
828 description: desc,
829 aliases: toml.aliases,
830 url: toml.url,
831 category: cat,
832 researched_version,
833 examples_safe,
834 examples_denied,
835 eval_safe,
836 eval_safe_flags: eval_safe_flags.clone(),
837 eval_safe_flag_values: eval_safe_flag_values.clone(),
838 eval_safe_required_flags: eval_safe_required_flags.clone(),
839 kind: DispatchKind::WriteFlagged {
840 policy,
841 base_level: level,
842 write_flags: toml.write_flags,
843 },
844 };
845 }
846
847 if !toml.require_any.is_empty() {
848 return CommandSpec {
849 name: toml.name,
850 description: desc,
851 aliases: toml.aliases,
852 url: toml.url,
853 category: cat,
854 researched_version,
855 examples_safe,
856 examples_denied,
857 eval_safe,
858 eval_safe_flags: eval_safe_flags.clone(),
859 eval_safe_flag_values: eval_safe_flag_values.clone(),
860 eval_safe_required_flags: eval_safe_required_flags.clone(),
861 kind: DispatchKind::RequireAny {
862 require_any: toml.require_any,
863 policy,
864 level,
865 accept_bare_help: false,
866 },
867 };
868 }
869
870 CommandSpec {
871 name: toml.name,
872 description: desc,
873 aliases: toml.aliases,
874 url: toml.url,
875 category: cat,
876 researched_version,
877 examples_safe,
878 examples_denied,
879 eval_safe,
880 eval_safe_flags,
881 eval_safe_flag_values,
882 eval_safe_required_flags,
883 kind: DispatchKind::Policy {
884 policy,
885 level,
886 },
887 }
888}
889
890pub fn load_toml(source: &str, category: &str) -> Vec<CommandSpec> {
891 let file: TomlFile = match toml::from_str(source) {
892 Ok(f) => f,
893 Err(e) => {
894 let preview: String = source.chars().take(80).collect();
895 panic!("invalid TOML command definition: {e}\n source begins: {preview}");
896 }
897 };
898 file.command.into_iter()
899 .filter(|cmd| !cmd.candidate.unwrap_or(false))
900 .map(|cmd| build_command(cmd, category))
901 .collect()
902}
903
904pub fn build_registry(specs: Vec<CommandSpec>) -> HashMap<String, CommandSpec> {
905 let mut map = HashMap::new();
906 for spec in specs {
907 insert_spec(&mut map, spec);
908 }
909 map
910}
911
912pub fn insert_spec(map: &mut HashMap<String, CommandSpec>, spec: CommandSpec) {
918 map.retain(|_, s| s.name != spec.name);
919 for alias in &spec.aliases {
920 map.insert(alias.clone(), CommandSpec {
921 name: spec.name.clone(),
922 description: spec.description.clone(),
923 aliases: vec![],
924 url: spec.url.clone(),
925 category: spec.category.clone(),
926 researched_version: spec.researched_version.clone(),
927 examples_safe: vec![],
928 examples_denied: vec![],
929 eval_safe: spec.eval_safe,
930 eval_safe_flags: spec.eval_safe_flags.clone(),
931 eval_safe_flag_values: spec.eval_safe_flag_values.clone(),
932 eval_safe_required_flags: spec.eval_safe_required_flags.clone(),
933 kind: spec.kind.clone(),
934 });
935 }
936 map.insert(spec.name.clone(), spec);
937}