Skip to main content

foundry_cli/opts/
global.rs

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/// Global arguments for the CLI.
9#[derive(Clone, Debug, Default, Serialize, Deserialize, Parser)]
10pub struct GlobalArgs {
11    /// Verbosity level of the log messages.
12    ///
13    /// Pass multiple times to increase the verbosity (e.g. -v, -vv, -vvv).
14    ///
15    /// Depending on the context the verbosity levels have different meanings.
16    ///
17    /// For example, the verbosity levels of the EVM are:
18    /// - 2 (-vv): Print logs for all tests.
19    /// - 3 (-vvv): Print execution traces for failing tests.
20    /// - 4 (-vvvv): Print execution traces for all tests, and setup traces for failing tests.
21    /// - 5 (-vvvvv): Print execution and setup traces for all tests, including storage changes and
22    ///   backtraces with line numbers.
23    #[arg(help_heading = "Display options", global = true, short, long, verbatim_doc_comment, conflicts_with = "quiet", action = ArgAction::Count)]
24    verbosity: Verbosity,
25
26    /// Do not print log messages.
27    #[arg(help_heading = "Display options", global = true, short, long, alias = "silent")]
28    quiet: bool,
29
30    /// Format log messages as JSON.
31    #[arg(help_heading = "Display options", global = true, long, alias = "format-json", conflicts_with_all = &["quiet", "color"])]
32    json: bool,
33
34    /// Format log messages as Markdown.
35    #[arg(
36        help_heading = "Display options",
37        global = true,
38        long,
39        alias = "markdown",
40        conflicts_with = "json"
41    )]
42    md: bool,
43
44    /// The color of the log messages.
45    #[arg(help_heading = "Display options", global = true, long, value_enum)]
46    color: Option<ColorChoice>,
47
48    /// Number of threads to use. Specifying 0 defaults to the number of logical cores.
49    #[arg(global = true, long, short = 'j', visible_alias = "jobs")]
50    threads: Option<usize>,
51}
52
53impl GlobalArgs {
54    /// Check if `--markdown-help` was passed and print CLI reference as Markdown, then exit.
55    ///
56    /// This must be called **before** parsing arguments, since commands with required
57    /// subcommands would fail parsing before the flag is checked.
58    pub fn check_markdown_help<C: clap::CommandFactory>() {
59        if std::env::args().take_while(|a| a != "--").any(|a| a == "--markdown-help") {
60            // Pre-parse: `Shell` is not initialized yet, so `sh_*` is unavailable.
61            #[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    /// Check if `--introspect` was passed and print the introspection document as JSON, then exit.
74    ///
75    /// Must run **before** clap parsing so required subcommands or args don't block discovery.
76    /// Uses `CommandRegistry::EMPTY`; for per-command metadata, call
77    /// [`check_introspect_with`](Self::check_introspect_with) with a populated registry.
78    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    /// Like [`check_introspect`](Self::check_introspect) but uses an explicit
86    /// [`Command`](clap::Command) and [`CommandRegistry`](crate::introspect::CommandRegistry).
87    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    /// Initialize the global options.
97    pub fn init(&self) -> eyre::Result<()> {
98        // Set the global shell.
99        let shell = self.shell();
100        // Argument takes precedence over the env var global color choice.
101        match shell.color_choice() {
102            ColorChoice::Auto => {}
103            ColorChoice::Always => yansi::enable(),
104            ColorChoice::Never => yansi::disable(),
105        }
106        shell.set();
107
108        // Initialize the thread pool only if `threads` was requested to avoid unnecessary overhead.
109        if self.threads.is_some() {
110            self.force_init_thread_pool()?;
111        }
112
113        // Display a warning message if the current version is not stable.
114        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    /// Create a new shell instance.
125    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    /// Initialize the global thread pool.
143    pub fn force_init_thread_pool(&self) -> eyre::Result<()> {
144        init_thread_pool(self.threads.unwrap_or(0))
145    }
146
147    /// Creates a new tokio runtime.
148    #[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    /// Creates a new tokio runtime and blocks on the future.
160    #[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    // Pre-parse: `Shell` is not initialized yet, so `sh_*` is unavailable.
172    #[allow(clippy::disallowed_macros)]
173    {
174        println!("{json}");
175    }
176    std::process::exit(0);
177}
178
179/// Returns whether `flag` is present among the binary's leading top-level options.
180///
181/// The scan ends at the first `--` separator, the first subcommand or
182/// positional token, or the first non-UTF-8 token (none of which can match
183/// the ASCII flags this helper supports). Values of known value-taking
184/// global options are skipped, so e.g. `forge --color always --introspect`
185/// and `forge -j 4 --introspect` still match.
186fn pre_parse_flag_present(flag: &str) -> bool {
187    pre_parse_flag_present_in(std::env::args_os().skip(1), flag)
188}
189
190/// Value-taking global options declared on [`GlobalArgs`]; their following
191/// argv token is the value, not a flag, and must not be considered by the
192/// pre-parse scan. Must stay in sync with the `#[arg(... long, short)]`
193/// declarations above.
194const 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            // Non-UTF-8 token: cannot be `--introspect` / `--markdown-help`
207            // and most likely a positional value — stop scanning here.
208            return false;
209        };
210        if !s.starts_with('-') {
211            return false;
212        }
213        if s == flag {
214            return true;
215        }
216        // Skip the value of `--opt VALUE` form; `--opt=VALUE` is one token.
217        if !s.contains('=') && VALUE_TAKING_GLOBAL_OPTIONS.contains(&s) {
218            iter.next();
219        }
220    }
221    false
222}
223
224/// Initialize the global thread pool.
225pub 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            // `cast call ADDR "method(string)" --data --introspect`
262            &["call", "ADDR", "method(string)", "--data", "--introspect"],
263        ] {
264            assert!(!pre_parse_flag_present_in(argv(case), "--introspect"), "case: {case:?}");
265        }
266    }
267}