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", "machine"])]
32    json: bool,
33
34    /// Activate the agent contract: disables color and wraps CLI-runtime
35    /// exits (parse / usage / help / version) in a structured envelope.
36    /// Per-command machine output (declared `output_mode`, progress and
37    /// prompt suppression, canonical exit codes) is adopted incrementally
38    /// — see `docs/agents/spec.md` §10. Mutually exclusive with `--json`
39    /// and `--md` to keep machine-mode output unambiguous.
40    #[arg(help_heading = "Display options", global = true, long, conflicts_with_all = &["color", "md", "json"])]
41    machine: bool,
42
43    /// Format log messages as Markdown.
44    #[arg(
45        help_heading = "Display options",
46        global = true,
47        long,
48        alias = "markdown",
49        conflicts_with = "json"
50    )]
51    md: bool,
52
53    /// The color of the log messages.
54    #[arg(help_heading = "Display options", global = true, long, value_enum)]
55    color: Option<ColorChoice>,
56
57    /// Number of threads to use. Specifying 0 defaults to the number of logical cores.
58    #[arg(global = true, long, short = 'j', visible_alias = "jobs")]
59    threads: Option<usize>,
60}
61
62impl GlobalArgs {
63    /// Check if `--markdown-help` was passed and print CLI reference as Markdown, then exit.
64    ///
65    /// This must be called **before** parsing arguments, since commands with required
66    /// subcommands would fail parsing before the flag is checked.
67    pub fn check_markdown_help<C: clap::CommandFactory>() {
68        if std::env::args().take_while(|a| a != "--").any(|a| a == "--markdown-help") {
69            // Pre-parse: `Shell` is not initialized yet, so `sh_*` is unavailable.
70            #[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    /// Check if `--introspect` was passed and print the introspection document as JSON, then exit.
83    ///
84    /// Must run **before** clap parsing so required subcommands or args don't block discovery.
85    /// Uses `CommandRegistry::EMPTY`; for per-command metadata, call
86    /// [`check_introspect_with`](Self::check_introspect_with) with a populated registry.
87    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    /// Like [`check_introspect`](Self::check_introspect) but uses an explicit
95    /// [`CommandRegistry`](crate::introspect::CommandRegistry). The clap command
96    /// is built lazily so normal startup pays nothing when `--introspect` is absent.
97    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    /// Initialize the global options.
107    pub fn init(&self) -> eyre::Result<()> {
108        // Keep the runtime machine-mode flag in sync with what clap parsed.
109        // `check_machine` runs pre-parse on the binary entry point; this
110        // covers callers that construct `GlobalArgs` programmatically and
111        // ensures the flag never goes stale.
112        crate::machine::set_machine(self.machine);
113
114        // Set the global shell.
115        let shell = self.shell();
116        // Argument takes precedence over the env var global color choice.
117        match shell.color_choice() {
118            ColorChoice::Auto => {}
119            ColorChoice::Always => yansi::enable(),
120            ColorChoice::Never => yansi::disable(),
121        }
122        shell.set();
123
124        // Initialize the thread pool only if `threads` was requested to avoid unnecessary overhead.
125        if self.threads.is_some() {
126            self.force_init_thread_pool()?;
127        }
128
129        // Display a warning message if the current version is not stable.
130        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    /// Create a new shell instance.
142    pub fn shell(&self) -> Shell {
143        // `--machine` forces Quiet; structured output goes through
144        // `print_json` / `print_stream_record`, which bypass the quiet check.
145        let mode = match self.quiet || self.machine {
146            true => OutputMode::Quiet,
147            false => OutputMode::Normal,
148        };
149        // `--machine` forces no-color; `--json` already forces no-color.
150        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    /// Initialize the global thread pool.
166    pub fn force_init_thread_pool(&self) -> eyre::Result<()> {
167        init_thread_pool(self.threads.unwrap_or(0))
168    }
169
170    /// Creates a new tokio runtime.
171    #[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    /// Creates a new tokio runtime and blocks on the future.
183    #[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    // Pre-parse: `Shell` is not initialized yet, so `sh_*` is unavailable.
195    #[allow(clippy::disallowed_macros)]
196    {
197        println!("{json}");
198    }
199    std::process::exit(0);
200}
201
202/// Returns whether `flag` is present among the binary's leading top-level options.
203///
204/// The scan ends at the first `--` separator, the first subcommand or
205/// positional token, or the first non-UTF-8 token (none of which can match
206/// the ASCII flags this helper supports). Values of known value-taking
207/// global options are skipped, so e.g. `forge --color always --introspect`
208/// and `forge -j 4 --introspect` still match.
209///
210/// Use for non-clap-global pre-parse flags (`--introspect`, `--markdown-help`).
211/// For clap-globals like `--machine`, use [`pre_parse_global_flag_present`].
212pub(crate) fn pre_parse_flag_present(flag: &str) -> bool {
213    pre_parse_flag_present_in(std::env::args_os().skip(1), flag)
214}
215
216/// Like [`pre_parse_flag_present`] but scans past subcommands/positionals,
217/// honoring `clap`'s `global = true` placement (e.g. `cast call --machine --help`).
218/// Still stops at `--` and skips values of known value-taking globals.
219pub(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
223/// Value-taking global options declared on [`GlobalArgs`]; their following
224/// argv token is the value, not a flag, and must not be considered by the
225/// pre-parse scan. Must stay in sync with the `#[arg(... long, short)]`
226/// declarations above.
227const 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            // Non-UTF-8 token: cannot be `--introspect` / `--markdown-help`
240            // and most likely a positional value — stop scanning here.
241            return false;
242        };
243        if !s.starts_with('-') {
244            return false;
245        }
246        if s == flag {
247            return true;
248        }
249        // Skip the value of `--opt VALUE` form; `--opt=VALUE` is one token.
250        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        // Non-UTF-8 token: skip but keep scanning.
267        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
278/// Initialize the global thread pool.
279pub 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            // `cast call ADDR "method(string)" --data --introspect`
316            &["call", "ADDR", "method(string)", "--data", "--introspect"],
317        ] {
318            assert!(!pre_parse_flag_present_in(argv(case), "--introspect"), "case: {case:?}");
319        }
320    }
321
322    /// Clap-global scanner: matches `--machine` anywhere except after `--`
323    /// or as the value of a known value-taking global.
324    #[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    /// Non-UTF-8 argv must not panic the scanner; it must short-circuit
348    /// to `false` at the offending token.
349    #[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    /// Non-UTF-8 token must not block a later `--machine` from matching.
359    #[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}