1use crate::{
23 diagnostic,
24 exit_code::ExitCode,
25 json::{JsonEnvelope, JsonMessage, print_json},
26};
27use clap::{CommandFactory, Parser, error::ErrorKind};
28use serde_json::json;
29use std::sync::atomic::{AtomicBool, Ordering};
30
31static MACHINE_MODE: AtomicBool = AtomicBool::new(false);
32
33pub fn is_machine() -> bool {
37 MACHINE_MODE.load(Ordering::Relaxed)
38}
39
40pub(crate) fn set_machine(on: bool) {
44 MACHINE_MODE.store(on, Ordering::Relaxed);
45}
46
47pub fn check_machine() {
53 if crate::opts::pre_parse_global_flag_present("--machine") {
54 set_machine(true);
55 }
56}
57
58pub fn parse_or_exit<T: Parser + CommandFactory>() -> T {
66 if is_machine() {
67 let mut cmd = T::command().color(clap::ColorChoice::Never);
71 let mut matches = match cmd.try_get_matches_from_mut(std::env::args_os()) {
72 Ok(m) => m,
73 Err(err) => handle_machine_clap_error(err),
74 };
75 match T::from_arg_matches_mut(&mut matches) {
76 Ok(t) => t,
77 Err(err) => handle_machine_clap_error(err),
78 }
79 } else {
80 match T::try_parse() {
81 Ok(t) => t,
82 Err(err) => err.exit(),
83 }
84 }
85}
86
87fn handle_machine_clap_error(err: clap::Error) -> ! {
97 let exit = exit_code_for_clap_error(&err);
98 match err.kind() {
99 ErrorKind::DisplayHelp => {
100 let rendered = err.render().to_string();
101 let envelope = JsonEnvelope::success(json!({ "help": rendered }));
102 let _ = print_json(&envelope);
103 }
104 ErrorKind::DisplayVersion => {
105 let rendered = err.render().to_string();
106 let envelope = JsonEnvelope::success(json!({ "version": rendered }));
107 let _ = print_json(&envelope);
108 }
109 _ => {
110 let message = err.to_string();
114 let envelope =
115 JsonEnvelope::error(JsonMessage::error(diagnostic::cli::USAGE_INVALID, message));
116 let _ = print_json(&envelope);
117 }
118 }
119 std::process::exit(exit.to_i32());
120}
121
122fn exit_code_for_clap_error(err: &clap::Error) -> ExitCode {
129 match err.kind() {
130 ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => ExitCode::Success,
131 _ => ExitCode::Usage,
132 }
133}
134
135pub fn bail_machine_usage(message: impl Into<String>) -> ! {
139 let envelope = JsonEnvelope::error(JsonMessage::error(diagnostic::cli::USAGE_INVALID, message));
140 let _ = print_json(&envelope);
141 std::process::exit(ExitCode::Usage.to_i32());
142}
143
144pub fn bail_machine_usage_with_details(
147 message: impl Into<String>,
148 details: serde_json::Value,
149) -> ! {
150 let envelope = JsonEnvelope::error(
151 JsonMessage::error(diagnostic::cli::USAGE_INVALID, message).with_details(details),
152 );
153 let _ = print_json(&envelope);
154 std::process::exit(ExitCode::Usage.to_i32());
155}
156
157pub fn report_machine_error(report: &eyre::Report) {
161 let cause_chain: Vec<String> = report.chain().map(ToString::to_string).collect();
162 let message = cause_chain.first().cloned().unwrap_or_else(|| report.to_string());
163 let envelope = JsonEnvelope::error(
164 JsonMessage::error(diagnostic::cli::UNKNOWN, message)
165 .with_details(json!({ "cause_chain": cause_chain })),
166 );
167 let _ = print_json(&envelope);
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn machine_flag_default_off() {
176 set_machine(false);
177 assert!(!is_machine());
178 }
179
180 #[test]
181 fn machine_flag_can_be_toggled() {
182 set_machine(true);
183 assert!(is_machine());
184 set_machine(false);
185 assert!(!is_machine());
186 }
187
188 #[derive(Debug, Parser)]
189 #[command(name = "demo", version = "0.1.0")]
190 struct Demo {
191 #[arg(long)]
192 name: Option<String>,
193 #[command(subcommand)]
194 cmd: Option<DemoSub>,
195 }
196
197 #[derive(Debug, clap::Subcommand)]
198 enum DemoSub {
199 Build {
201 #[arg(long)]
202 path: Option<String>,
203 },
204 }
205
206 #[derive(Debug, Parser)]
207 #[command(
208 name = "strict",
209 version = "0.1.0",
210 subcommand_required = true,
211 arg_required_else_help = true
212 )]
213 struct Strict {
214 #[command(subcommand)]
215 cmd: StrictSub,
216 }
217
218 #[derive(Debug, clap::Subcommand)]
219 enum StrictSub {
220 Run,
221 }
222
223 #[test]
226 fn report_machine_error_uses_cli_unknown_and_preserves_cause_chain() {
227 use eyre::WrapErr as _;
228 let leaf: eyre::Report = eyre::eyre!("solc: missing semicolon");
229 let report: eyre::Report = Result::<(), _>::Err(leaf)
230 .wrap_err("compile failed")
231 .wrap_err("build failed")
232 .unwrap_err();
233
234 let cause_chain: Vec<String> = report.chain().map(ToString::to_string).collect();
235 let message = cause_chain.first().cloned().unwrap();
236 let envelope = JsonEnvelope::error(
237 JsonMessage::error(diagnostic::cli::UNKNOWN, message)
238 .with_details(json!({ "cause_chain": cause_chain })),
239 );
240
241 assert!(!envelope.success);
242 assert_eq!(envelope.errors.len(), 1);
243 assert_eq!(envelope.errors[0].code, diagnostic::cli::UNKNOWN);
244 assert_eq!(envelope.errors[0].message, "build failed");
245 let details = envelope.errors[0].details.as_ref().expect("details");
246 let chain = details.get("cause_chain").and_then(|v| v.as_array()).expect("chain");
247 assert_eq!(chain.len(), 3);
248 assert_eq!(chain[0], "build failed");
249 assert_eq!(chain[2], "solc: missing semicolon");
250 }
251
252 #[test]
253 fn clap_error_kinds_map_to_exit_codes() {
254 let bad = Demo::try_parse_from(["demo", "--unknown"]).unwrap_err();
255 assert_eq!(exit_code_for_clap_error(&bad), ExitCode::Usage);
256
257 let help = Demo::try_parse_from(["demo", "--help"]).unwrap_err();
258 assert_eq!(exit_code_for_clap_error(&help), ExitCode::Success);
259
260 let version = Demo::try_parse_from(["demo", "--version"]).unwrap_err();
261 assert_eq!(exit_code_for_clap_error(&version), ExitCode::Success);
262 }
263
264 #[test]
268 fn missing_required_subcommand_classifies_as_usage() {
269 let err = Strict::try_parse_from(["strict"]).unwrap_err();
270 assert_eq!(err.kind(), ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand);
271 assert_eq!(exit_code_for_clap_error(&err), ExitCode::Usage);
272 }
273
274 #[test]
278 fn subcommand_help_preserves_command_context() {
279 let err = Demo::try_parse_from(["demo", "build", "--help"]).unwrap_err();
280 assert_eq!(err.kind(), ErrorKind::DisplayHelp);
281 let rendered = err.render().to_string();
282 assert!(
283 rendered.contains("Build the project"),
284 "subcommand help should mention the subcommand description, got: {rendered}"
285 );
286 assert!(
288 !rendered.contains("Usage: demo [OPTIONS]"),
289 "subcommand help leaked root usage: {rendered}"
290 );
291 }
292}