diff --git a/Cargo.lock b/Cargo.lock index 886cc3ff2644a..4de047d59d445 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3664,6 +3664,8 @@ dependencies = [ "alloy-transport-http", "alloy-transport-ipc", "alloy-transport-ws", + "anstream", + "anstyle", "async-trait", "clap", "comfy-table", @@ -3681,6 +3683,7 @@ dependencies = [ "serde", "serde_json", "similar-asserts", + "terminal_size", "thiserror", "tokio", "tower 0.4.13", diff --git a/Cargo.toml b/Cargo.toml index 245c6da411008..5c5c1bb8abfec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -222,6 +222,11 @@ alloy-trie = "0.6.0" ## op-alloy for tests in anvil op-alloy-rpc-types = "0.2.9" +## cli +anstream = "0.6.15" +anstyle = "1.0.8" +terminal_size = "0.3.0" + ## misc async-trait = "0.1" auto_impl = "1" diff --git a/clippy.toml b/clippy.toml index 92b35ffc0afc8..3e45486a86f4f 100644 --- a/clippy.toml +++ b/clippy.toml @@ -2,3 +2,11 @@ msrv = "1.80" # bytes::Bytes is included by default and alloy_primitives::Bytes is a wrapper around it, # so it is safe to ignore it as well ignore-interior-mutability = ["bytes::Bytes", "alloy_primitives::Bytes"] + +# disallowed-macros = [ +# # See `foundry_common::shell` +# { path = "std::print", reason = "use `sh_print` or similar macros instead" }, +# { path = "std::eprint", reason = "use `sh_eprint` or similar macros instead" }, +# { path = "std::println", reason = "use `sh_println` or similar macros instead" }, +# { path = "std::eprintln", reason = "use `sh_eprintln` or similar macros instead" }, +# ] diff --git a/crates/cast/bin/args.rs b/crates/cast/bin/args.rs index fdb6a113354a2..694a4072cb662 100644 --- a/crates/cast/bin/args.rs +++ b/crates/cast/bin/args.rs @@ -8,7 +8,7 @@ use alloy_primitives::{Address, B256, U256}; use alloy_rpc_types::BlockId; use clap::{Parser, Subcommand, ValueHint}; use eyre::Result; -use foundry_cli::opts::{EtherscanOpts, RpcOpts}; +use foundry_cli::opts::{EtherscanOpts, RpcOpts, ShellOpts}; use foundry_common::ens::NameOrAddress; use std::{path::PathBuf, str::FromStr}; @@ -32,6 +32,8 @@ const VERSION_MESSAGE: &str = concat!( pub struct Cast { #[command(subcommand)] pub cmd: CastSubcommand, + #[clap(flatten)] + pub shell: ShellOpts, } #[derive(Subcommand)] diff --git a/crates/cast/bin/main.rs b/crates/cast/bin/main.rs index 0b861f90d02a5..ddac6804458fd 100644 --- a/crates/cast/bin/main.rs +++ b/crates/cast/bin/main.rs @@ -1,6 +1,3 @@ -#[macro_use] -extern crate tracing; - use alloy_primitives::{eip191_hash_message, hex, keccak256, Address, B256}; use alloy_provider::Provider; use alloy_rpc_types::{BlockId, BlockNumberOrTag::Latest}; @@ -8,7 +5,7 @@ use cast::{Cast, SimpleCast}; use clap::{CommandFactory, Parser}; use clap_complete::generate; use eyre::Result; -use foundry_cli::{handler, prompt, stdin, utils}; +use foundry_cli::{handler, utils}; use foundry_common::{ abi::get_event, ens::{namehash, ProviderEnsExt}, @@ -19,10 +16,17 @@ use foundry_common::{ import_selectors, parse_signatures, pretty_calldata, ParsedSignatures, SelectorImportData, SelectorType, }, + stdin, }; use foundry_config::Config; use std::time::Instant; +#[macro_use] +extern crate tracing; + +#[macro_use] +extern crate foundry_common; + pub mod args; pub mod cmd; pub mod tx; @@ -33,12 +37,21 @@ use args::{Cast as CastArgs, CastSubcommand, ToBaseArgs}; #[global_allocator] static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; -fn main() -> Result<()> { +#[tokio::main] +async fn main() { + if let Err(err) = run().await { + let _ = foundry_common::Shell::get().error(&err); + std::process::exit(1); + } +} + +async fn run() -> Result<()> { handler::install(); utils::load_dotenv(); utils::subscriber(); utils::enable_paint(); let args = CastArgs::parse(); + args.shell.shell().set(); main_args(args) } diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 6f5e2f6076932..e5766683ea62b 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -10,5 +10,4 @@ extern crate tracing; pub mod handler; pub mod opts; -pub mod stdin; pub mod utils; diff --git a/crates/cli/src/opts/build/core.rs b/crates/cli/src/opts/build/core.rs index ebb4da9f1d45e..5ded2b3cdb33b 100644 --- a/crates/cli/src/opts/build/core.rs +++ b/crates/cli/src/opts/build/core.rs @@ -105,11 +105,6 @@ pub struct CoreBuildArgs { #[serde(skip)] pub revert_strings: Option, - /// Don't print anything on startup. - #[arg(long, help_heading = "Compiler options")] - #[serde(skip)] - pub silent: bool, - /// Generate build info files. #[arg(long, help_heading = "Project options")] #[serde(skip)] diff --git a/crates/cli/src/opts/dependency.rs b/crates/cli/src/opts/dependency.rs index 6fa33a53f8a1f..6fc16e7c955bb 100644 --- a/crates/cli/src/opts/dependency.rs +++ b/crates/cli/src/opts/dependency.rs @@ -2,7 +2,7 @@ use eyre::Result; use regex::Regex; -use std::{str::FromStr, sync::LazyLock}; +use std::{fmt, str::FromStr, sync::LazyLock}; static GH_REPO_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"[\w-]+/[\w.-]+").unwrap()); @@ -46,6 +46,19 @@ pub struct Dependency { pub alias: Option, } +impl fmt::Display for Dependency { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.name())?; + if let Some(tag) = &self.tag { + write!(f, "{VERSION_SEPARATOR}{tag}")?; + } + if let Some(url) = &self.url { + write!(f, " ({url})")?; + } + Ok(()) + } +} + impl FromStr for Dependency { type Err = eyre::Error; fn from_str(dependency: &str) -> Result { diff --git a/crates/cli/src/opts/mod.rs b/crates/cli/src/opts/mod.rs index 7825cba3c8b36..95bf9e126cc63 100644 --- a/crates/cli/src/opts/mod.rs +++ b/crates/cli/src/opts/mod.rs @@ -2,10 +2,12 @@ mod build; mod chain; mod dependency; mod ethereum; +mod shell; mod transaction; pub use build::*; pub use chain::*; pub use dependency::*; pub use ethereum::*; +pub use shell::*; pub use transaction::*; diff --git a/crates/cli/src/opts/shell.rs b/crates/cli/src/opts/shell.rs new file mode 100644 index 0000000000000..cf94c89e7dc55 --- /dev/null +++ b/crates/cli/src/opts/shell.rs @@ -0,0 +1,32 @@ +use clap::Parser; +use foundry_common::shell::{ColorChoice, Shell, Verbosity}; + +// note: `verbose` and `quiet` cannot have `short` because of conflicts with multiple commands. + +/// Global shell options. +#[derive(Clone, Copy, Debug, Parser)] +pub struct ShellOpts { + /// Use verbose output. + #[clap(long, global = true, conflicts_with = "quiet")] + pub verbose: bool, + + /// Do not print log messages. + #[clap(long, global = true, alias = "silent", conflicts_with = "verbose")] + pub quiet: bool, + + /// Log messages coloring. + #[clap(long, global = true, value_enum)] + pub color: Option, +} + +impl ShellOpts { + pub fn shell(self) -> Shell { + let verbosity = match (self.verbose, self.quiet) { + (true, false) => Verbosity::Verbose, + (false, true) => Verbosity::Quiet, + (false, false) => Verbosity::Normal, + (true, true) => unreachable!(), + }; + Shell::new_with(self.color.unwrap_or_default(), verbosity) + } +} diff --git a/crates/cli/src/utils/mod.rs b/crates/cli/src/utils/mod.rs index 736793e67cd52..81d5f9ae71619 100644 --- a/crates/cli/src/utils/mod.rs +++ b/crates/cli/src/utils/mod.rs @@ -161,23 +161,6 @@ pub fn block_on(future: F) -> F::Output { rt.block_on(future) } -/// Conditionally print a message -/// -/// This macro accepts a predicate and the message to print if the predicate is true -/// -/// ```ignore -/// let quiet = true; -/// p_println!(!quiet => "message"); -/// ``` -#[macro_export] -macro_rules! p_println { - ($p:expr => $($arg:tt)*) => {{ - if $p { - println!($($arg)*) - } - }} -} - /// Loads a dotenv file, from the cwd and the project root, ignoring potential failure. /// /// We could use `warn!` here, but that would imply that the dotenv file can't configure diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 73bd90cc71266..28add2a42ff88 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -63,6 +63,10 @@ url.workspace = true walkdir.workspace = true yansi.workspace = true +anstream.workspace = true +anstyle.workspace = true +terminal_size.workspace = true + [dev-dependencies] foundry-macros.workspace = true similar-asserts.workspace = true diff --git a/crates/common/src/compile.rs b/crates/common/src/compile.rs index a75ac0819ddef..2dae4577a844a 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -63,7 +63,7 @@ impl ProjectCompiler { verify: None, print_names: None, print_sizes: None, - quiet: Some(crate::shell::verbosity().is_silent()), + quiet: Some(crate::shell::verbosity().is_quiet()), bail: None, files: Vec::new(), } diff --git a/crates/common/src/io/macros.rs b/crates/common/src/io/macros.rs new file mode 100644 index 0000000000000..e024debd3db47 --- /dev/null +++ b/crates/common/src/io/macros.rs @@ -0,0 +1,192 @@ +/// Prints a message to [`stdout`][io::stdout] and [reads a line from stdin into a String](read). +/// +/// Returns `Result`, so sometimes `T` must be explicitly specified, like in `str::parse`. +/// +/// # Examples +/// +/// ```no_run +/// use foundry_common::prompt; +/// +/// let response: String = prompt!("Would you like to continue? [y/N] ")?; +/// if !matches!(response.as_str(), "y" | "Y") { +/// return Ok(()) +/// } +/// # Ok::<(), Box>(()) +/// ``` +#[macro_export] +macro_rules! prompt { + () => { + $crate::stdin::parse_line() + }; + + ($($tt:tt)+) => {{ + let _ = $crate::sh_print!($($tt)+); + match ::std::io::Write::flush(&mut ::std::io::stdout()) { + ::core::result::Result::Ok(()) => $crate::prompt!(), + ::core::result::Result::Err(e) => ::core::result::Result::Err(::eyre::eyre!("Could not flush stdout: {e}")) + } + }}; +} + +/// Prints a formatted error to stderr. +#[macro_export] +macro_rules! sh_err { + ($($args:tt)*) => { + $crate::__sh_dispatch!(error $($args)*) + }; +} + +/// Prints a formatted warning to stderr. +#[macro_export] +macro_rules! sh_warn { + ($($args:tt)*) => { + $crate::__sh_dispatch!(warn $($args)*) + }; +} + +/// Prints a formatted note to stderr. +#[macro_export] +macro_rules! sh_note { + ($($args:tt)*) => { + $crate::__sh_dispatch!(note $($args)*) + }; +} + +/// Prints a raw formatted message to stdout. +/// +/// **Note**: This macro is **not** affected by the `--quiet` flag. +#[macro_export] +macro_rules! sh_print { + ($($args:tt)*) => { + $crate::__sh_dispatch!(print_out $($args)*) + }; +} + +/// Prints a raw formatted message to stderr. +/// +/// **Note**: This macro **is** affected by the `--quiet` flag. +#[macro_export] +macro_rules! sh_eprint { + ($($args:tt)*) => { + $crate::__sh_dispatch!(print_err $($args)*) + }; +} + +/// Prints a raw formatted message to stdout, with a trailing newline. +/// +/// **Note**: This macro is **not** affected by the `--quiet` flag. +#[macro_export] +macro_rules! sh_println { + () => { + $crate::sh_print!("\n") + }; + + ($fmt:literal $($args:tt)*) => { + $crate::sh_print!("{}\n", ::core::format_args!($fmt $($args)*)) + }; + + ($shell:expr $(,)?) => { + $crate::sh_print!($shell, "\n") + }; + + ($shell:expr, $($args:tt)*) => { + $crate::sh_print!($shell, "{}\n", ::core::format_args!($($args)*)) + }; + + ($($args:tt)*) => { + $crate::sh_print!("{}\n", ::core::format_args!($($args)*)) + }; +} + +/// Prints a raw formatted message to stderr, with a trailing newline. +/// +/// **Note**: This macro **is** affected by the `--quiet` flag. +#[macro_export] +macro_rules! sh_eprintln { + () => { + $crate::sh_eprint!("\n") + }; + + ($fmt:literal $($args:tt)*) => { + $crate::sh_eprint!("{}\n", ::core::format_args!($fmt $($args)*)) + }; + + ($shell:expr $(,)?) => { + $crate::sh_eprint!($shell, "\n") + }; + + ($shell:expr, $($args:tt)*) => { + $crate::sh_eprint!($shell, "{}\n", ::core::format_args!($($args)*)) + }; + + ($($args:tt)*) => { + $crate::sh_eprint!("{}\n", ::core::format_args!($($args)*)) + }; +} + +/// Prints a justified status header with an optional message. +#[macro_export] +macro_rules! sh_status { + ($header:expr) => { + $crate::Shell::status_header(&mut *$crate::Shell::get(), $header) + }; + + ($header:expr => $($args:tt)*) => { + $crate::Shell::status(&mut *$crate::Shell::get(), $header, ::core::format_args!($($args)*)) + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __sh_dispatch { + ($f:ident $fmt:literal $($args:tt)*) => { + $crate::Shell::$f(&mut *$crate::Shell::get(), ::core::format_args!($fmt $($args)*)) + }; + + ($f:ident $shell:expr, $($args:tt)*) => { + $crate::Shell::$f($shell, ::core::format_args!($($args)*)) + }; + + ($f:ident $($args:tt)*) => { + $crate::Shell::$f(&mut *$crate::Shell::get(), ::core::format_args!($($args)*)) + }; +} + +#[cfg(test)] +mod tests { + #[test] + fn macros() { + sh_err!("err").unwrap(); + sh_err!("err {}", "arg").unwrap(); + + sh_warn!("warn").unwrap(); + sh_warn!("warn {}", "arg").unwrap(); + + sh_note!("note").unwrap(); + sh_note!("note {}", "arg").unwrap(); + + sh_print!("print -").unwrap(); + sh_print!("print {} -", "arg").unwrap(); + + sh_println!().unwrap(); + sh_println!("println").unwrap(); + sh_println!("println {}", "arg").unwrap(); + + sh_eprint!("eprint -").unwrap(); + sh_eprint!("eprint {} -", "arg").unwrap(); + + sh_eprintln!().unwrap(); + sh_eprintln!("eprintln").unwrap(); + sh_eprintln!("eprintln {}", "arg").unwrap(); + } + + #[test] + fn macros_with_shell() { + let shell = &mut crate::Shell::new(); + sh_eprintln!(shell).unwrap(); + sh_eprintln!(shell,).unwrap(); + sh_eprintln!(shell, "shelled eprintln").unwrap(); + sh_eprintln!(shell, "shelled eprintln {}", "arg").unwrap(); + sh_eprintln!(&mut crate::Shell::new(), "shelled eprintln {}", "arg").unwrap(); + } +} diff --git a/crates/common/src/io/mod.rs b/crates/common/src/io/mod.rs new file mode 100644 index 0000000000000..f62fd034617bf --- /dev/null +++ b/crates/common/src/io/mod.rs @@ -0,0 +1,11 @@ +//! Utilities for working with standard input, output, and error. + +#[macro_use] +mod macros; + +pub mod shell; +pub mod stdin; +pub mod style; + +#[doc(no_inline)] +pub use shell::Shell; diff --git a/crates/common/src/io/shell.rs b/crates/common/src/io/shell.rs new file mode 100644 index 0000000000000..b93552e6252d2 --- /dev/null +++ b/crates/common/src/io/shell.rs @@ -0,0 +1,576 @@ +//! Utility functions for writing to [`stdout`](std::io::stdout) and [`stderr`](std::io::stderr). +//! +//! Originally from [cargo](https://github.com/rust-lang/cargo/blob/35814255a1dbaeca9219fae81d37a8190050092c/src/cargo/core/shell.rs). + +use super::style::*; +use anstream::AutoStream; +use anstyle::Style; +use clap::ValueEnum; +use eyre::Result; +use std::{ + fmt, + io::{prelude::*, IsTerminal}, + ops::DerefMut, + sync::{ + atomic::{AtomicBool, Ordering}, + Mutex, OnceLock, PoisonError, + }, +}; + +/// Returns the currently set verbosity. +pub fn verbosity() -> Verbosity { + Shell::get().verbosity() +} + +/// The global shell instance. +static GLOBAL_SHELL: OnceLock> = OnceLock::new(); + +/// Terminal width. +pub enum TtyWidth { + /// Not a terminal, or could not determine size. + NoTty, + /// A known width. + Known(usize), + /// A guess at the width. + Guess(usize), +} + +impl TtyWidth { + /// Returns the width of the terminal from the environment, if known. + pub fn get() -> Self { + // use stderr + #[cfg(unix)] + #[allow(clippy::useless_conversion)] + let opt = terminal_size::terminal_size_using_fd(2.into()); + #[cfg(not(unix))] + let opt = terminal_size::terminal_size(); + match opt { + Some((w, _)) => Self::Known(w.0 as usize), + None => Self::NoTty, + } + } + + /// Returns the width used by progress bars for the tty. + pub fn progress_max_width(&self) -> Option { + match *self { + Self::NoTty => None, + Self::Known(width) | Self::Guess(width) => Some(width), + } + } +} + +/// The requested verbosity of output. +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub enum Verbosity { + /// All output + Verbose, + /// Default output + #[default] + Normal, + /// No output + Quiet, +} + +impl Verbosity { + /// Returns true if the verbosity level is `Verbose`. + #[inline] + pub fn is_verbose(self) -> bool { + self == Self::Verbose + } + + /// Returns true if the verbosity level is `Normal`. + #[inline] + pub fn is_normal(self) -> bool { + self == Self::Normal + } + + /// Returns true if the verbosity level is `Quiet`. + #[inline] + pub fn is_quiet(self) -> bool { + self == Self::Quiet + } +} + +/// An abstraction around console output that remembers preferences for output +/// verbosity and color. +pub struct Shell { + /// Wrapper around stdout/stderr. This helps with supporting sending + /// output to a memory buffer which is useful for tests. + output: ShellOut, + + /// How verbose messages should be. + verbosity: Verbosity, + + /// Flag that indicates the current line needs to be cleared before + /// printing. Used when a progress bar is currently displayed. + needs_clear: AtomicBool, +} + +impl fmt::Debug for Shell { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut s = f.debug_struct("Shell"); + s.field("verbosity", &self.verbosity); + if let ShellOut::Stream { color_choice, .. } = self.output { + s.field("color_choice", &color_choice); + } + s.finish() + } +} + +/// A `Write`able object, either with or without color support. +enum ShellOut { + /// Color-enabled stdio, with information on whether color should be used. + Stream { + stdout: AutoStream, + stderr: AutoStream, + stderr_tty: bool, + color_choice: ColorChoice, + }, + /// A write object that ignores all output. + Empty(std::io::Empty), +} + +/// Whether messages should use color output. +#[derive(Debug, Default, PartialEq, Clone, Copy, ValueEnum)] +pub enum ColorChoice { + /// Intelligently guess whether to use color output (default). + #[default] + Auto, + /// Force color output. + Always, + /// Force disable color output. + Never, +} + +impl Default for Shell { + #[inline] + fn default() -> Self { + Self::new() + } +} + +impl Shell { + /// Creates a new shell (color choice and verbosity), defaulting to 'auto' color and verbose + /// output. + #[inline] + pub fn new() -> Self { + Self::new_with(ColorChoice::Auto, Verbosity::Verbose) + } + + /// Creates a new shell with the given color choice and verbosity. + #[inline] + pub fn new_with(color: ColorChoice, verbosity: Verbosity) -> Self { + Self { + output: ShellOut::Stream { + stdout: AutoStream::new(std::io::stdout(), color.to_anstream_color_choice()), + stderr: AutoStream::new(std::io::stderr(), color.to_anstream_color_choice()), + color_choice: color, + stderr_tty: std::io::stderr().is_terminal(), + }, + verbosity, + needs_clear: AtomicBool::new(false), + } + } + + /// Creates a shell that ignores all output. + #[inline] + pub fn empty() -> Self { + Self { + output: ShellOut::Empty(std::io::empty()), + verbosity: Verbosity::Quiet, + needs_clear: AtomicBool::new(false), + } + } + + /// Get a static reference to the global shell. + #[inline] + #[cfg_attr(debug_assertions, track_caller)] + pub fn get() -> impl DerefMut + 'static { + #[inline(never)] + #[cold] + #[cfg_attr(debug_assertions, track_caller)] + fn shell_get_fail() -> Mutex { + if cfg!(test) { + Mutex::new(Shell::new()) + } else { + panic!("attempted to get global shell before it was set"); + } + } + + GLOBAL_SHELL.get_or_init(shell_get_fail).lock().unwrap_or_else(PoisonError::into_inner) + } + + /// Set the global shell. + /// + /// # Panics + /// + /// Panics if the global shell has already been set. + #[inline] + #[track_caller] + pub fn set(self) { + if GLOBAL_SHELL.get().is_some() { + panic!("attempted to set global shell twice"); + } + GLOBAL_SHELL.get_or_init(|| Mutex::new(self)); + } + + /// Sets whether the next print should clear the current line and returns the previous value. + #[inline] + pub fn set_needs_clear(&self, needs_clear: bool) -> bool { + self.needs_clear.swap(needs_clear, Ordering::Relaxed) + } + + /// Returns `true` if the `needs_clear` flag is set. + #[inline] + pub fn needs_clear(&self) -> bool { + self.needs_clear.load(Ordering::Relaxed) + } + + /// Returns `true` if the `needs_clear` flag is unset. + #[inline] + pub fn is_cleared(&self) -> bool { + !self.needs_clear() + } + + /// Returns the width of the terminal in spaces, if any. + #[inline] + pub fn err_width(&self) -> TtyWidth { + match self.output { + ShellOut::Stream { stderr_tty: true, .. } => TtyWidth::get(), + _ => TtyWidth::NoTty, + } + } + + /// Gets the verbosity of the shell. + #[inline] + pub fn verbosity(&self) -> Verbosity { + self.verbosity + } + + /// Gets the current color choice. + /// + /// If we are not using a color stream, this will always return `Never`, even if the color + /// choice has been set to something else. + #[inline] + pub fn color_choice(&self) -> ColorChoice { + match self.output { + ShellOut::Stream { color_choice, .. } => color_choice, + ShellOut::Empty(_) => ColorChoice::Never, + } + } + + /// Returns `true` if stderr is a tty. + #[inline] + pub fn is_err_tty(&self) -> bool { + match self.output { + ShellOut::Stream { stderr_tty, .. } => stderr_tty, + ShellOut::Empty(_) => false, + } + } + + /// Whether `stderr` supports color. + #[inline] + pub fn err_supports_color(&self) -> bool { + match &self.output { + ShellOut::Stream { stderr, .. } => supports_color(stderr.current_choice()), + ShellOut::Empty(_) => false, + } + } + + /// Whether `stdout` supports color. + #[inline] + pub fn out_supports_color(&self) -> bool { + match &self.output { + ShellOut::Stream { stdout, .. } => supports_color(stdout.current_choice()), + ShellOut::Empty(_) => false, + } + } + + /// Gets a reference to the underlying stdout writer. + #[inline] + pub fn out(&mut self) -> &mut dyn Write { + self.maybe_err_erase_line(); + self.output.stdout() + } + + /// Gets a reference to the underlying stderr writer. + #[inline] + pub fn err(&mut self) -> &mut dyn Write { + self.maybe_err_erase_line(); + self.output.stderr() + } + + /// Erase from cursor to end of line if needed. + #[inline] + pub fn maybe_err_erase_line(&mut self) { + if self.err_supports_color() && self.set_needs_clear(false) { + // This is the "EL - Erase in Line" sequence. It clears from the cursor + // to the end of line. + // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences + let _ = self.output.stderr().write_all(b"\x1B[K"); + } + } + + /// Shortcut to right-align and color green a status message. + #[inline] + pub fn status(&mut self, status: T, message: U) -> Result<()> + where + T: fmt::Display, + U: fmt::Display, + { + self.print(&status, Some(&message), &HEADER, true) + } + + /// Shortcut to right-align and color cyan a status without a message. + #[inline] + pub fn status_header(&mut self, status: impl fmt::Display) -> Result<()> { + self.print(&status, None, &NOTE, true) + } + + /// Shortcut to right-align a status message. + #[inline] + pub fn status_with_color(&mut self, status: T, message: U, color: &Style) -> Result<()> + where + T: fmt::Display, + U: fmt::Display, + { + self.print(&status, Some(&message), color, true) + } + + /// Runs the callback only if we are in verbose mode. + #[inline] + pub fn verbose(&mut self, mut callback: impl FnMut(&mut Self) -> Result<()>) -> Result<()> { + match self.verbosity { + Verbosity::Verbose => callback(self), + _ => Ok(()), + } + } + + /// Runs the callback if we are not in verbose mode. + #[inline] + pub fn concise(&mut self, mut callback: impl FnMut(&mut Self) -> Result<()>) -> Result<()> { + match self.verbosity { + Verbosity::Verbose => Ok(()), + _ => callback(self), + } + } + + /// Prints a red 'error' message. Use the [`sh_err!`] macro instead. + #[inline] + pub fn error(&mut self, message: impl fmt::Display) -> Result<()> { + self.maybe_err_erase_line(); + self.output.message_stderr(&"error", Some(&message), &ERROR, false) + } + + /// Prints an amber 'warning' message. Use the [`sh_warn!`] macro instead. + #[inline] + pub fn warn(&mut self, message: impl fmt::Display) -> Result<()> { + match self.verbosity { + Verbosity::Quiet => Ok(()), + _ => self.print(&"warning", Some(&message), &WARN, false), + } + } + + /// Prints a cyan 'note' message. Use the [`sh_note!`] macro instead. + #[inline] + pub fn note(&mut self, message: impl fmt::Display) -> Result<()> { + self.print(&"note", Some(&message), &NOTE, false) + } + + /// Write a styled fragment. + /// + /// Caller is responsible for deciding whether [`Shell::verbosity`] is affects output. + #[inline] + pub fn write_stdout(&mut self, fragment: impl fmt::Display, color: &Style) -> Result<()> { + self.output.write_stdout(fragment, color) + } + + /// Write a styled fragment with the default color. Use the [`sh_print!`] macro instead. + /// + /// **Note**: `verbosity` is ignored. + #[inline] + pub fn print_out(&mut self, fragment: impl fmt::Display) -> Result<()> { + self.write_stdout(fragment, &Style::new()) + } + + /// Write a styled fragment + /// + /// Caller is responsible for deciding whether [`Shell::verbosity`] is affects output. + #[inline] + pub fn write_stderr(&mut self, fragment: impl fmt::Display, color: &Style) -> Result<()> { + self.output.write_stderr(fragment, color) + } + + /// Write a styled fragment with the default color. Use the [`sh_eprint!`] macro instead. + /// + /// **Note**: if `verbosity` is set to `Quiet`, this is a no-op. + #[inline] + pub fn print_err(&mut self, fragment: impl fmt::Display) -> Result<()> { + if self.verbosity == Verbosity::Quiet { + Ok(()) + } else { + self.write_stderr(fragment, &Style::new()) + } + } + + /// Prints a message to stderr and translates ANSI escape code into console colors. + #[inline] + pub fn print_ansi_stderr(&mut self, message: &[u8]) -> Result<()> { + self.maybe_err_erase_line(); + self.err().write_all(message)?; + Ok(()) + } + + /// Prints a message to stdout and translates ANSI escape code into console colors. + #[inline] + pub fn print_ansi_stdout(&mut self, message: &[u8]) -> Result<()> { + self.maybe_err_erase_line(); + self.out().write_all(message)?; + Ok(()) + } + + /// Serializes an object to JSON and prints it to `stdout`. + #[inline] + pub fn print_json(&mut self, obj: &impl serde::Serialize) -> Result<()> { + // Path may fail to serialize to JSON ... + let encoded = serde_json::to_string(&obj)?; + // ... but don't fail due to a closed pipe. + let _ = writeln!(self.out(), "{encoded}"); + Ok(()) + } + + /// Prints a message, where the status will have `color` color, and can be justified. The + /// messages follows without color. + fn print( + &mut self, + status: &dyn fmt::Display, + message: Option<&dyn fmt::Display>, + color: &Style, + justified: bool, + ) -> Result<()> { + match self.verbosity { + Verbosity::Quiet => Ok(()), + _ => { + self.maybe_err_erase_line(); + self.output.message_stderr(status, message, color, justified) + } + } + } +} + +impl ShellOut { + /// Prints out a message with a status. The status comes first, and is bold plus the given + /// color. The status can be justified, in which case the max width that will right align is + /// 12 chars. + fn message_stderr( + &mut self, + status: &dyn fmt::Display, + message: Option<&dyn fmt::Display>, + style: &Style, + justified: bool, + ) -> Result<()> { + let style = style.render(); + let bold = (anstyle::Style::new() | anstyle::Effects::BOLD).render(); + let reset = anstyle::Reset.render(); + + let mut buffer = Vec::new(); + if justified { + write!(&mut buffer, "{style}{status:>12}{reset}")?; + } else { + write!(&mut buffer, "{style}{status}{reset}{bold}:{reset}")?; + } + match message { + Some(message) => writeln!(buffer, " {message}")?, + None => write!(buffer, " ")?, + } + self.stderr().write_all(&buffer)?; + Ok(()) + } + + /// Write a styled fragment + fn write_stdout(&mut self, fragment: impl fmt::Display, style: &Style) -> Result<()> { + let style = style.render(); + let reset = anstyle::Reset.render(); + + let mut buffer = Vec::new(); + write!(buffer, "{style}{fragment}{reset}")?; + self.stdout().write_all(&buffer)?; + Ok(()) + } + + /// Write a styled fragment + fn write_stderr(&mut self, fragment: impl fmt::Display, style: &Style) -> Result<()> { + let style = style.render(); + let reset = anstyle::Reset.render(); + + let mut buffer = Vec::new(); + write!(buffer, "{style}{fragment}{reset}")?; + self.stderr().write_all(&buffer)?; + Ok(()) + } + + /// Gets stdout as a [`io::Write`](Write) trait object. + #[inline] + fn stdout(&mut self) -> &mut dyn Write { + match self { + Self::Stream { stdout, .. } => stdout, + Self::Empty(e) => e, + } + } + + /// Gets stderr as a [`io::Write`](Write) trait object. + #[inline] + fn stderr(&mut self) -> &mut dyn Write { + match self { + Self::Stream { stderr, .. } => stderr, + Self::Empty(e) => e, + } + } +} + +impl ColorChoice { + /// Converts our color choice to [`anstream`]'s version. + fn to_anstream_color_choice(self) -> anstream::ColorChoice { + match self { + Self::Always => anstream::ColorChoice::Always, + Self::Never => anstream::ColorChoice::Never, + Self::Auto => anstream::ColorChoice::Auto, + } + } +} + +fn supports_color(choice: anstream::ColorChoice) -> bool { + match choice { + anstream::ColorChoice::Always | + anstream::ColorChoice::AlwaysAnsi | + anstream::ColorChoice::Auto => true, + anstream::ColorChoice::Never => false, + } +} + +/// Deprecated +/// +/// Get a mutable reference to the global shell. +pub fn with_shell(callback: impl FnOnce(&mut Shell) -> T) -> T { + let mut shell = Shell::get(); + callback(&mut shell) +} + +/// Deprecated +/// +/// Prints the given message to the shell. +pub fn println(msg: impl fmt::Display) -> Result<()> { + with_shell(|shell| if !shell.verbosity.is_quiet() { shell.print_out(msg) } else { Ok(()) }) +} + +/// Deprecated +/// +/// Prints the given message serialized to JSON to the shell. +pub fn print_json(obj: &T) -> Result<()> { + with_shell(|shell| shell.print_json(obj)) +} + +/// Prints the given message to the shell +pub fn eprintln(msg: impl fmt::Display) -> Result<()> { + with_shell(|shell| if !shell.verbosity.is_quiet() { shell.print_err(msg) } else { Ok(()) }) +} diff --git a/crates/cli/src/stdin.rs b/crates/common/src/io/stdin.rs similarity index 76% rename from crates/cli/src/stdin.rs rename to crates/common/src/io/stdin.rs index 8242cc8057724..17b40a2cff1fe 100644 --- a/crates/cli/src/stdin.rs +++ b/crates/common/src/io/stdin.rs @@ -7,37 +7,6 @@ use std::{ str::FromStr, }; -/// Prints a message to [`stdout`][io::stdout] and [reads a line from stdin into a String](read). -/// -/// Returns `Result`, so sometimes `T` must be explicitly specified, like in `str::parse`. -/// -/// # Examples -/// -/// ```no_run -/// # use foundry_cli::prompt; -/// let response: String = prompt!("Would you like to continue? [y/N] ")?; -/// if !matches!(response.as_str(), "y" | "Y") { -/// return Ok(()) -/// } -/// # Ok::<(), Box>(()) -/// ``` -#[macro_export] -macro_rules! prompt { - () => { - $crate::stdin::parse_line() - }; - - ($($tt:tt)+) => { - { - ::std::print!($($tt)+); - match ::std::io::Write::flush(&mut ::std::io::stdout()) { - ::core::result::Result::Ok(_) => $crate::prompt!(), - ::core::result::Result::Err(e) => ::core::result::Result::Err(::eyre::eyre!("Could not flush stdout: {}", e)) - } - } - }; -} - /// Unwraps the given `Option` or [reads stdin into a String](read) and parses it as `T`. pub fn unwrap(value: Option, read_line: bool) -> Result where @@ -50,6 +19,7 @@ where } } +/// Shortcut for `(unwrap(a), unwrap(b))`. #[inline] pub fn unwrap2(a: Option, b: Option) -> Result<(A, B)> where diff --git a/crates/common/src/io/style.rs b/crates/common/src/io/style.rs new file mode 100644 index 0000000000000..93e5b3260bf40 --- /dev/null +++ b/crates/common/src/io/style.rs @@ -0,0 +1,14 @@ +#![allow(missing_docs)] +use anstyle::*; + +pub const NOP: Style = Style::new(); +pub const HEADER: Style = AnsiColor::Green.on_default().effects(Effects::BOLD); +pub const USAGE: Style = AnsiColor::Green.on_default().effects(Effects::BOLD); +pub const LITERAL: Style = AnsiColor::Cyan.on_default().effects(Effects::BOLD); +pub const PLACEHOLDER: Style = AnsiColor::Cyan.on_default(); +pub const ERROR: Style = AnsiColor::Red.on_default().effects(Effects::BOLD); +pub const WARN: Style = AnsiColor::Yellow.on_default().effects(Effects::BOLD); +pub const NOTE: Style = AnsiColor::Cyan.on_default().effects(Effects::BOLD); +pub const GOOD: Style = AnsiColor::Green.on_default().effects(Effects::BOLD); +pub const VALID: Style = AnsiColor::Cyan.on_default().effects(Effects::BOLD); +pub const INVALID: Style = AnsiColor::Yellow.on_default().effects(Effects::BOLD); diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index a33a7b2231565..a2f344cc33c00 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -11,6 +11,9 @@ extern crate self as foundry_common; #[macro_use] extern crate tracing; +#[macro_use] +pub mod io; + pub use foundry_common_fmt as fmt; pub mod abi; @@ -26,7 +29,6 @@ pub mod provider; pub mod retry; pub mod selectors; pub mod serde_helpers; -pub mod shell; pub mod term; pub mod traits; pub mod transactions; @@ -37,3 +39,5 @@ pub use contracts::*; pub use traits::*; pub use transactions::*; pub use utils::*; + +pub use io::{shell, stdin, Shell}; diff --git a/crates/common/src/shell.rs b/crates/common/src/shell.rs deleted file mode 100644 index 8ab98e64a9c72..0000000000000 --- a/crates/common/src/shell.rs +++ /dev/null @@ -1,307 +0,0 @@ -//! Helpers for printing to output - -use serde::Serialize; -use std::{ - error::Error, - fmt, io, - io::Write, - sync::{Arc, Mutex, OnceLock}, -}; - -/// Stores the configured shell for the duration of the program -static SHELL: OnceLock = OnceLock::new(); - -/// Error indicating that `set_hook` was unable to install the provided ErrorHook -#[derive(Clone, Copy, Debug)] -pub struct InstallError; - -impl fmt::Display for InstallError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("cannot install provided Shell, a shell has already been installed") - } -} - -impl Error for InstallError {} - -/// Install the provided shell -pub fn set_shell(shell: Shell) -> Result<(), InstallError> { - SHELL.set(shell).map_err(|_| InstallError) -} - -/// Runs the given closure with the current shell, or default shell if none was set -pub fn with_shell(f: F) -> R -where - F: FnOnce(&Shell) -> R, -{ - if let Some(shell) = SHELL.get() { - f(shell) - } else { - let shell = Shell::default(); - f(&shell) - } -} - -/// Prints the given message to the shell -pub fn println(msg: impl fmt::Display) -> io::Result<()> { - with_shell(|shell| if !shell.verbosity.is_silent() { shell.write_stdout(msg) } else { Ok(()) }) -} -/// Prints the given message to the shell -pub fn print_json(obj: &T) -> serde_json::Result<()> { - with_shell(|shell| shell.print_json(obj)) -} - -/// Prints the given message to the shell -pub fn eprintln(msg: impl fmt::Display) -> io::Result<()> { - with_shell(|shell| if !shell.verbosity.is_silent() { shell.write_stderr(msg) } else { Ok(()) }) -} - -/// Returns the configured verbosity -pub fn verbosity() -> Verbosity { - with_shell(|shell| shell.verbosity) -} - -/// An abstraction around console output that also considers verbosity -#[derive(Default)] -pub struct Shell { - /// Wrapper around stdout/stderr. - output: ShellOut, - /// How to emit messages. - verbosity: Verbosity, -} - -impl Shell { - /// Creates a new shell instance - pub fn new(output: ShellOut, verbosity: Verbosity) -> Self { - Self { output, verbosity } - } - - /// Returns a new shell that conforms to the specified verbosity arguments, where `json` - /// or `junit` takes higher precedence. - pub fn from_args(silent: bool, json: bool) -> Self { - match (silent, json) { - (_, true) => Self::json(), - (true, _) => Self::silent(), - _ => Default::default(), - } - } - - /// Returns a new shell that won't emit anything - pub fn silent() -> Self { - Self::from_verbosity(Verbosity::Silent) - } - - /// Returns a new shell that'll only emit json - pub fn json() -> Self { - Self::from_verbosity(Verbosity::Json) - } - - /// Creates a new shell instance with default output and the given verbosity - pub fn from_verbosity(verbosity: Verbosity) -> Self { - Self::new(Default::default(), verbosity) - } - - /// Write a fragment to stdout - /// - /// Caller is responsible for deciding whether [`Shell`] verbosity affects output. - pub fn write_stdout(&self, fragment: impl fmt::Display) -> io::Result<()> { - self.output.write_stdout(fragment) - } - - /// Write a fragment to stderr - /// - /// Caller is responsible for deciding whether [`Shell`] verbosity affects output. - pub fn write_stderr(&self, fragment: impl fmt::Display) -> io::Result<()> { - self.output.write_stderr(fragment) - } - - /// Prints the object to stdout as json - pub fn print_json(&self, obj: &T) -> serde_json::Result<()> { - if self.verbosity.is_json() { - let json = serde_json::to_string(&obj)?; - let _ = self.output.with_stdout(|out| writeln!(out, "{json}")); - } - Ok(()) - } - /// Prints the object to stdout as pretty json - pub fn pretty_print_json(&self, obj: &T) -> serde_json::Result<()> { - if self.verbosity.is_json() { - let json = serde_json::to_string_pretty(&obj)?; - let _ = self.output.with_stdout(|out| writeln!(out, "{json}")); - } - Ok(()) - } -} - -impl fmt::Debug for Shell { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.output { - ShellOut::Write(_) => { - f.debug_struct("Shell").field("verbosity", &self.verbosity).finish() - } - ShellOut::Stream => { - f.debug_struct("Shell").field("verbosity", &self.verbosity).finish() - } - } - } -} - -/// Helper trait for custom shell output -/// -/// Can be used for debugging -pub trait ShellWrite { - /// Write the fragment - fn write(&self, fragment: impl fmt::Display) -> io::Result<()>; - - /// Executes a closure on the current stdout - fn with_stdout(&self, f: F) -> R - where - for<'r> F: FnOnce(&'r mut (dyn Write + 'r)) -> R; - - /// Executes a closure on the current stderr - fn with_err(&self, f: F) -> R - where - for<'r> F: FnOnce(&'r mut (dyn Write + 'r)) -> R; -} - -/// A guarded shell output type -pub struct WriteShellOut(Arc>>); - -unsafe impl Send for WriteShellOut {} -unsafe impl Sync for WriteShellOut {} - -impl ShellWrite for WriteShellOut { - fn write(&self, fragment: impl fmt::Display) -> io::Result<()> { - if let Ok(mut lock) = self.0.lock() { - writeln!(lock, "{fragment}")?; - } - Ok(()) - } - /// Executes a closure on the current stdout - fn with_stdout(&self, f: F) -> R - where - for<'r> F: FnOnce(&'r mut (dyn Write + 'r)) -> R, - { - let mut lock = self.0.lock().unwrap(); - f(&mut *lock) - } - - /// Executes a closure on the current stderr - fn with_err(&self, f: F) -> R - where - for<'r> F: FnOnce(&'r mut (dyn Write + 'r)) -> R, - { - let mut lock = self.0.lock().unwrap(); - f(&mut *lock) - } -} - -/// A `Write`able object, either with or without color support -#[derive(Default)] -pub enum ShellOut { - /// A plain write object - /// - /// Can be used for debug purposes - Write(WriteShellOut), - /// Streams to `stdio` - #[default] - Stream, -} - -impl ShellOut { - /// Creates a new shell that writes to memory - pub fn memory() -> Self { - #[allow(clippy::box_default)] - #[allow(clippy::arc_with_non_send_sync)] - Self::Write(WriteShellOut(Arc::new(Mutex::new(Box::new(Vec::new()))))) - } - - /// Write a fragment to stdout - fn write_stdout(&self, fragment: impl fmt::Display) -> io::Result<()> { - match *self { - Self::Stream => { - let stdout = io::stdout(); - let mut handle = stdout.lock(); - writeln!(handle, "{fragment}")?; - } - Self::Write(ref w) => { - w.write(fragment)?; - } - } - Ok(()) - } - - /// Write output to stderr - fn write_stderr(&self, fragment: impl fmt::Display) -> io::Result<()> { - match *self { - Self::Stream => { - let stderr = io::stderr(); - let mut handle = stderr.lock(); - writeln!(handle, "{fragment}")?; - } - Self::Write(ref w) => { - w.write(fragment)?; - } - } - Ok(()) - } - - /// Executes a closure on the current stdout - fn with_stdout(&self, f: F) -> R - where - for<'r> F: FnOnce(&'r mut (dyn Write + 'r)) -> R, - { - match *self { - Self::Stream => { - let stdout = io::stdout(); - let mut handler = stdout.lock(); - f(&mut handler) - } - Self::Write(ref w) => w.with_stdout(f), - } - } - - /// Executes a closure on the current stderr - #[allow(unused)] - fn with_err(&self, f: F) -> R - where - for<'r> F: FnOnce(&'r mut (dyn Write + 'r)) -> R, - { - match *self { - Self::Stream => { - let stderr = io::stderr(); - let mut handler = stderr.lock(); - f(&mut handler) - } - Self::Write(ref w) => w.with_err(f), - } - } -} - -/// The requested verbosity of output. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum Verbosity { - /// only allow json output - Json, - /// print as is - #[default] - Normal, - /// print nothing - Silent, -} - -impl Verbosity { - /// Returns true if json mode - pub fn is_json(&self) -> bool { - matches!(self, Self::Json) - } - - /// Returns true if silent - pub fn is_silent(&self) -> bool { - matches!(self, Self::Silent) - } - - /// Returns true if normal verbosity - pub fn is_normal(&self) -> bool { - matches!(self, Self::Normal) - } -} diff --git a/crates/forge/bin/cmd/build.rs b/crates/forge/bin/cmd/build.rs index 53bc5bc2001b9..f55e0f9b14b12 100644 --- a/crates/forge/bin/cmd/build.rs +++ b/crates/forge/bin/cmd/build.rs @@ -70,7 +70,7 @@ pub struct BuildArgs { /// Output the compilation errors in the json format. /// This is useful when you want to use the output in other tools. - #[arg(long, conflicts_with = "silent")] + #[clap(long, conflicts_with = "quiet")] #[serde(skip)] pub format_json: bool, } @@ -79,9 +79,7 @@ impl BuildArgs { pub fn run(self) -> Result { let mut config = self.try_load_config_emit_warnings()?; - if install::install_missing_dependencies(&mut config, self.args.silent) && - config.auto_detect_remappings - { + if install::install_missing_dependencies(&mut config) && config.auto_detect_remappings { // need to re-configure here to also catch additional remappings config = self.load_config(); } diff --git a/crates/forge/bin/cmd/clone.rs b/crates/forge/bin/cmd/clone.rs index f3879b931b778..88052d85dd365 100644 --- a/crates/forge/bin/cmd/clone.rs +++ b/crates/forge/bin/cmd/clone.rs @@ -7,7 +7,7 @@ use foundry_block_explorers::{ errors::EtherscanError, Client, }; -use foundry_cli::{opts::EtherscanOpts, p_println, utils::Git}; +use foundry_cli::{opts::EtherscanOpts, utils::Git}; use foundry_common::{compile::ProjectCompiler, fs}; use foundry_compilers::{ artifacts::{ @@ -102,7 +102,7 @@ impl CloneArgs { let client = Client::new(chain, etherscan_api_key.clone())?; // step 1. get the metadata from client - p_println!(!opts.quiet => "Downloading the source code of {} from Etherscan...", address); + sh_note!("Downloading the source code of {} from Etherscan", address)?; let meta = Self::collect_metadata_from_client(address, &client).await?; // step 2. initialize an empty project @@ -117,17 +117,16 @@ impl CloneArgs { // step 4. collect the compilation metadata // if the etherscan api key is not set, we need to wait for 3 seconds between calls - p_println!(!opts.quiet => "Collecting the creation information of {} from Etherscan...", address); + sh_note!("Collecting the creation information of {} from Etherscan...", address)?; if etherscan_api_key.is_empty() { - p_println!(!opts.quiet => "Waiting for 5 seconds to avoid rate limit..."); + sh_eprintln!("Waiting for 5 seconds to avoid rate limit...")?; tokio::time::sleep(Duration::from_secs(5)).await; } - Self::collect_compilation_metadata(&meta, chain, address, &root, &client, opts.quiet) - .await?; + Self::collect_compilation_metadata(&meta, chain, address, &root, &client).await?; // step 5. git add and commit the changes if needed if !opts.no_commit { - let git = Git::new(&root).quiet(opts.quiet); + let git = Git::new(&root); git.add(Some("--all"))?; let msg = format!("chore: forge clone {address}"); git.commit(&msg)?; @@ -185,10 +184,9 @@ impl CloneArgs { address: Address, root: &PathBuf, client: &C, - quiet: bool, ) -> Result<()> { // compile the cloned contract - let compile_output = compile_project(root, quiet)?; + let compile_output = compile_project(root)?; let (main_file, main_artifact) = find_main_contract(&compile_output, &meta.contract_name)?; let main_file = main_file.strip_prefix(root)?.to_path_buf(); let storage_layout = @@ -546,11 +544,11 @@ fn dump_sources(meta: &Metadata, root: &PathBuf, no_reorg: bool) -> Result Result { +pub fn compile_project(root: &Path) -> Result { let mut config = Config::load_with_root(root).sanitized(); config.extra_output.push(ContractOutputSelection::StorageLayout); let project = config.project()?; - let compiler = ProjectCompiler::new().quiet_if(quiet); + let compiler = ProjectCompiler::new(); compiler.compile(&project) } @@ -619,7 +617,7 @@ mod tests { fn assert_successful_compilation(root: &PathBuf) -> ProjectCompileOutput { println!("project_root: {root:#?}"); - compile_project(root, false).expect("compilation failure") + compile_project(root).expect("compilation failure") } fn assert_compilation_result( @@ -721,7 +719,6 @@ mod tests { address, &project_root, &client, - false, ) .await .unwrap(); diff --git a/crates/forge/bin/cmd/coverage.rs b/crates/forge/bin/cmd/coverage.rs index 487c0a7f15802..f2f3f5c31d573 100644 --- a/crates/forge/bin/cmd/coverage.rs +++ b/crates/forge/bin/cmd/coverage.rs @@ -13,10 +13,7 @@ use forge::{ utils::IcPcMap, MultiContractRunnerBuilder, TestOptions, }; -use foundry_cli::{ - p_println, - utils::{LoadConfig, STATIC_FUZZ_SEED}, -}; +use foundry_cli::utils::{LoadConfig, STATIC_FUZZ_SEED}; use foundry_common::{compile::ProjectCompiler, fs}; use foundry_compilers::{ artifacts::{sourcemap::SourceMap, CompactBytecode, CompactDeployedBytecode}, @@ -76,9 +73,7 @@ impl CoverageArgs { let (mut config, evm_opts) = self.load_config_and_evm_opts_emit_warnings()?; // install missing dependencies - if install::install_missing_dependencies(&mut config, self.test.build_args().silent) && - config.auto_detect_remappings - { + if install::install_missing_dependencies(&mut config) && config.auto_detect_remappings { // need to re-configure here to also catch additional remappings config = self.load_config(); } @@ -90,10 +85,10 @@ impl CoverageArgs { config.ast = true; let (project, output) = self.build(&config)?; - p_println!(!self.test.build_args().silent => "Analysing contracts..."); + sh_eprintln!("Analysing contracts...")?; let report = self.prepare(&project, &output)?; - p_println!(!self.test.build_args().silent => "Running tests..."); + sh_eprintln!("Running tests...")?; self.collect(project, &output, report, Arc::new(config), evm_opts).await } @@ -121,7 +116,7 @@ impl CoverageArgs { "Note that \"viaIR\" is only available in Solidity 0.8.13 and above.\n", "See more: https://github.com/foundry-rs/foundry/issues/3357", ).yellow(); - p_println!(!self.test.build_args().silent => "{msg}"); + sh_eprintln!("{msg}")?; // Enable viaIR with minimum optimization // https://github.com/ethereum/solidity/issues/12533#issuecomment-1013073350 diff --git a/crates/forge/bin/cmd/create.rs b/crates/forge/bin/cmd/create.rs index 3bcf6b36cbde1..d08a419435dfc 100644 --- a/crates/forge/bin/cmd/create.rs +++ b/crates/forge/bin/cmd/create.rs @@ -108,8 +108,7 @@ impl CreateArgs { project.find_contract_path(&self.contract.name)? }; - let mut output = - compile::compile_target(&target_path, &project, self.json || self.opts.silent)?; + let mut output = compile::compile_target(&target_path, &project, self.json)?; let (abi, bin, _) = remove_contract(&mut output, &target_path, &self.contract.name)?; diff --git a/crates/forge/bin/cmd/init.rs b/crates/forge/bin/cmd/init.rs index 1882eca60c64a..060347dcb3acf 100644 --- a/crates/forge/bin/cmd/init.rs +++ b/crates/forge/bin/cmd/init.rs @@ -1,12 +1,14 @@ use super::install::DependencyInstallOpts; use clap::{Parser, ValueHint}; use eyre::Result; -use foundry_cli::{p_println, utils::Git}; +use foundry_cli::utils::Git; use foundry_common::fs; use foundry_compilers::artifacts::remappings::Remapping; use foundry_config::Config; -use std::path::{Path, PathBuf}; -use yansi::Paint; +use std::{ + fmt::Write, + path::{Path, PathBuf}, +}; /// CLI arguments for `forge init`. #[derive(Clone, Debug, Default, Parser)] @@ -44,25 +46,26 @@ pub struct InitArgs { impl InitArgs { pub fn run(self) -> Result<()> { let Self { root, template, branch, opts, offline, force, vscode } = self; - let DependencyInstallOpts { shallow, no_git, no_commit, quiet } = opts; + let DependencyInstallOpts { shallow, no_git, no_commit } = opts; // create the root dir if it does not exist if !root.exists() { fs::create_dir_all(&root)?; } - let root = dunce::canonicalize(root)?; - let git = Git::new(&root).quiet(quiet).shallow(shallow); + let root_rel = &root; + let root = dunce::canonicalize(&root)?; + let git = Git::new(&root).shallow(shallow); // if a template is provided, then this command initializes a git repo, // fetches the template repo, and resets the git history to the head of the fetched // repo with no other history - if let Some(template) = template { + if let Some(template) = &template { let template = if template.contains("://") { - template + template.clone() } else { "https://github.com/".to_string() + &template }; - p_println!(!quiet => "Initializing {} from {}...", root.display(), template); + sh_status!("Initializing" => "{} from {template}", root_rel.display())?; // initialize the git repository git.init()?; @@ -96,7 +99,7 @@ impl InitArgs { ); } - p_println!(!quiet => "Target directory is not empty, but `--force` was specified"); + sh_note!("Target directory is not empty, but `--force` was specified")?; } // ensure git status is clean before generating anything @@ -104,7 +107,7 @@ impl InitArgs { git.ensure_clean()?; } - p_println!(!quiet => "Initializing {}...", root.display()); + sh_status!("Initializing" => "{}...", root_rel.display())?; // make the dirs let src = root.join("src"); @@ -145,7 +148,7 @@ impl InitArgs { // install forge-std if !offline { if root.join("lib/forge-std").exists() { - p_println!(!quiet => "\"lib/forge-std\" already exists, skipping install...."); + sh_status!("Skipping" => "forge-std install")?; self.opts.install(&mut config, vec![])?; } else { let dep = "https://github.com/foundry-rs/forge-std".parse()?; @@ -159,7 +162,15 @@ impl InitArgs { } } - p_println!(!quiet => " {} forge project", "Initialized".green()); + let mut msg = "Foundry project".to_string(); + if let Some(template) = &template { + write!(msg, " from {template}").unwrap(); + } + if root_rel != Path::new(".") { + write!(msg, " in {}", root_rel.display()).unwrap(); + } + sh_status!("Created" => "{msg}")?; + Ok(()) } } diff --git a/crates/forge/bin/cmd/install.rs b/crates/forge/bin/cmd/install.rs index 448d5b1ad7b7b..9884ee16ab85e 100644 --- a/crates/forge/bin/cmd/install.rs +++ b/crates/forge/bin/cmd/install.rs @@ -2,7 +2,6 @@ use clap::{Parser, ValueHint}; use eyre::{Context, Result}; use foundry_cli::{ opts::Dependency, - p_println, prompt, utils::{CommandUtils, Git, LoadConfig}, }; use foundry_common::fs; @@ -77,15 +76,11 @@ pub struct DependencyInstallOpts { /// Do not create a commit. #[arg(long)] pub no_commit: bool, - - /// Do not print any messages. - #[arg(short, long)] - pub quiet: bool, } impl DependencyInstallOpts { pub fn git(self, config: &Config) -> Git<'_> { - Git::from_config(config).quiet(self.quiet).shallow(self.shallow) + Git::from_config(config).shallow(self.shallow) } /// Installs all missing dependencies. @@ -94,27 +89,23 @@ impl DependencyInstallOpts { /// /// Returns true if any dependency was installed. pub fn install_missing_dependencies(mut self, config: &mut Config) -> bool { - let Self { quiet, .. } = self; let lib = config.install_lib_dir(); if self.git(config).has_missing_dependencies(Some(lib)).unwrap_or(false) { // The extra newline is needed, otherwise the compiler output will overwrite the message - p_println!(!quiet => "Missing dependencies found. Installing now...\n"); + let _ = sh_eprintln!("Missing dependencies found. Installing now...\n"); self.no_commit = true; - if self.install(config, Vec::new()).is_err() && !quiet { - eprintln!( - "{}", - "Your project has missing dependencies that could not be installed.".yellow() - ) + if self.install(config, Vec::new()).is_err() { + let _ = + sh_warn!("Your project has missing dependencies that could not be installed."); } true } else { false } } - /// Installs all dependencies pub fn install(self, config: &mut Config, dependencies: Vec) -> Result<()> { - let Self { no_git, no_commit, quiet, .. } = self; + let Self { no_git, no_commit, .. } = self; let git = self.git(config); @@ -126,7 +117,7 @@ impl DependencyInstallOpts { let root = Git::root_of(git.root)?; match git.has_submodules(Some(&root)) { Ok(true) => { - p_println!(!quiet => "Updating dependencies in {}", libs.display()); + sh_status!("Updating" => "dependencies in {}", libs.display())?; // recursively fetch all submodules (without fetching latest) git.submodule_update(false, false, false, true, Some(&libs))?; } @@ -148,7 +139,7 @@ impl DependencyInstallOpts { let rel_path = path .strip_prefix(git.root) .wrap_err("Library directory is not relative to the repository root")?; - p_println!(!quiet => "Installing {} in {} (url: {:?}, tag: {:?})", dep.name, path.display(), dep.url, dep.tag); + sh_status!("Installing" => "{dep} to {}", path.display())?; // this tracks the actual installed tag let installed_tag; @@ -190,14 +181,12 @@ impl DependencyInstallOpts { } } - if !quiet { - let mut msg = format!(" {} {}", "Installed".green(), dep.name); - if let Some(tag) = dep.tag.or(installed_tag) { - msg.push(' '); - msg.push_str(tag.as_str()); - } - println!("{msg}"); + let mut msg = format!(" {} {}", "Installed".green(), dep.name); + if let Some(tag) = dep.tag.or(installed_tag) { + msg.push(' '); + msg.push_str(tag.as_str()); } + println!("{msg}"); } // update `libs` in config if not included yet @@ -209,8 +198,8 @@ impl DependencyInstallOpts { } } -pub fn install_missing_dependencies(config: &mut Config, quiet: bool) -> bool { - DependencyInstallOpts { quiet, ..Default::default() }.install_missing_dependencies(config) +pub fn install_missing_dependencies(config: &mut Config) -> bool { + DependencyInstallOpts::default().install_missing_dependencies(config) } #[derive(Clone, Copy, Debug)] diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index 6bf1ffd8183a4..926c1558c5216 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -181,7 +181,6 @@ impl TestArgs { pub async fn run(self) -> Result { trace!(target: "forge::test", "executing test command"); - shell::set_shell(shell::Shell::from_args(self.opts.silent, self.json || self.junit))?; self.execute_tests().await } @@ -286,9 +285,7 @@ impl TestArgs { let mut project = config.project()?; // Install missing dependencies. - if install::install_missing_dependencies(&mut config, self.build_args().silent) && - config.auto_detect_remappings - { + if install::install_missing_dependencies(&mut config) && config.auto_detect_remappings { // need to re-configure here to also catch additional remappings config = self.load_config(); project = config.project()?; @@ -299,9 +296,8 @@ impl TestArgs { let sources_to_compile = self.get_sources_to_compile(&config, &filter)?; - let compiler = ProjectCompiler::new() - .quiet_if(self.json || self.junit || self.opts.silent) - .files(sources_to_compile); + let compiler = + ProjectCompiler::new().quiet_if(self.json || self.junit).files(sources_to_compile); let output = compiler.compile(&project)?; diff --git a/crates/forge/bin/main.rs b/crates/forge/bin/main.rs index 9a98d8aeffd02..0b5f86711727a 100644 --- a/crates/forge/bin/main.rs +++ b/crates/forge/bin/main.rs @@ -1,12 +1,15 @@ -#[macro_use] -extern crate tracing; - use clap::{CommandFactory, Parser}; use clap_complete::generate; use eyre::Result; use foundry_cli::{handler, utils}; use foundry_evm::inspectors::cheatcodes::{set_execution_context, ForgeContext}; +#[macro_use] +extern crate foundry_common; + +#[macro_use] +extern crate tracing; + mod cmd; use cmd::{cache::CacheSubcommands, generate::GenerateSubcommands, watch}; @@ -17,13 +20,21 @@ use opts::{Forge, ForgeSubcommand}; #[global_allocator] static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; -fn main() -> Result<()> { +fn main() { + if let Err(err) = run() { + let _ = foundry_common::Shell::get().error(&err); + std::process::exit(1); + } +} + +fn run() -> Result<()> { handler::install(); utils::load_dotenv(); utils::subscriber(); utils::enable_paint(); let opts = Forge::parse(); + opts.shell.shell().set(); init_execution_context(&opts.cmd); match opts.cmd { @@ -35,14 +46,7 @@ fn main() -> Result<()> { outcome.ensure_ok() } } - ForgeSubcommand::Script(cmd) => { - // install the shell before executing the command - foundry_common::shell::set_shell(foundry_common::shell::Shell::from_args( - cmd.opts.silent, - cmd.json, - ))?; - utils::block_on(cmd.run_script()) - } + ForgeSubcommand::Script(cmd) => utils::block_on(cmd.run_script()), ForgeSubcommand::Coverage(cmd) => utils::block_on(cmd.run()), ForgeSubcommand::Bind(cmd) => cmd.run(), ForgeSubcommand::Build(cmd) => { diff --git a/crates/forge/bin/opts.rs b/crates/forge/bin/opts.rs index c929d0185ba7e..a7a4ee1d1c601 100644 --- a/crates/forge/bin/opts.rs +++ b/crates/forge/bin/opts.rs @@ -7,6 +7,7 @@ use crate::cmd::{ use clap::{Parser, Subcommand, ValueHint}; use forge_script::ScriptArgs; use forge_verify::{VerifyArgs, VerifyBytecodeArgs, VerifyCheckArgs}; +use foundry_cli::opts::ShellOpts; use std::path::PathBuf; const VERSION_MESSAGE: &str = concat!( @@ -29,6 +30,8 @@ const VERSION_MESSAGE: &str = concat!( pub struct Forge { #[command(subcommand)] pub cmd: ForgeSubcommand, + #[clap(flatten)] + pub shell: ShellOpts, } #[derive(Subcommand)] diff --git a/crates/forge/src/result.rs b/crates/forge/src/result.rs index 171a234a5ee02..0d24d2c372d32 100644 --- a/crates/forge/src/result.rs +++ b/crates/forge/src/result.rs @@ -155,7 +155,7 @@ impl TestOutcome { return Ok(()); } - if !shell::verbosity().is_normal() { + if foundry_common::Shell::get().verbosity().is_quiet() { // TODO: Avoid process::exit std::process::exit(1); } diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index b0c5a2947a4d1..9fac7f4bf59b3 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -190,10 +190,7 @@ impl PreprocessedState { ) .chain([target_path.to_path_buf()]); - let output = ProjectCompiler::new() - .quiet_if(args.opts.silent) - .files(sources_to_compile) - .compile(&project)?; + let output = ProjectCompiler::new().files(sources_to_compile).compile(&project)?; let mut target_id: Option = None; diff --git a/crates/script/src/simulate.rs b/crates/script/src/simulate.rs index 7c692ea232efd..f253dc33dae1b 100644 --- a/crates/script/src/simulate.rs +++ b/crates/script/src/simulate.rs @@ -212,7 +212,7 @@ impl PreSimulationState { /// Build [ScriptRunner] forking given RPC for each RPC used in the script. async fn build_runners(&self) -> Result> { let rpcs = self.execution_artifacts.rpc_data.total_rpcs.clone(); - if !shell::verbosity().is_silent() { + if !shell::verbosity().is_quiet() { let n = rpcs.len(); let s = if n != 1 { "s" } else { "" }; println!("\n## Setting up {n} EVM{s}.");