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"])]
32 json: bool,
33
34 #[arg(
36 help_heading = "Display options",
37 global = true,
38 long,
39 alias = "markdown",
40 conflicts_with = "json"
41 )]
42 md: bool,
43
44 #[arg(help_heading = "Display options", global = true, long, value_enum)]
46 color: Option<ColorChoice>,
47
48 #[arg(global = true, long, short = 'j', visible_alias = "jobs")]
50 threads: Option<usize>,
51}
52
53impl GlobalArgs {
54 pub fn check_markdown_help<C: clap::CommandFactory>() {
59 if std::env::args().take_while(|a| a != "--").any(|a| a == "--markdown-help") {
60 #[allow(clippy::disallowed_macros)]
62 {
63 eprintln!(
64 "note: `--markdown-help` is intended for human documentation; \
65 agents should use `--introspect` for machine-readable command discovery",
66 );
67 }
68 foundry_cli_markdown::print_help_markdown::<C>();
69 std::process::exit(0);
70 }
71 }
72
73 pub fn check_introspect<C: clap::CommandFactory>() {
79 if !pre_parse_flag_present("--introspect") {
80 return;
81 }
82 emit_introspect_and_exit(C::command(), &crate::introspect::CommandRegistry::EMPTY);
83 }
84
85 pub fn check_introspect_with(
88 command: clap::Command,
89 registry: &crate::introspect::CommandRegistry,
90 ) {
91 if pre_parse_flag_present("--introspect") {
92 emit_introspect_and_exit(command, registry);
93 }
94 }
95
96 pub fn init(&self) -> eyre::Result<()> {
98 let shell = self.shell();
100 match shell.color_choice() {
102 ColorChoice::Auto => {}
103 ColorChoice::Always => yansi::enable(),
104 ColorChoice::Never => yansi::disable(),
105 }
106 shell.set();
107
108 if self.threads.is_some() {
110 self.force_init_thread_pool()?;
111 }
112
113 if IS_NIGHTLY_VERSION
115 && !self.json
116 && std::env::var_os("FOUNDRY_DISABLE_NIGHTLY_WARNING").is_none()
117 {
118 let _ = sh_warn!("{}", NIGHTLY_VERSION_WARNING_MESSAGE);
119 }
120
121 Ok(())
122 }
123
124 pub fn shell(&self) -> Shell {
126 let mode = match self.quiet {
127 true => OutputMode::Quiet,
128 false => OutputMode::Normal,
129 };
130 let color = self.json.then_some(ColorChoice::Never).or(self.color).unwrap_or_default();
131 let format = if self.json {
132 OutputFormat::Json
133 } else if self.md {
134 OutputFormat::Markdown
135 } else {
136 OutputFormat::Text
137 };
138
139 Shell::new_with(format, mode, color, self.verbosity)
140 }
141
142 pub fn force_init_thread_pool(&self) -> eyre::Result<()> {
144 init_thread_pool(self.threads.unwrap_or(0))
145 }
146
147 #[track_caller]
149 pub fn tokio_runtime(&self) -> tokio::runtime::Runtime {
150 let mut builder = tokio::runtime::Builder::new_multi_thread();
151 if let Some(threads) = self.threads
152 && threads > 0
153 {
154 builder.worker_threads(threads);
155 }
156 builder.enable_all().build().expect("failed to create tokio runtime")
157 }
158
159 #[track_caller]
161 pub fn block_on<F: std::future::Future>(&self, future: F) -> F::Output {
162 self.tokio_runtime().block_on(future)
163 }
164}
165
166fn emit_introspect_and_exit(
167 command: clap::Command,
168 registry: &crate::introspect::CommandRegistry,
169) -> ! {
170 let json = crate::introspect::render_introspect_document(&command, registry);
171 #[allow(clippy::disallowed_macros)]
173 {
174 println!("{json}");
175 }
176 std::process::exit(0);
177}
178
179fn pre_parse_flag_present(flag: &str) -> bool {
187 pre_parse_flag_present_in(std::env::args_os().skip(1), flag)
188}
189
190const VALUE_TAKING_GLOBAL_OPTIONS: &[&str] = &["--color", "-j", "--threads", "--jobs"];
195
196fn pre_parse_flag_present_in<I>(args: I, flag: &str) -> bool
197where
198 I: IntoIterator<Item = std::ffi::OsString>,
199{
200 let mut iter = args.into_iter();
201 while let Some(a) = iter.next() {
202 if a == "--" {
203 return false;
204 }
205 let Some(s) = a.to_str() else {
206 return false;
209 };
210 if !s.starts_with('-') {
211 return false;
212 }
213 if s == flag {
214 return true;
215 }
216 if !s.contains('=') && VALUE_TAKING_GLOBAL_OPTIONS.contains(&s) {
218 iter.next();
219 }
220 }
221 false
222}
223
224pub fn init_thread_pool(threads: usize) -> eyre::Result<()> {
226 rayon::ThreadPoolBuilder::new()
227 .thread_name(|i| format!("foundry-{i}"))
228 .num_threads(threads)
229 .build_global()?;
230 Ok(())
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use std::ffi::OsString;
237
238 fn argv(slice: &[&str]) -> Vec<OsString> {
239 slice.iter().map(|s| OsString::from(*s)).collect()
240 }
241
242 #[test]
243 fn pre_parse_flag_present_matches_top_level_flags() {
244 for case in [
245 &["--introspect"][..],
246 &["--color", "always", "--introspect"],
247 &["-j", "4", "--introspect"],
248 &["--color=always", "--introspect"],
249 &["--quiet", "--introspect"],
250 ] {
251 assert!(pre_parse_flag_present_in(argv(case), "--introspect"), "case: {case:?}");
252 }
253 }
254
255 #[test]
256 fn pre_parse_flag_present_ignores_values_and_subcommands() {
257 for case in [
258 &[][..],
259 &["--", "--introspect"],
260 &["test", "--introspect"],
261 &["call", "ADDR", "method(string)", "--data", "--introspect"],
263 ] {
264 assert!(!pre_parse_flag_present_in(argv(case), "--introspect"), "case: {case:?}");
265 }
266 }
267}