1use clap::{ArgAction, Parser};
2use foundry_common::{
3 shell::{ColorChoice, OutputFormat, OutputMode, Shell, Verbosity},
4 version::{IS_NIGHTLY_VERSION, NIGHTLY_VERSION_WARNING_MESSAGE},
5};
6use serde::{Deserialize, Serialize};
7
8#[derive(Clone, Debug, Default, Serialize, Deserialize, Parser)]
10pub struct GlobalArgs {
11 #[arg(help_heading = "Display options", global = true, short, long, verbatim_doc_comment, conflicts_with = "quiet", action = ArgAction::Count)]
24 verbosity: Verbosity,
25
26 #[arg(help_heading = "Display options", global = true, short, long, alias = "silent")]
28 quiet: bool,
29
30 #[arg(help_heading = "Display options", global = true, long, alias = "format-json", conflicts_with_all = &["quiet", "color", "machine"])]
32 json: bool,
33
34 #[arg(help_heading = "Display options", global = true, long, conflicts_with_all = &["color", "md", "json"])]
41 machine: bool,
42
43 #[arg(
45 help_heading = "Display options",
46 global = true,
47 long,
48 alias = "markdown",
49 conflicts_with = "json"
50 )]
51 md: bool,
52
53 #[arg(help_heading = "Display options", global = true, long, value_enum)]
55 color: Option<ColorChoice>,
56
57 #[arg(global = true, long, short = 'j', visible_alias = "jobs")]
59 threads: Option<usize>,
60}
61
62impl GlobalArgs {
63 pub fn check_markdown_help<C: clap::CommandFactory>() {
68 if std::env::args().take_while(|a| a != "--").any(|a| a == "--markdown-help") {
69 #[allow(clippy::disallowed_macros)]
71 {
72 eprintln!(
73 "note: `--markdown-help` is intended for human documentation; \
74 agents should use `--introspect` for machine-readable command discovery",
75 );
76 }
77 foundry_cli_markdown::print_help_markdown::<C>();
78 std::process::exit(0);
79 }
80 }
81
82 pub fn check_introspect<C: clap::CommandFactory>() {
88 if !pre_parse_flag_present("--introspect") {
89 return;
90 }
91 emit_introspect_and_exit(C::command(), &crate::introspect::CommandRegistry::EMPTY);
92 }
93
94 pub fn check_introspect_with(
98 make_command: impl FnOnce() -> clap::Command,
99 registry: &crate::introspect::CommandRegistry,
100 ) {
101 if pre_parse_flag_present("--introspect") {
102 emit_introspect_and_exit(make_command(), registry);
103 }
104 }
105
106 pub fn init(&self) -> eyre::Result<()> {
108 crate::machine::set_machine(self.machine);
113
114 let shell = self.shell();
116 match shell.color_choice() {
118 ColorChoice::Auto => {}
119 ColorChoice::Always => yansi::enable(),
120 ColorChoice::Never => yansi::disable(),
121 }
122 shell.set();
123
124 if self.threads.is_some() {
126 self.force_init_thread_pool()?;
127 }
128
129 if IS_NIGHTLY_VERSION
131 && !self.json
132 && !self.machine
133 && std::env::var_os("FOUNDRY_DISABLE_NIGHTLY_WARNING").is_none()
134 {
135 let _ = sh_warn!("{}", NIGHTLY_VERSION_WARNING_MESSAGE);
136 }
137
138 Ok(())
139 }
140
141 pub fn shell(&self) -> Shell {
143 let mode = match self.quiet || self.machine {
146 true => OutputMode::Quiet,
147 false => OutputMode::Normal,
148 };
149 let color = (self.json || self.machine)
151 .then_some(ColorChoice::Never)
152 .or(self.color)
153 .unwrap_or_default();
154 let format = if self.json {
155 OutputFormat::Json
156 } else if self.md {
157 OutputFormat::Markdown
158 } else {
159 OutputFormat::Text
160 };
161
162 Shell::new_with(format, mode, color, self.verbosity)
163 }
164
165 pub fn force_init_thread_pool(&self) -> eyre::Result<()> {
167 init_thread_pool(self.threads.unwrap_or(0))
168 }
169
170 #[track_caller]
172 pub fn tokio_runtime(&self) -> tokio::runtime::Runtime {
173 let mut builder = tokio::runtime::Builder::new_multi_thread();
174 if let Some(threads) = self.threads
175 && threads > 0
176 {
177 builder.worker_threads(threads);
178 }
179 builder.enable_all().build().expect("failed to create tokio runtime")
180 }
181
182 #[track_caller]
184 pub fn block_on<F: std::future::Future>(&self, future: F) -> F::Output {
185 self.tokio_runtime().block_on(future)
186 }
187}
188
189fn emit_introspect_and_exit(
190 command: clap::Command,
191 registry: &crate::introspect::CommandRegistry,
192) -> ! {
193 let json = crate::introspect::render_introspect_document(&command, registry);
194 #[allow(clippy::disallowed_macros)]
196 {
197 println!("{json}");
198 }
199 std::process::exit(0);
200}
201
202pub(crate) fn pre_parse_flag_present(flag: &str) -> bool {
213 pre_parse_flag_present_in(std::env::args_os().skip(1), flag)
214}
215
216pub(crate) fn pre_parse_global_flag_present(flag: &str) -> bool {
220 pre_parse_global_flag_present_in(std::env::args_os().skip(1), flag)
221}
222
223const VALUE_TAKING_GLOBAL_OPTIONS: &[&str] = &["--color", "-j", "--threads", "--jobs"];
228
229pub(crate) fn pre_parse_flag_present_in<I>(args: I, flag: &str) -> bool
230where
231 I: IntoIterator<Item = std::ffi::OsString>,
232{
233 let mut iter = args.into_iter();
234 while let Some(a) = iter.next() {
235 if a == "--" {
236 return false;
237 }
238 let Some(s) = a.to_str() else {
239 return false;
242 };
243 if !s.starts_with('-') {
244 return false;
245 }
246 if s == flag {
247 return true;
248 }
249 if !s.contains('=') && VALUE_TAKING_GLOBAL_OPTIONS.contains(&s) {
251 iter.next();
252 }
253 }
254 false
255}
256
257pub(crate) fn pre_parse_global_flag_present_in<I>(args: I, flag: &str) -> bool
258where
259 I: IntoIterator<Item = std::ffi::OsString>,
260{
261 let mut iter = args.into_iter();
262 while let Some(a) = iter.next() {
263 if a == "--" {
264 return false;
265 }
266 let Some(s) = a.to_str() else { continue };
268 if s == flag {
269 return true;
270 }
271 if s.starts_with('-') && !s.contains('=') && VALUE_TAKING_GLOBAL_OPTIONS.contains(&s) {
272 iter.next();
273 }
274 }
275 false
276}
277
278pub fn init_thread_pool(threads: usize) -> eyre::Result<()> {
280 rayon::ThreadPoolBuilder::new()
281 .thread_name(|i| format!("foundry-{i}"))
282 .num_threads(threads)
283 .build_global()?;
284 Ok(())
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use std::ffi::OsString;
291
292 fn argv(slice: &[&str]) -> Vec<OsString> {
293 slice.iter().map(|s| OsString::from(*s)).collect()
294 }
295
296 #[test]
297 fn pre_parse_flag_present_matches_top_level_flags() {
298 for case in [
299 &["--introspect"][..],
300 &["--color", "always", "--introspect"],
301 &["-j", "4", "--introspect"],
302 &["--color=always", "--introspect"],
303 &["--quiet", "--introspect"],
304 ] {
305 assert!(pre_parse_flag_present_in(argv(case), "--introspect"), "case: {case:?}");
306 }
307 }
308
309 #[test]
310 fn pre_parse_flag_present_ignores_values_and_subcommands() {
311 for case in [
312 &[][..],
313 &["--", "--introspect"],
314 &["test", "--introspect"],
315 &["call", "ADDR", "method(string)", "--data", "--introspect"],
317 ] {
318 assert!(!pre_parse_flag_present_in(argv(case), "--introspect"), "case: {case:?}");
319 }
320 }
321
322 #[test]
325 fn pre_parse_global_flag_present_machine_cases() {
326 for case in [
327 &["--machine"][..],
328 &["--color", "always", "--machine"],
329 &["-j", "4", "--machine"],
330 &["build", "--machine"],
331 &["build", "--machine", "--help"],
332 &["call", "ADDR", "sig(string)", "--data", "0x00", "--machine"],
333 ] {
334 assert!(
335 pre_parse_global_flag_present_in(argv(case), "--machine"),
336 "expected match: {case:?}"
337 );
338 }
339 for case in [&["--", "--machine"][..], &["--color", "--machine"]] {
340 assert!(
341 !pre_parse_global_flag_present_in(argv(case), "--machine"),
342 "expected NO match: {case:?}"
343 );
344 }
345 }
346
347 #[test]
350 #[cfg(unix)]
351 fn pre_parse_flag_present_handles_non_utf8() {
352 use std::os::unix::ffi::OsStringExt;
353 let bad = OsString::from_vec(vec![0xff, 0xfe]);
354 let args = vec![bad, OsString::from("--machine")];
355 assert!(!pre_parse_flag_present_in(args, "--machine"));
356 }
357
358 #[test]
360 #[cfg(unix)]
361 fn pre_parse_global_flag_present_handles_non_utf8() {
362 use std::os::unix::ffi::OsStringExt;
363 let bad = OsString::from_vec(vec![0xff, 0xfe]);
364 let args = vec![bad, OsString::from("--machine")];
365 assert!(pre_parse_global_flag_present_in(args, "--machine"));
366 }
367}