1use crate::kdl::{KdlEntry, KdlNode, KdlValue};
20use serde::Serialize;
21
22use crate::error::Result;
23use crate::spec::cmd::SpecCommand;
24use crate::spec::context::ParsingContext;
25use crate::spec::helpers::{string_entry, NodeHelper};
26use crate::spec::Spec;
27
28#[derive(Debug, Default, Clone, Serialize)]
30#[non_exhaustive]
31pub struct SpecExitCode {
32 pub code: i64,
33 pub help: String,
34}
35
36impl SpecExitCode {
37 pub fn new(code: i64, help: impl Into<String>) -> Self {
38 Self {
39 code,
40 help: help.into(),
41 }
42 }
43
44 pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self> {
45 node.ensure_arg_len(2..=2)?;
46 let entry = node.arg(0)?;
47 let Some(code) = entry.value.as_integer() else {
48 bail_parse!(ctx, entry.entry.span(), "an exit code must be a number");
49 };
50 if !(0..=255).contains(&code) {
55 bail_parse!(
56 ctx,
57 entry.entry.span(),
58 "exit code {code} is outside 0-255; a POSIX shell reports the low byte, so \
59 anything higher is indistinguishable from {}",
60 code & 0xff
61 );
62 }
63 let help = node.arg(1)?.ensure_string()?;
64 if help.is_empty() {
65 bail_parse!(
66 ctx,
67 node.span(),
68 "exit code {code} needs a description; an undocumented code is what the \
69 declaration exists to replace"
70 );
71 }
72 Ok(SpecExitCode {
73 code: code as i64,
74 help,
75 })
76 }
77}
78
79impl From<&SpecExitCode> for KdlNode {
80 fn from(exit_code: &SpecExitCode) -> KdlNode {
81 let mut node = KdlNode::new("exit_code");
82 node.push(KdlEntry::new(KdlValue::Integer(exit_code.code as i128)));
83 node.push(string_entry(None, &exit_code.help));
84 node
85 }
86}
87
88pub fn effective_exit_codes(spec: &Spec, path: &[SpecCommand]) -> Vec<SpecExitCode> {
96 effective_exit_codes_ref(spec, path.iter())
97}
98
99pub fn effective_exit_codes_ref<'a>(
101 spec: &Spec,
102 path: impl IntoIterator<Item = &'a SpecCommand>,
103) -> Vec<SpecExitCode> {
104 let mut out: Vec<SpecExitCode> = spec.exit_codes.clone();
105 for cmd in path {
106 for code in &cmd.exit_codes {
107 match out.iter_mut().find(|e| e.code == code.code) {
108 Some(existing) => *existing = code.clone(),
109 None => out.push(code.clone()),
110 }
111 }
112 }
113 out
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::error::UsageErr;
120
121 fn parse(src: &str) -> Spec {
122 src.parse().expect("the fixture should parse")
123 }
124
125 fn error(src: &str) -> String {
126 match src.parse::<Spec>().expect_err("should be rejected") {
127 UsageErr::InvalidInput(msg, ..) => msg,
128 other => other.to_string(),
129 }
130 }
131
132 #[test]
133 fn a_command_refines_the_cli_wide_table_rather_than_replacing_it() {
134 let spec = parse(
135 r#"
136name "ex"
137exit_code 0 "ok"
138exit_code 1 "error"
139exit_code 130 "interrupted"
140cmd "check" {
141 exit_code 1 "a check failed"
142 exit_code 2 "configuration error"
143}
144"#,
145 );
146 let check = spec.cmd.subcommands["check"].clone();
147 let codes: Vec<(i64, String)> = effective_exit_codes(&spec, &[check])
148 .into_iter()
149 .map(|e| (e.code, e.help))
150 .collect();
151 assert_eq!(
155 codes,
156 vec![
157 (0, "ok".to_string()),
158 (1, "a check failed".to_string()),
159 (130, "interrupted".to_string()),
160 (2, "configuration error".to_string()),
161 ]
162 );
163 }
164
165 #[test]
166 fn a_code_outside_a_byte_says_what_a_shell_would_report() {
167 let err = error("name \"ex\"\nexit_code 256 \"nope\"\n");
168 assert!(err.contains("outside 0-255"), "{err}");
169 assert!(err.contains("indistinguishable from 0"), "{err}");
170 }
171
172 #[test]
173 fn a_code_needs_a_description() {
174 assert!(error("name \"ex\"\nexit_code 1 \"\"\n").contains("needs a description"));
175 }
176
177 #[test]
178 fn codes_survive_a_round_trip() {
179 let src = "name \"ex\"\nexit_code 0 ok\ncmd \"go\" {\n exit_code 3 \"broke\"\n}\n";
180 let spec = parse(src);
181 let again = parse(&spec.to_string());
182 assert_eq!(spec.to_string(), again.to_string());
183 assert_eq!(again.exit_codes[0].code, 0);
184 assert_eq!(again.cmd.subcommands["go"].exit_codes[0].help, "broke");
185 }
186}