From cd71da404df324f8a3851f9673e4686d2cd762ef Mon Sep 17 00:00:00 2001 From: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Date: Wed, 23 Oct 2024 14:14:25 +0200 Subject: [PATCH] feat: add `foundry_common::shell` to unify log behavior (#9109) * replace existing shell::println, add macros * finish replacing shell::println * remove p_println * remove redundant quiet or silent variables * install global shells in binaries * CastArgs -> Cast, Cast -> CastInstance * fix tests, always initialize Mutex::new(Shell::new()) on initial access, for some reason cfg!(test) path is not handled when running with tokio tests * revert .quiet(true) * add back quiet * undo CastInstance -> Cast, Cast -> CastArgs * add global --json format * use global --json flag * revert sequence / multisequence save silent mode * fix failing tests * fix tests * fix tests * replace cli_warn with sh_warn * use shell json directly instead of passing in * clean up, document print modes in respect to --quiet flag * group shell options under display options * revert global --json flag * remove redundant import * fix: quiet * remove faulty argument conflict test as there is no way to currently assert a conflict between global and local args without custom reject at runtime * add back conflicts_with quiet flag, global args w/ conflicts_with works fine * remove yellow() * Apply suggestions from code review - update dependencies - use default Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> * fix deprecated terminal_size method * revert quiet flag, fix os:fd import for windows * add replacing tests, add back quiet conflicting flag * make output windows compatible * to avoid visual regression, style warning message content in yellow, error message content in red - both not bold * fix docs links * fix junit throwing mixed content on warnings, avoid modifying global verbosity * remove set_verbosity shell helper, redundant * revert default .expect on printing, prefer passing. revert message style formatting - no longer style the message * fix doc test, fix formatting * fix merge issues --------- Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> --- Cargo.lock | 3 + Cargo.toml | 5 + clippy.toml | 8 + crates/anvil/src/anvil.rs | 16 +- crates/anvil/src/cmd.rs | 5 - crates/anvil/src/config.rs | 24 +- crates/anvil/src/lib.rs | 38 +- crates/anvil/tests/it/fork.rs | 8 +- crates/cast/bin/args.rs | 5 +- crates/cast/bin/cmd/run.rs | 2 +- crates/cast/bin/cmd/send.rs | 4 +- crates/cast/bin/main.rs | 16 +- crates/cast/tests/cli/main.rs | 40 +- crates/chisel/bin/main.rs | 15 +- crates/cli/src/lib.rs | 4 +- crates/cli/src/opts/build/core.rs | 5 - crates/cli/src/opts/mod.rs | 2 + crates/cli/src/opts/shell.rs | 39 ++ crates/cli/src/utils/cmd.rs | 18 +- crates/cli/src/utils/mod.rs | 24 +- crates/common/Cargo.toml | 4 + crates/common/fmt/Cargo.toml | 2 +- crates/common/src/compile.rs | 11 +- crates/common/src/io/macros.rs | 185 ++++++++ crates/common/src/io/mod.rs | 11 + crates/common/src/io/shell.rs | 518 +++++++++++++++++++++ crates/{cli/src => common/src/io}/stdin.rs | 32 +- crates/common/src/io/style.rs | 5 + crates/common/src/lib.rs | 5 +- crates/common/src/shell.rs | 307 ------------ crates/common/src/term.rs | 15 - crates/forge/bin/cmd/build.rs | 38 +- crates/forge/bin/cmd/clone.rs | 25 +- crates/forge/bin/cmd/config.rs | 6 +- crates/forge/bin/cmd/coverage.rs | 21 +- crates/forge/bin/cmd/create.rs | 3 +- crates/forge/bin/cmd/fmt.rs | 8 +- crates/forge/bin/cmd/init.rs | 17 +- crates/forge/bin/cmd/install.rs | 47 +- crates/forge/bin/cmd/snapshot.rs | 2 +- crates/forge/bin/cmd/test/mod.rs | 35 +- crates/forge/bin/main.rs | 37 +- crates/forge/bin/opts.rs | 4 + crates/forge/src/lib.rs | 3 + crates/forge/src/result.rs | 18 +- crates/forge/tests/cli/build.rs | 30 +- crates/forge/tests/cli/cmd.rs | 92 ++-- crates/forge/tests/cli/config.rs | 8 +- crates/forge/tests/cli/debug.rs | 11 +- crates/forge/tests/cli/script.rs | 91 ++-- crates/forge/tests/cli/test_cmd.rs | 11 +- crates/script-sequence/src/lib.rs | 3 + crates/script-sequence/src/sequence.rs | 6 +- crates/script/src/broadcast.rs | 6 +- crates/script/src/build.rs | 5 +- crates/script/src/execute.rs | 43 +- crates/script/src/lib.rs | 21 +- crates/script/src/multi_sequence.rs | 4 +- crates/script/src/simulate.rs | 34 +- crates/test-utils/src/script.rs | 8 +- crates/verify/src/bytecode.rs | 6 +- crates/verify/src/etherscan/mod.rs | 4 +- crates/verify/src/lib.rs | 9 +- 63 files changed, 1267 insertions(+), 765 deletions(-) create mode 100644 crates/cli/src/opts/shell.rs create mode 100644 crates/common/src/io/macros.rs create mode 100644 crates/common/src/io/mod.rs create mode 100644 crates/common/src/io/shell.rs rename crates/{cli/src => common/src/io}/stdin.rs (76%) create mode 100644 crates/common/src/io/style.rs delete mode 100644 crates/common/src/shell.rs diff --git a/Cargo.lock b/Cargo.lock index 42c763b6229f..cae078105157 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3717,6 +3717,8 @@ dependencies = [ "alloy-transport-http", "alloy-transport-ipc", "alloy-transport-ws", + "anstream", + "anstyle", "async-trait", "clap", "comfy-table", @@ -3733,6 +3735,7 @@ dependencies = [ "serde", "serde_json", "similar-asserts", + "terminal_size", "thiserror", "tokio", "tower 0.4.13", diff --git a/Cargo.toml b/Cargo.toml index a273b4a1bbec..db0ad3de4692 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -225,6 +225,11 @@ alloy-trie = "0.6.0" op-alloy-rpc-types = "0.5.0" op-alloy-consensus = "0.5.0" +## cli +anstream = "0.6.15" +anstyle = "1.0.8" +terminal_size = "0.4" + # macros proc-macro2 = "1.0.82" quote = "1.0" diff --git a/clippy.toml b/clippy.toml index 92b35ffc0afc..3e45486a86f4 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/anvil/src/anvil.rs b/crates/anvil/src/anvil.rs index 38bbbbbd62e3..9d4a0cf7ba20 100644 --- a/crates/anvil/src/anvil.rs +++ b/crates/anvil/src/anvil.rs @@ -2,7 +2,8 @@ use anvil::cmd::NodeArgs; use clap::{CommandFactory, Parser, Subcommand}; -use foundry_cli::utils; +use eyre::Result; +use foundry_cli::{opts::ShellOpts, utils}; #[cfg(all(feature = "jemalloc", unix))] #[global_allocator] @@ -17,6 +18,9 @@ pub struct Anvil { #[command(subcommand)] pub cmd: Option, + + #[clap(flatten)] + pub shell: ShellOpts, } #[derive(Subcommand)] @@ -33,10 +37,18 @@ pub enum AnvilSubcommand { GenerateFigSpec, } -fn main() -> eyre::Result<()> { +fn main() { + if let Err(err) = run() { + let _ = foundry_common::Shell::get().error(&err); + std::process::exit(1); + } +} + +fn run() -> Result<()> { utils::load_dotenv(); let mut args = Anvil::parse(); + args.shell.shell().set(); args.node.evm_opts.resolve_rpc_alias(); if let Some(cmd) = &args.cmd { diff --git a/crates/anvil/src/cmd.rs b/crates/anvil/src/cmd.rs index 13182686a333..2121f5b64795 100644 --- a/crates/anvil/src/cmd.rs +++ b/crates/anvil/src/cmd.rs @@ -72,10 +72,6 @@ pub struct NodeArgs { #[arg(long)] pub derivation_path: Option, - /// Don't print anything on startup and don't print logs - #[arg(long)] - pub silent: bool, - /// The EVM hardfork to use. /// /// Choose the hardfork by name, e.g. `shanghai`, `paris`, `london`, etc... @@ -258,7 +254,6 @@ impl NodeArgs { .with_storage_caching(self.evm_opts.no_storage_caching) .with_server_config(self.server_config) .with_host(self.host) - .set_silent(self.silent) .set_config_out(self.config_out) .with_chain_id(self.evm_opts.chain_id) .with_transaction_order(self.order) diff --git a/crates/anvil/src/config.rs b/crates/anvil/src/config.rs index 273dbad89ffd..a54da25a3be9 100644 --- a/crates/anvil/src/config.rs +++ b/crates/anvil/src/config.rs @@ -123,8 +123,6 @@ pub struct NodeConfig { pub port: u16, /// maximum number of transactions in a block pub max_transactions: usize, - /// don't print anything on startup - pub silent: bool, /// url of the rpc server that should be used for any rpc calls pub eth_rpc_url: Option, /// pins the block number or transaction hash for the state fork @@ -394,7 +392,7 @@ impl NodeConfig { /// random, free port by setting it to `0` #[doc(hidden)] pub fn test() -> Self { - Self { enable_tracing: true, silent: true, port: 0, ..Default::default() } + Self { enable_tracing: true, port: 0, ..Default::default() } } /// Returns a new config which does not initialize any accounts on node startup. @@ -429,7 +427,6 @@ impl Default for NodeConfig { port: NODE_PORT, // TODO make this something dependent on block capacity max_transactions: 1_000, - silent: false, eth_rpc_url: None, fork_choice: None, account_generator: None, @@ -732,18 +729,6 @@ impl NodeConfig { self } - /// Makes the node silent to not emit anything on stdout - #[must_use] - pub fn silent(self) -> Self { - self.set_silent(true) - } - - #[must_use] - pub fn set_silent(mut self, silent: bool) -> Self { - self.silent = silent; - self - } - /// Sets the ipc path to use /// /// Note: this is a double Option for @@ -763,7 +748,7 @@ impl NodeConfig { self } - /// Makes the node silent to not emit anything on stdout + /// Disables storage caching #[must_use] pub fn no_storage_caching(self) -> Self { self.with_storage_caching(true) @@ -921,11 +906,8 @@ impl NodeConfig { ) .expect("Failed writing json"); } - if self.silent { - return; - } - println!("{}", self.as_string(fork)) + let _ = sh_println!("{}", self.as_string(fork)); } /// Returns the path where the cache file should be stored diff --git a/crates/anvil/src/lib.rs b/crates/anvil/src/lib.rs index ee66caa672ba..11cb4473fad7 100644 --- a/crates/anvil/src/lib.rs +++ b/crates/anvil/src/lib.rs @@ -1,9 +1,6 @@ #![doc = include_str!("../README.md")] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -#[macro_use] -extern crate tracing; - use crate::{ eth::{ backend::{info::StorageInfo, mem}, @@ -23,7 +20,10 @@ use crate::{ use alloy_primitives::{Address, U256}; use alloy_signer_local::PrivateKeySigner; use eth::backend::fork::ClientFork; -use foundry_common::provider::{ProviderBuilder, RetryProvider}; +use foundry_common::{ + provider::{ProviderBuilder, RetryProvider}, + shell, +}; use foundry_evm::revm; use futures::{FutureExt, TryFutureExt}; use parking_lot::Mutex; @@ -74,6 +74,12 @@ mod tasks; #[cfg(feature = "cmd")] pub mod cmd; +#[macro_use] +extern crate foundry_common; + +#[macro_use] +extern crate tracing; + /// Creates the node and runs the server. /// /// Returns the [EthApi] that can be used to interact with the node and the [JoinHandle] of the @@ -125,7 +131,7 @@ pub async fn spawn(config: NodeConfig) -> (EthApi, NodeHandle) { /// ``` pub async fn try_spawn(mut config: NodeConfig) -> io::Result<(EthApi, NodeHandle)> { let logger = if config.enable_tracing { init_tracing() } else { Default::default() }; - logger.set_enabled(!config.silent); + logger.set_enabled(!shell::is_quiet()); let backend = Arc::new(config.setup().await); @@ -292,19 +298,17 @@ impl NodeHandle { /// Prints the launch info. pub(crate) fn print(&self, fork: Option<&ClientFork>) { self.config.print(fork); - if !self.config.silent { - if let Some(ipc_path) = self.ipc_path() { - println!("IPC path: {ipc_path}"); - } - println!( - "Listening on {}", - self.addresses - .iter() - .map(|addr| { addr.to_string() }) - .collect::>() - .join(", ") - ); + if let Some(ipc_path) = self.ipc_path() { + let _ = sh_println!("IPC path: {ipc_path}"); } + let _ = sh_println!( + "Listening on {}", + self.addresses + .iter() + .map(|addr| { addr.to_string() }) + .collect::>() + .join(", ") + ); } /// The address of the launched server. diff --git a/crates/anvil/tests/it/fork.rs b/crates/anvil/tests/it/fork.rs index 17e5f9bd4f92..a35d7c267d62 100644 --- a/crates/anvil/tests/it/fork.rs +++ b/crates/anvil/tests/it/fork.rs @@ -57,7 +57,6 @@ pub fn fork_config() -> NodeConfig { NodeConfig::test() .with_eth_rpc_url(Some(rpc::next_http_archive_rpc_endpoint())) .with_fork_block_number(Some(BLOCK_NUMBER)) - .silent() } #[tokio::test(flavor = "multi_thread")] @@ -829,10 +828,9 @@ async fn test_fork_init_base_fee() { #[tokio::test(flavor = "multi_thread")] async fn test_reset_fork_on_new_blocks() { - let (api, handle) = spawn( - NodeConfig::test().with_eth_rpc_url(Some(rpc::next_http_archive_rpc_endpoint())).silent(), - ) - .await; + let (api, handle) = + spawn(NodeConfig::test().with_eth_rpc_url(Some(rpc::next_http_archive_rpc_endpoint()))) + .await; let anvil_provider = handle.http_provider(); let endpoint = next_http_rpc_endpoint(); diff --git a/crates/cast/bin/args.rs b/crates/cast/bin/args.rs index fdb6a113354a..e4c8dcb4cda5 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,9 @@ 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/cmd/run.rs b/crates/cast/bin/cmd/run.rs index 9d04ed1e2c1f..5eb6a490da79 100644 --- a/crates/cast/bin/cmd/run.rs +++ b/crates/cast/bin/cmd/run.rs @@ -45,7 +45,7 @@ pub struct RunArgs { /// Executes the transaction only with the state from the previous block. /// /// May result in different results than the live execution! - #[arg(long, short)] + #[arg(long)] quick: bool, /// Prints the full address of the contract. diff --git a/crates/cast/bin/cmd/send.rs b/crates/cast/bin/cmd/send.rs index cf3582fe03a6..c8c0faf59623 100644 --- a/crates/cast/bin/cmd/send.rs +++ b/crates/cast/bin/cmd/send.rs @@ -12,7 +12,7 @@ use foundry_cli::{ opts::{EthereumOpts, TransactionOpts}, utils, }; -use foundry_common::{cli_warn, ens::NameOrAddress}; +use foundry_common::ens::NameOrAddress; use foundry_config::Config; use std::{path::PathBuf, str::FromStr}; @@ -145,7 +145,7 @@ impl SendTxArgs { // switch chain if current chain id is not the same as the one specified in the // config if config_chain_id != current_chain_id { - cli_warn!("Switching to chain {}", config_chain); + sh_warn!("Switching to chain {}", config_chain)?; provider .raw_request( "wallet_switchEthereumChain".into(), diff --git a/crates/cast/bin/main.rs b/crates/cast/bin/main.rs index 607f4439188a..1631ce4183fd 100644 --- a/crates/cast/bin/main.rs +++ b/crates/cast/bin/main.rs @@ -8,7 +8,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,6 +19,7 @@ use foundry_common::{ import_selectors, parse_signatures, pretty_calldata, ParsedSignatures, SelectorImportData, SelectorType, }, + stdin, }; use foundry_config::Config; use std::time::Instant; @@ -29,16 +30,27 @@ pub mod tx; use args::{Cast as CastArgs, CastSubcommand, ToBaseArgs}; +#[macro_use] +extern crate foundry_common; + #[cfg(all(feature = "jemalloc", unix))] #[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 args = CastArgs::parse(); + args.shell.shell().set(); main_args(args) } diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index 5487dc9f374b..d43a78f1337f 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -25,8 +25,26 @@ Commands: ... Options: - -h, --help Print help - -V, --version Print version + -h, --help + Print help (see a summary with '-h') + + -V, --version + Print version + +Display options: + --color + Log messages coloring + + Possible values: + - auto: Intelligently guess whether to use color output (default) + - always: Force color output + - never: Force disable color output + + -q, --quiet + Do not print log messages + + --verbose + Use verbose output Find more information in the book: http://book.getfoundry.sh/reference/cast/cast.html @@ -152,8 +170,7 @@ Validation succeeded. Address 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf signed .args(["wallet", "verify", "-a", address, "other msg", expected]) .assert_failure() .stderr_eq(str![[r#" -Error: -Validation failed. Address 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf did not sign this message. +Error: Validation failed. Address 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf did not sign this message. "#]]); }); @@ -935,8 +952,7 @@ casttest!(mktx_requires_to, |_prj, cmd| { "1", ]); cmd.assert_failure().stderr_eq(str![[r#" -Error: -Must specify a recipient address or contract code to deploy +Error: Must specify a recipient address or contract code to deploy "#]]); }); @@ -953,8 +969,7 @@ casttest!(mktx_signer_from_mismatch, |_prj, cmd| { "0x0000000000000000000000000000000000000001", ]); cmd.assert_failure().stderr_eq(str![[r#" -Error: -The specified sender via CLI/env vars does not match the sender configured via +Error: The specified sender via CLI/env vars does not match the sender configured via the hardware wallet's HD Path. Please use the `--hd-path ` parameter to specify the BIP32 Path which corresponds to the sender, or let foundry automatically detect it by not specifying any sender address. @@ -1025,8 +1040,7 @@ casttest!(send_requires_to, |_prj, cmd| { "1", ]); cmd.assert_failure().stderr_eq(str![[r#" -Error: -Must specify a recipient address or contract code to deploy +Error: Must specify a recipient address or contract code to deploy "#]]); }); @@ -1279,8 +1293,7 @@ casttest!(ens_resolve_no_dot_eth, |_prj, cmd| { cmd.args(["resolve-name", "emo", "--rpc-url", ð_rpc_url, "--verify"]) .assert_failure() .stderr_eq(str![[r#" -Error: -ENS resolver not found for name "emo" +Error: ENS resolver not found for name "emo" "#]]); }); @@ -1295,8 +1308,7 @@ casttest!(index7201, |_prj, cmd| { casttest!(index7201_unknown_formula_id, |_prj, cmd| { cmd.args(["index-erc7201", "test", "--formula-id", "unknown"]).assert_failure().stderr_eq( str![[r#" -Error: -unsupported formula ID: unknown +Error: unsupported formula ID: unknown "#]], ); diff --git a/crates/chisel/bin/main.rs b/crates/chisel/bin/main.rs index 70425279484a..c612a504706f 100644 --- a/crates/chisel/bin/main.rs +++ b/crates/chisel/bin/main.rs @@ -11,7 +11,7 @@ use clap::{Parser, Subcommand}; use eyre::Context; use foundry_cli::{ handler, - opts::CoreBuildArgs, + opts::{CoreBuildArgs, ShellOpts}, utils::{self, LoadConfig}, }; use foundry_common::{evm::EvmArgs, fs}; @@ -50,6 +50,9 @@ pub struct Chisel { #[command(subcommand)] pub cmd: Option, + #[clap(flatten)] + pub shell: ShellOpts, + /// Path to a directory containing Solidity files to import, or path to a single Solidity file. /// /// These files will be evaluated before the top-level of the @@ -100,11 +103,19 @@ pub enum ChiselSubcommand { }, } -fn main() -> eyre::Result<()> { +fn main() { + if let Err(err) = run() { + let _ = foundry_common::Shell::get().error(&err); + std::process::exit(1); + } +} + +fn run() -> eyre::Result<()> { handler::install(); utils::subscriber(); utils::load_dotenv(); let args = Chisel::parse(); + args.shell.shell().set(); main_args(args) } diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 6f5e2f607693..9c1fb848edaa 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -5,10 +5,12 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#[macro_use] +extern crate foundry_common; + #[macro_use] 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 59da53372c77..58d0ace85ea2 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/mod.rs b/crates/cli/src/opts/mod.rs index 7825cba3c8b3..95bf9e126cc6 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 000000000000..9213c670224c --- /dev/null +++ b/crates/cli/src/opts/shell.rs @@ -0,0 +1,39 @@ +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", help_heading = "Display options")] + pub verbose: bool, + + /// Do not print log messages. + #[clap( + short, + long, + global = true, + alias = "silent", + conflicts_with = "verbose", + help_heading = "Display options" + )] + pub quiet: bool, + + /// Log messages coloring. + #[clap(long, global = true, value_enum, help_heading = "Display options")] + 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/cmd.rs b/crates/cli/src/utils/cmd.rs index 4e289ba90e59..eab1e0b318c0 100644 --- a/crates/cli/src/utils/cmd.rs +++ b/crates/cli/src/utils/cmd.rs @@ -1,7 +1,7 @@ use alloy_json_abi::JsonAbi; use alloy_primitives::Address; use eyre::{Result, WrapErr}; -use foundry_common::{cli_warn, fs, TestFunctionExt}; +use foundry_common::{fs, TestFunctionExt}; use foundry_compilers::{ artifacts::{CompactBytecode, CompactDeployedBytecode, Settings}, cache::{CacheEntry, CompilerCache}, @@ -275,35 +275,41 @@ where fn load_config_emit_warnings(self) -> Config { let config = self.load_config(); - config.warnings.iter().for_each(|w| cli_warn!("{w}")); + config.warnings.iter().for_each(|w| sh_warn!("{w}").unwrap()); config } fn try_load_config_emit_warnings(self) -> Result { let config = self.try_load_config()?; - config.warnings.iter().for_each(|w| cli_warn!("{w}")); + emit_warnings(&config); Ok(config) } fn load_config_and_evm_opts_emit_warnings(self) -> Result<(Config, EvmOpts)> { let (config, evm_opts) = self.load_config_and_evm_opts()?; - config.warnings.iter().for_each(|w| cli_warn!("{w}")); + emit_warnings(&config); Ok((config, evm_opts)) } fn load_config_unsanitized_emit_warnings(self) -> Config { let config = self.load_config_unsanitized(); - config.warnings.iter().for_each(|w| cli_warn!("{w}")); + emit_warnings(&config); config } fn try_load_config_unsanitized_emit_warnings(self) -> Result { let config = self.try_load_config_unsanitized()?; - config.warnings.iter().for_each(|w| cli_warn!("{w}")); + emit_warnings(&config); Ok(config) } } +fn emit_warnings(config: &Config) { + for warning in &config.warnings { + let _ = sh_warn!("{warning}"); + } +} + /// Read contract constructor arguments from the given file. pub fn read_constructor_args_file(constructor_args_path: PathBuf) -> Result> { if !constructor_args_path.exists() { diff --git a/crates/cli/src/utils/mod.rs b/crates/cli/src/utils/mod.rs index 5b7523447ec9..8aa56aab8c41 100644 --- a/crates/cli/src/utils/mod.rs +++ b/crates/cli/src/utils/mod.rs @@ -3,7 +3,10 @@ use alloy_primitives::U256; use alloy_provider::{network::AnyNetwork, Provider}; use alloy_transport::Transport; use eyre::{ContextCompat, Result}; -use foundry_common::provider::{ProviderBuilder, RetryProvider}; +use foundry_common::{ + provider::{ProviderBuilder, RetryProvider}, + shell, +}; use foundry_config::{Chain, Config}; use serde::de::DeserializeOwned; use std::{ @@ -167,23 +170,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 @@ -288,7 +274,7 @@ pub struct Git<'a> { impl<'a> Git<'a> { #[inline] pub fn new(root: &'a Path) -> Self { - Self { root, quiet: false, shallow: false } + Self { root, quiet: shell::is_quiet(), shallow: false } } #[inline] diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index fbc5c82ca771..af95e94bcc35 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -62,6 +62,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/fmt/Cargo.toml b/crates/common/fmt/Cargo.toml index 9902de608f41..2a56b3b10e01 100644 --- a/crates/common/fmt/Cargo.toml +++ b/crates/common/fmt/Cargo.toml @@ -16,7 +16,6 @@ workspace = true [dependencies] alloy-primitives.workspace = true alloy-dyn-abi = { workspace = true, features = ["eip712"] } -yansi.workspace = true # ui alloy-consensus.workspace = true @@ -28,6 +27,7 @@ serde_json.workspace = true chrono.workspace = true revm-primitives.workspace = true comfy-table.workspace = true +yansi.workspace = true [dev-dependencies] foundry-macros.workspace = true diff --git a/crates/common/src/compile.rs b/crates/common/src/compile.rs index 39998b3a61c1..d504e1688e64 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -66,7 +66,7 @@ impl ProjectCompiler { verify: None, print_names: None, print_sizes: None, - quiet: Some(crate::shell::verbosity().is_silent()), + quiet: Some(crate::shell::is_quiet()), bail: None, ignore_eip_3860: false, files: Vec::new(), @@ -102,15 +102,6 @@ impl ProjectCompiler { self } - /// Do not print anything at all if true. Overrides other `print` options. - #[inline] - pub fn quiet_if(mut self, maybe: bool) -> Self { - if maybe { - self.quiet = Some(true); - } - self - } - /// Sets whether to bail on compiler errors. #[inline] pub fn bail(mut self, yes: bool) -> Self { diff --git a/crates/common/src/io/macros.rs b/crates/common/src/io/macros.rs new file mode 100644 index 000000000000..fe1e72dfecc9 --- /dev/null +++ b/crates/common/src/io/macros.rs @@ -0,0 +1,185 @@ +/// Prints a message to [`stdout`][std::io::stdout] and reads a line from stdin into a String. +/// +/// 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. +/// +/// **Note**: will log regardless of the verbosity level. +#[macro_export] +macro_rules! sh_err { + ($($args:tt)*) => { + $crate::__sh_dispatch!(error $($args)*) + }; +} + +/// Prints a formatted warning to stderr. +/// +/// **Note**: if `verbosity` is set to `Quiet`, this is a no-op. +#[macro_export] +macro_rules! sh_warn { + ($($args:tt)*) => { + $crate::__sh_dispatch!(warn $($args)*) + }; +} + +/// Prints a raw formatted message to stdout. +/// +/// **Note**: if `verbosity` is set to `Quiet`, this is a no-op. +#[macro_export] +macro_rules! sh_print { + ($($args:tt)*) => { + $crate::__sh_dispatch!(print_out $($args)*) + }; + + ($shell:expr, $($args:tt)*) => { + $crate::__sh_dispatch!(print_out $shell, $($args)*) + }; +} + +/// Prints a raw formatted message to stderr. +/// +/// **Note**: if `verbosity` is set to `Quiet`, this is a no-op. +#[macro_export] +macro_rules! sh_eprint { + ($($args:tt)*) => { + $crate::__sh_dispatch!(print_err $($args)*) + }; + + ($shell:expr, $($args:tt)*) => { + $crate::__sh_dispatch!(print_err $shell, $($args)*) + }; +} + +/// Prints a raw formatted message to stdout, with a trailing newline. +/// +/// **Note**: if `verbosity` is set to `Quiet`, this is a no-op. +#[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").expect("failed to write newline") + }; + + ($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**: if `verbosity` is set to `Quiet`, this is a no-op. +#[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)*)) + }; +} + +#[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() -> eyre::Result<()> { + sh_err!("err")?; + sh_err!("err {}", "arg")?; + + sh_warn!("warn")?; + sh_warn!("warn {}", "arg")?; + + sh_print!("print -")?; + sh_print!("print {} -", "arg")?; + + sh_println!()?; + sh_println!("println")?; + sh_println!("println {}", "arg")?; + + sh_eprint!("eprint -")?; + sh_eprint!("eprint {} -", "arg")?; + + sh_eprintln!()?; + sh_eprintln!("eprintln")?; + sh_eprintln!("eprintln {}", "arg")?; + + Ok(()) + } + + #[test] + fn macros_with_shell() -> eyre::Result<()> { + let shell = &mut crate::Shell::new(); + sh_eprintln!(shell)?; + sh_eprintln!(shell,)?; + sh_eprintln!(shell, "shelled eprintln")?; + sh_eprintln!(shell, "shelled eprintln {}", "arg")?; + sh_eprintln!(&mut crate::Shell::new(), "shelled eprintln {}", "arg")?; + + Ok(()) + } +} diff --git a/crates/common/src/io/mod.rs b/crates/common/src/io/mod.rs new file mode 100644 index 000000000000..f62fd034617b --- /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 000000000000..b60061eea4ac --- /dev/null +++ b/crates/common/src/io/shell.rs @@ -0,0 +1,518 @@ +//! 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() +} + +/// Returns whether the verbosity level is [`Verbosity::Quiet`]. +pub fn is_quiet() -> bool { + verbosity().is_quiet() +} + +/// 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_of(unsafe { + std::os::fd::BorrowedFd::borrow_raw(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 { + Mutex::new(Shell::new()) + } + + 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"); + } + } + + /// 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. + /// This will render a message in [ERROR] style with a bold `Error: ` prefix. + /// + /// **Note**: will log regardless of the verbosity level. + #[inline] + pub fn error(&mut self, message: impl fmt::Display) -> Result<()> { + self.maybe_err_erase_line(); + self.output.message_stderr(&"Error", &ERROR, Some(&message), false) + } + + /// Prints an amber 'warning' message. Use the [`sh_warn!`] macro instead. + /// This will render a message in [WARN] style with a bold `Warning: `prefix. + /// + /// **Note**: if `verbosity` is set to `Quiet`, this is a no-op. + #[inline] + pub fn warn(&mut self, message: impl fmt::Display) -> Result<()> { + match self.verbosity { + Verbosity::Quiet => Ok(()), + _ => self.print(&"Warning", &WARN, Some(&message), 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**: if `verbosity` is set to `Quiet`, this is a no-op. + #[inline] + pub fn print_out(&mut self, fragment: impl fmt::Display) -> Result<()> { + if self.verbosity == Verbosity::Quiet { + Ok(()) + } else { + 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, where the status will have `color` color, and can be justified. The + /// messages follows without color. + fn print( + &mut self, + status: &dyn fmt::Display, + style: &Style, + message: Option<&dyn fmt::Display>, + justified: bool, + ) -> Result<()> { + match self.verbosity { + Verbosity::Quiet => Ok(()), + _ => { + self.maybe_err_erase_line(); + self.output.message_stderr(status, style, message, justified) + } + } + } +} + +impl ShellOut { + /// Prints out a message with a status to stderr. 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, + style: &Style, + message: Option<&dyn fmt::Display>, + justified: bool, + ) -> Result<()> { + let buffer = Self::format_message(status, message, style, justified)?; + 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, + } + } + + /// Formats a message with a status and optional message. + fn format_message( + 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!(&mut buffer, " {message}")?; + } + None => write!(buffer, " ")?, + } + + Ok(buffer) + } +} + +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, + } +} 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 8242cc805772..17b40a2cff1f 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 000000000000..6103b2d37d10 --- /dev/null +++ b/crates/common/src/io/style.rs @@ -0,0 +1,5 @@ +#![allow(missing_docs)] +use anstyle::*; + +pub const ERROR: Style = AnsiColor::Red.on_default().effects(Effects::BOLD); +pub const WARN: Style = AnsiColor::Yellow.on_default().effects(Effects::BOLD); diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index a33a7b223156..68559d081b0a 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; @@ -34,6 +36,7 @@ mod utils; pub use constants::*; pub use contracts::*; +pub use io::{shell, stdin, Shell}; pub use traits::*; pub use transactions::*; pub use utils::*; diff --git a/crates/common/src/shell.rs b/crates/common/src/shell.rs deleted file mode 100644 index 8ab98e64a9c7..000000000000 --- 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/common/src/term.rs b/crates/common/src/term.rs index 30aaf6c52f82..4753b1e416f8 100644 --- a/crates/common/src/term.rs +++ b/crates/common/src/term.rs @@ -198,21 +198,6 @@ pub fn with_spinner_reporter(f: impl FnOnce() -> T) -> T { report::with_scoped(&reporter, f) } -#[macro_export] -/// Displays warnings on the cli -macro_rules! cli_warn { - ($($arg:tt)*) => { - eprintln!( - "{}{} {}", - yansi::Painted::new("warning").yellow().bold(), - yansi::Painted::new(":").bold(), - format_args!($($arg)*) - ) - } -} - -pub use cli_warn; - #[cfg(test)] mod tests { use super::*; diff --git a/crates/forge/bin/cmd/build.rs b/crates/forge/bin/cmd/build.rs index f0fa7c9006e4..f6d348334232 100644 --- a/crates/forge/bin/cmd/build.rs +++ b/crates/forge/bin/cmd/build.rs @@ -76,7 +76,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")] + #[arg(long, conflicts_with = "quiet")] #[serde(skip)] pub format_json: bool, } @@ -85,9 +85,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(); } @@ -115,7 +113,7 @@ impl BuildArgs { let output = compiler.compile(&project)?; if self.format_json { - println!("{}", serde_json::to_string_pretty(&output.output())?); + sh_println!("{}", serde_json::to_string_pretty(&output.output())?)?; } Ok(output) @@ -174,33 +172,3 @@ impl Provider for BuildArgs { Ok(Map::from([(Config::selected_profile(), dict)])) } } - -#[cfg(test)] -mod tests { - use super::*; - use foundry_config::filter::SkipBuildFilter; - - #[test] - fn can_parse_build_filters() { - let args: BuildArgs = BuildArgs::parse_from(["foundry-cli", "--skip", "tests"]); - assert_eq!(args.args.skip, Some(vec![SkipBuildFilter::Tests])); - - let args: BuildArgs = BuildArgs::parse_from(["foundry-cli", "--skip", "scripts"]); - assert_eq!(args.args.skip, Some(vec![SkipBuildFilter::Scripts])); - - let args: BuildArgs = - BuildArgs::parse_from(["foundry-cli", "--skip", "tests", "--skip", "scripts"]); - assert_eq!(args.args.skip, Some(vec![SkipBuildFilter::Tests, SkipBuildFilter::Scripts])); - - let args: BuildArgs = BuildArgs::parse_from(["foundry-cli", "--skip", "tests", "scripts"]); - assert_eq!(args.args.skip, Some(vec![SkipBuildFilter::Tests, SkipBuildFilter::Scripts])); - } - - #[test] - fn check_conflicts() { - let args: std::result::Result = - BuildArgs::try_parse_from(["foundry-cli", "--format-json", "--silent"]); - assert!(args.is_err()); - assert!(args.unwrap_err().kind() == clap::error::ErrorKind::ArgumentConflict); - } -} diff --git a/crates/forge/bin/cmd/clone.rs b/crates/forge/bin/cmd/clone.rs index 1c9ee47ec9fe..f1bb40cf6918 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,8 @@ 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_println!("Downloading the source code of {address} from Etherscan...")?; + let meta = Self::collect_metadata_from_client(address, &client).await?; // step 2. initialize an empty project @@ -117,17 +118,17 @@ 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_println!("Collecting the creation information of {address} from Etherscan...")?; + if etherscan_api_key.is_empty() { - p_println!(!opts.quiet => "Waiting for 5 seconds to avoid rate limit..."); + sh_warn!("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 +186,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 +546,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) } @@ -618,7 +618,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( @@ -720,7 +720,6 @@ mod tests { address, &project_root, &client, - false, ) .await .unwrap(); diff --git a/crates/forge/bin/cmd/config.rs b/crates/forge/bin/cmd/config.rs index fc325e39d99c..652c5a10fed3 100644 --- a/crates/forge/bin/cmd/config.rs +++ b/crates/forge/bin/cmd/config.rs @@ -2,7 +2,7 @@ use super::build::BuildArgs; use clap::Parser; use eyre::Result; use foundry_cli::utils::LoadConfig; -use foundry_common::{evm::EvmArgs, term::cli_warn}; +use foundry_common::evm::EvmArgs; use foundry_config::fix::fix_tomls; foundry_config::impl_figment_convert!(ConfigArgs, opts, evm_opts); @@ -34,7 +34,7 @@ impl ConfigArgs { pub fn run(self) -> Result<()> { if self.fix { for warning in fix_tomls() { - cli_warn!("{warning}"); + sh_warn!("{warning}")?; } return Ok(()) } @@ -57,7 +57,7 @@ impl ConfigArgs { config.to_string_pretty()? }; - println!("{s}"); + sh_println!("{s}")?; Ok(()) } } diff --git a/crates/forge/bin/cmd/coverage.rs b/crates/forge/bin/cmd/coverage.rs index e21153d09c19..56b2024e391b 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}, @@ -29,7 +26,6 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use yansi::Paint; // Loads project's figment and merges the build cli arguments into it foundry_config::impl_figment_convert!(CoverageArgs, test); @@ -74,9 +70,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(); } @@ -88,10 +82,10 @@ impl CoverageArgs { config.ast = true; let (project, output) = self.build(&config)?; - p_println!(!self.test.build_args().silent => "Analysing contracts..."); + sh_println!("Analysing contracts...")?; let report = self.prepare(&project, &output)?; - p_println!(!self.test.build_args().silent => "Running tests..."); + sh_println!("Running tests...")?; self.collect(project, &output, report, Arc::new(config), evm_opts).await } @@ -112,14 +106,13 @@ impl CoverageArgs { } // print warning message - let msg = concat!( + sh_warn!("{}", concat!( "Warning! \"--ir-minimum\" flag enables viaIR with minimum optimization, \ which can result in inaccurate source mappings.\n", "Only use this flag as a workaround if you are experiencing \"stack too deep\" errors.\n", "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}"); + ))?; // Enable viaIR with minimum optimization // https://github.com/ethereum/solidity/issues/12533#issuecomment-1013073350 @@ -254,7 +247,7 @@ impl CoverageArgs { let outcome = self.test.run_tests(runner, config.clone(), verbosity, &filter, output).await?; - outcome.ensure_ok()?; + outcome.ensure_ok(false)?; // Add hit data to the coverage report let data = outcome.results.iter().flat_map(|(_, suite)| { diff --git a/crates/forge/bin/cmd/create.rs b/crates/forge/bin/cmd/create.rs index df42f458c015..1962f41f6d0c 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/fmt.rs b/crates/forge/bin/cmd/fmt.rs index 9fd016ac70b0..ef1cf0461697 100644 --- a/crates/forge/bin/cmd/fmt.rs +++ b/crates/forge/bin/cmd/fmt.rs @@ -2,7 +2,7 @@ use clap::{Parser, ValueHint}; use eyre::{Context, Result}; use forge_fmt::{format_to, parse}; use foundry_cli::utils::{FoundryPathExt, LoadConfig}; -use foundry_common::{fs, term::cli_warn}; +use foundry_common::fs; use foundry_compilers::{compilers::solc::SolcLanguage, solc::SOLC_EXTENSIONS}; use foundry_config::{filter::expand_globs, impl_figment_convert_basic}; use rayon::prelude::*; @@ -111,7 +111,7 @@ impl FmtArgs { let mut lines = source[..loc.start().min(source.len())].split('\n'); let col = lines.next_back().unwrap().len() + 1; let row = lines.count() + 1; - cli_warn!("[{}:{}:{}] {}", name, row, col, warning); + sh_warn!("[{}:{}:{}] {}", name, row, col, warning)?; } } @@ -149,11 +149,11 @@ impl FmtArgs { Input::Stdin(source) => format(source, None).map(|diff| vec![diff]), Input::Paths(paths) => { if paths.is_empty() { - cli_warn!( + sh_warn!( "Nothing to format.\n\ HINT: If you are working outside of the project, \ try providing paths to your source files: `forge fmt `" - ); + )?; return Ok(()) } paths diff --git a/crates/forge/bin/cmd/init.rs b/crates/forge/bin/cmd/init.rs index 1882eca60c64..82f296e417e9 100644 --- a/crates/forge/bin/cmd/init.rs +++ b/crates/forge/bin/cmd/init.rs @@ -1,7 +1,7 @@ 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; @@ -44,14 +44,14 @@ 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 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 @@ -62,7 +62,7 @@ impl InitArgs { } else { "https://github.com/".to_string() + &template }; - p_println!(!quiet => "Initializing {} from {}...", root.display(), template); + sh_println!("Initializing {} from {}...", root.display(), template)?; // initialize the git repository git.init()?; @@ -95,8 +95,7 @@ impl InitArgs { Run with the `--force` flag to initialize regardless." ); } - - p_println!(!quiet => "Target directory is not empty, but `--force` was specified"); + sh_warn!("Target directory is not empty, but `--force` was specified")?; } // ensure git status is clean before generating anything @@ -104,7 +103,7 @@ impl InitArgs { git.ensure_clean()?; } - p_println!(!quiet => "Initializing {}...", root.display()); + sh_println!("Initializing {}...", root.display())?; // make the dirs let src = root.join("src"); @@ -145,7 +144,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_warn!("\"lib/forge-std\" already exists, skipping install...")?; self.opts.install(&mut config, vec![])?; } else { let dep = "https://github.com/foundry-rs/forge-std".parse()?; @@ -159,7 +158,7 @@ impl InitArgs { } } - p_println!(!quiet => " {} forge project", "Initialized".green()); + sh_println!("{}", " Initialized forge project".green())?; Ok(()) } } diff --git a/crates/forge/bin/cmd/install.rs b/crates/forge/bin/cmd/install.rs index 448d5b1ad7b7..2567825d45a1 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,17 +89,16 @@ 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_println!("Missing dependencies found. Installing now...\n"); self.no_commit = true; - if self.install(config, Vec::new()).is_err() && !quiet { - eprintln!( + if self.install(config, Vec::new()).is_err() { + let _ = sh_warn!( "{}", - "Your project has missing dependencies that could not be installed.".yellow() - ) + "Your project has missing dependencies that could not be installed." + ); } true } else { @@ -114,7 +108,7 @@ impl DependencyInstallOpts { /// 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 +120,8 @@ 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_println!("Updating dependencies in {}", libs.display())?; + // recursively fetch all submodules (without fetching latest) git.submodule_update(false, false, false, true, Some(&libs))?; } @@ -148,7 +143,13 @@ 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_println!( + "Installing {} in {} (url: {:?}, tag: {:?})", + dep.name, + path.display(), + dep.url, + dep.tag + )?; // this tracks the actual installed tag let installed_tag; @@ -190,14 +191,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()); } + sh_println!("{msg}")?; } // update `libs` in config if not included yet @@ -209,8 +208,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/snapshot.rs b/crates/forge/bin/cmd/snapshot.rs index 0d7c2843a83f..234ff48a5807 100644 --- a/crates/forge/bin/cmd/snapshot.rs +++ b/crates/forge/bin/cmd/snapshot.rs @@ -96,7 +96,7 @@ impl GasSnapshotArgs { self.test.fuzz_seed = Some(U256::from_be_bytes(STATIC_FUZZ_SEED)); let outcome = self.test.execute_tests().await?; - outcome.ensure_ok()?; + outcome.ensure_ok(false)?; let tests = self.config.apply(outcome); if let Some(path) = self.diff { diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index 39f2b45557e3..962c0104280a 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -20,7 +20,7 @@ use foundry_cli::{ opts::CoreBuildArgs, utils::{self, LoadConfig}, }; -use foundry_common::{cli_warn, compile::ProjectCompiler, evm::EvmArgs, fs, shell}; +use foundry_common::{compile::ProjectCompiler, evm::EvmArgs, fs}; use foundry_compilers::{ artifacts::output_selection::OutputSelection, compilers::{multi::MultiCompilerLanguage, CompilerSettings, Language}, @@ -118,11 +118,11 @@ pub struct TestArgs { /// Output test results in JSON format. #[arg(long, help_heading = "Display options")] - json: bool, + pub json: bool, /// Output test results as JUnit XML report. #[arg(long, conflicts_with_all(["json", "gas_report"]), help_heading = "Display options")] - junit: bool, + pub junit: bool, /// Stop running tests after the first failure. #[arg(long)] @@ -190,7 +190,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 } @@ -295,9 +294,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()?; @@ -308,9 +305,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(self.json || self.junit).files(sources_to_compile); let output = compiler.compile(&project)?; @@ -377,10 +373,10 @@ impl TestArgs { let mut maybe_override_mt = |flag, maybe_regex: Option<&Option>| { if let Some(Some(regex)) = maybe_regex { - cli_warn!( + sh_warn!( "specifying argument for --{flag} is deprecated and will be removed in the future, \ use --match-test instead" - ); + )?; let test_pattern = &mut filter.args_mut().test_pattern; if test_pattern.is_some() { @@ -623,7 +619,7 @@ impl TestArgs { // Process individual test results, printing logs and traces when necessary. for (name, result) in tests { if !silent { - shell::println(result.short_result(name))?; + sh_println!("{}", result.short_result(name))?; // We only display logs at level 2 and above if verbosity >= 2 { @@ -678,9 +674,9 @@ impl TestArgs { } if !silent && !decoded_traces.is_empty() { - shell::println("Traces:")?; + sh_println!("Traces:")?; for trace in &decoded_traces { - shell::println(trace)?; + sh_println!("{trace}")?; } } @@ -785,7 +781,7 @@ impl TestArgs { // Print suite summary. if !silent { - shell::println(suite_result.summary())?; + sh_println!("{}", suite_result.summary())?; } // Add the suite result to the outcome. @@ -803,16 +799,16 @@ impl TestArgs { if let Some(gas_report) = gas_report { let finalized = gas_report.finalize(); - shell::println(&finalized)?; + sh_println!("{}", &finalized)?; outcome.gas_report = Some(finalized); } if !silent && !outcome.results.is_empty() { - shell::println(outcome.summary(duration))?; + sh_println!("{}", outcome.summary(duration))?; if self.summary { let mut summary_table = TestSummaryReporter::new(self.detailed); - shell::println("\n\nTest Summary:")?; + sh_println!("\n\nTest Summary:")?; summary_table.print_summary(&outcome); } } @@ -1079,7 +1075,6 @@ contract FooBarTest is DSTest { "--gas-report", "--root", &prj.root().to_string_lossy(), - "--silent", ]); let outcome = args.run().await.unwrap(); diff --git a/crates/forge/bin/main.rs b/crates/forge/bin/main.rs index 925788ef8fda..a78218e94e0a 100644 --- a/crates/forge/bin/main.rs +++ b/crates/forge/bin/main.rs @@ -1,6 +1,3 @@ -#[macro_use] -extern crate tracing; - use clap::{CommandFactory, Parser}; use clap_complete::generate; use eyre::Result; @@ -13,36 +10,44 @@ use cmd::{cache::CacheSubcommands, generate::GenerateSubcommands, watch}; mod opts; use opts::{Forge, ForgeSubcommand}; +#[macro_use] +extern crate foundry_common; + +#[macro_use] +extern crate tracing; + #[cfg(all(feature = "jemalloc", unix))] #[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(); - init_execution_context(&opts.cmd); + let args = Forge::parse(); + args.shell.shell().set(); + init_execution_context(&args.cmd); - match opts.cmd { + match args.cmd { ForgeSubcommand::Test(cmd) => { if cmd.is_watch() { utils::block_on(watch::watch_test(cmd)) } else { + let silent = cmd.junit || cmd.json; let outcome = utils::block_on(cmd.run())?; - outcome.ensure_ok() + outcome.ensure_ok(silent) } } - 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 d0dfecd3a132..39bc89e63b8f 100644 --- a/crates/forge/bin/opts.rs +++ b/crates/forge/bin/opts.rs @@ -8,6 +8,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!( @@ -30,6 +31,9 @@ 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/lib.rs b/crates/forge/src/lib.rs index 6c6552d05347..0bec55153099 100644 --- a/crates/forge/src/lib.rs +++ b/crates/forge/src/lib.rs @@ -1,6 +1,9 @@ #![doc = include_str!("../README.md")] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#[macro_use] +extern crate foundry_common; + #[macro_use] extern crate tracing; diff --git a/crates/forge/src/result.rs b/crates/forge/src/result.rs index 4fb88dfd0b59..5f83168bb50a 100644 --- a/crates/forge/src/result.rs +++ b/crates/forge/src/result.rs @@ -151,20 +151,20 @@ impl TestOutcome { } /// Checks if there are any failures and failures are disallowed. - pub fn ensure_ok(&self) -> eyre::Result<()> { + pub fn ensure_ok(&self, silent: bool) -> eyre::Result<()> { let outcome = self; let failures = outcome.failures().count(); if outcome.allow_failure || failures == 0 { return Ok(()); } - if !shell::verbosity().is_normal() { + if shell::is_quiet() || silent { // TODO: Avoid process::exit std::process::exit(1); } - shell::println("")?; - shell::println("Failing tests:")?; + sh_println!()?; + sh_println!("Failing tests:")?; for (suite_name, suite) in outcome.results.iter() { let failed = suite.failed(); if failed == 0 { @@ -172,18 +172,18 @@ impl TestOutcome { } let term = if failed > 1 { "tests" } else { "test" }; - shell::println(format!("Encountered {failed} failing {term} in {suite_name}"))?; + sh_println!("Encountered {failed} failing {term} in {suite_name}")?; for (name, result) in suite.failures() { - shell::println(result.short_result(name))?; + sh_println!("{}", result.short_result(name))?; } - shell::println("")?; + sh_println!()?; } let successes = outcome.passed(); - shell::println(format!( + sh_println!( "Encountered a total of {} failing tests, {} tests succeeded", failures.to_string().red(), successes.to_string().green() - ))?; + )?; // TODO: Avoid process::exit std::process::exit(1); diff --git a/crates/forge/tests/cli/build.rs b/crates/forge/tests/cli/build.rs index 81919241fa00..812754c72552 100644 --- a/crates/forge/tests/cli/build.rs +++ b/crates/forge/tests/cli/build.rs @@ -3,7 +3,35 @@ use foundry_config::Config; use foundry_test_utils::{forgetest, snapbox::IntoData, str}; use globset::Glob; -// tests that json is printed when --json is passed +forgetest_init!(can_parse_build_filters, |prj, cmd| { + prj.clear(); + + cmd.args(["build", "--names", "--skip", "tests", "scripts"]).assert_success().stdout_eq(str![ + [r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + compiler version: [..] + - Counter + +"#] + ]); +}); + +forgetest!(throws_on_conflicting_args, |prj, cmd| { + prj.clear(); + + cmd.args(["compile", "--format-json", "--quiet"]).assert_failure().stderr_eq(str![[r#" +error: the argument '--format-json' cannot be used with '--quiet' + +Usage: forge[..] build --format-json [PATHS]... + +For more information, try '--help'. + +"#]]); +}); + +// tests that json is printed when --format-json is passed forgetest!(compile_json, |prj, cmd| { prj.add_source( "jsonError", diff --git a/crates/forge/tests/cli/cmd.rs b/crates/forge/tests/cli/cmd.rs index 78c18bbaf65d..6177e973dce6 100644 --- a/crates/forge/tests/cli/cmd.rs +++ b/crates/forge/tests/cli/cmd.rs @@ -30,8 +30,26 @@ Commands: ... Options: - -h, --help Print help - -V, --version Print version + -h, --help + Print help (see a summary with '-h') + + -V, --version + Print version + +Display options: + --color + Log messages coloring + + Possible values: + - auto: Intelligently guess whether to use color output (default) + - always: Force color output + - never: Force disable color output + + -q, --quiet + Do not print log messages + + --verbose + Use verbose output Find more information in the book: http://book.getfoundry.sh/reference/forge/forge.html @@ -225,13 +243,20 @@ forgetest!(can_init_repo_with_config, |prj, cmd| { let foundry_toml = prj.root().join(Config::FILE_NAME); assert!(!foundry_toml.exists()); - cmd.args(["init", "--force"]).arg(prj.root()).assert_success().stdout_eq(str![[r#" -Target directory is not empty, but `--force` was specified + cmd.args(["init", "--force"]) + .arg(prj.root()) + .assert_success() + .stdout_eq(str![[r#" Initializing [..]... Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) Installed forge-std[..] Initialized forge project +"#]]) + .stderr_eq(str![[r#" +Warning: Target directory is not empty, but `--force` was specified +... + "#]]); let s = read_string(&foundry_toml); @@ -253,8 +278,7 @@ forgetest!(can_detect_dirty_git_status_on_init, |prj, cmd| { cmd.current_dir(&nested); cmd.arg("init").assert_failure().stderr_eq(str![[r#" -Error: -The target directory is a part of or on its own an already initialized git repository, +Error: The target directory is a part of or on its own an already initialized git repository, and it requires clean working and staging areas, including no untracked files. Check the current git repository's status with `git status`. @@ -349,19 +373,24 @@ Initializing [..] from https://github.com/foundry-rs/forge-template... forgetest!(can_init_non_empty, |prj, cmd| { prj.create_file("README.md", "non-empty dir"); cmd.arg("init").arg(prj.root()).assert_failure().stderr_eq(str![[r#" -Error: -Cannot run `init` on a non-empty directory. +Error: Cannot run `init` on a non-empty directory. Run with the `--force` flag to initialize regardless. "#]]); - cmd.arg("--force").assert_success().stdout_eq(str![[r#" -Target directory is not empty, but `--force` was specified + cmd.arg("--force") + .assert_success() + .stdout_eq(str![[r#" Initializing [..]... Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) Installed forge-std[..] Initialized forge project +"#]]) + .stderr_eq(str![[r#" +Warning: Target directory is not empty, but `--force` was specified +... + "#]]); assert!(prj.root().join(".git").exists()); @@ -384,20 +413,26 @@ forgetest!(can_init_in_empty_repo, |prj, cmd| { assert!(root.join(".git").exists()); cmd.arg("init").arg(root).assert_failure().stderr_eq(str![[r#" -Error: -Cannot run `init` on a non-empty directory. +Error: Cannot run `init` on a non-empty directory. Run with the `--force` flag to initialize regardless. "#]]); - cmd.arg("--force").assert_success().stdout_eq(str![[r#" -Target directory is not empty, but `--force` was specified + cmd.arg("--force") + .assert_success() + .stdout_eq(str![[r#" Initializing [..]... Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) Installed forge-std[..] Initialized forge project +"#]]) + .stderr_eq(str![[r#" +Warning: Target directory is not empty, but `--force` was specified +... + "#]]); + assert!(root.join("lib/forge-std").exists()); }); @@ -420,20 +455,26 @@ forgetest!(can_init_in_non_empty_repo, |prj, cmd| { prj.create_file(".gitignore", "not foundry .gitignore"); cmd.arg("init").arg(root).assert_failure().stderr_eq(str![[r#" -Error: -Cannot run `init` on a non-empty directory. +Error: Cannot run `init` on a non-empty directory. Run with the `--force` flag to initialize regardless. "#]]); - cmd.arg("--force").assert_success().stdout_eq(str![[r#" -Target directory is not empty, but `--force` was specified + cmd.arg("--force") + .assert_success() + .stdout_eq(str![[r#" Initializing [..]... Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) Installed forge-std[..] Initialized forge project +"#]]) + .stderr_eq(str![[r#" +Warning: Target directory is not empty, but `--force` was specified +... + "#]]); + assert!(root.join("lib/forge-std").exists()); // not overwritten @@ -520,8 +561,7 @@ forgetest!(fail_init_nonexistent_template, |prj, cmd| { cmd.args(["init", "--template", "a"]).arg(prj.root()).assert_failure().stderr_eq(str![[r#" remote: Not Found fatal: repository 'https://github.com/a/' not found -Error: -git fetch exited with code 128 +Error: git fetch exited with code 128 "#]]); }); @@ -1072,8 +1112,7 @@ Warning: SPDX license identifier not provided in source file. Before publishing, prj.write_config(config); cmd.forge_fuse().args(["build", "--force"]).assert_failure().stderr_eq(str![[r#" -Error: -Compiler run failed: +Error: Compiler run failed: Warning (1878): SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: " to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information. Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: " to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information. [FILE] @@ -1151,8 +1190,7 @@ contract CTest is DSTest { // `forge build --force` which should fail cmd.forge_fuse().args(["build", "--force"]).assert_failure().stderr_eq(str![[r#" -Error: -Compiler run failed: +Error: Compiler run failed: Error (2314): Expected ';' but got identifier [FILE]:7:19: | @@ -1168,8 +1206,7 @@ Error (2314): Expected ';' but got identifier // still errors cmd.forge_fuse().args(["build", "--force"]).assert_failure().stderr_eq(str![[r#" -Error: -Compiler run failed: +Error: Compiler run failed: Error (2314): Expected ';' but got identifier [FILE]:7:19: | @@ -1209,8 +1246,7 @@ Compiler run successful! // introduce the error again but building without force prj.add_source("CTest.t.sol", syntax_err).unwrap(); cmd.forge_fuse().arg("build").assert_failure().stderr_eq(str![[r#" -Error: -Compiler run failed: +Error: Compiler run failed: Error (2314): Expected ';' but got identifier [FILE]:7:19: | diff --git a/crates/forge/tests/cli/config.rs b/crates/forge/tests/cli/config.rs index c061af78f0ef..491171cad165 100644 --- a/crates/forge/tests/cli/config.rs +++ b/crates/forge/tests/cli/config.rs @@ -397,8 +397,7 @@ Compiler run successful! // fails to use solc that does not exist cmd.forge_fuse().args(["build", "--use", "this/solc/does/not/exist"]); cmd.assert_failure().stderr_eq(str![[r#" -Error: -`solc` this/solc/does/not/exist does not exist +Error: `solc` this/solc/does/not/exist does not exist "#]]); @@ -434,8 +433,7 @@ contract Foo { .unwrap(); cmd.arg("build").assert_failure().stderr_eq(str![[r#" -Error: -Compiler run failed: +Error: Compiler run failed: Error (6553): The msize instruction cannot be used when the Yul optimizer is activated because it can change its semantics. Either disable the Yul optimizer or do not use the instruction. [FILE]:6:8: | @@ -652,7 +650,7 @@ Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std fs::write(prj.root().join("lib").join("forge-std").join("foundry.toml"), faulty_toml).unwrap(); cmd.forge_fuse().args(["config"]).assert_success().stderr_eq(str![[r#" -warning: Found unknown config section in foundry.toml: [default] +Warning: Found unknown config section in foundry.toml: [default] This notation for profiles has been deprecated and may result in the profile not being registered in future versions. Please use [profile.default] instead or run `forge config --fix`. diff --git a/crates/forge/tests/cli/debug.rs b/crates/forge/tests/cli/debug.rs index bbed7dc72324..e8cd084187bb 100644 --- a/crates/forge/tests/cli/debug.rs +++ b/crates/forge/tests/cli/debug.rs @@ -7,13 +7,20 @@ forgetest_async!( #[ignore = "ran manually"] manual_debug_setup, |prj, cmd| { - cmd.args(["init", "--force"]).arg(prj.root()).assert_success().stdout_eq(str![[r#" -Target directory is not empty, but `--force` was specified + cmd.args(["init", "--force"]) + .arg(prj.root()) + .assert_success() + .stdout_eq(str![[r#" Initializing [..]... Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) Installed forge-std[..] Initialized forge project +"#]]) + .stderr_eq(str![[r#" +Warning: Target directory is not empty, but `--force` was specified +... + "#]]); prj.add_source("Counter2.sol", r#" diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index 62f606eebab3..c4a69223d521 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -150,8 +150,7 @@ forgetest_async!(assert_exit_code_error_on_failure_script, |prj, cmd| { // run command and assert error exit code cmd.assert_failure().stderr_eq(str![[r#" -Error: -script failed: revert: failed +Error: script failed: revert: failed "#]]); }); @@ -167,8 +166,7 @@ forgetest_async!(assert_exit_code_error_on_failure_script_with_json, |prj, cmd| // run command and assert error exit code cmd.assert_failure().stderr_eq(str![[r#" -Error: -script failed: revert: failed +Error: script failed: revert: failed "#]]); }); @@ -201,7 +199,7 @@ contract DeployScript is Script { let deploy_contract = deploy_script.display().to_string() + ":DeployScript"; let node_config = - NodeConfig::test().with_eth_rpc_url(Some(rpc::next_http_archive_rpc_endpoint())).silent(); + NodeConfig::test().with_eth_rpc_url(Some(rpc::next_http_archive_rpc_endpoint())); let (_api, handle) = spawn(node_config).await; let dev = handle.dev_accounts().next().unwrap(); cmd.set_current_dir(prj.root()); @@ -303,7 +301,7 @@ contract DeployScript is Script { let deploy_contract = deploy_script.display().to_string() + ":DeployScript"; let node_config = - NodeConfig::test().with_eth_rpc_url(Some(rpc::next_http_archive_rpc_endpoint())).silent(); + NodeConfig::test().with_eth_rpc_url(Some(rpc::next_http_archive_rpc_endpoint())); let (_api, handle) = spawn(node_config).await; let private_key = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80".to_string(); @@ -493,7 +491,7 @@ contract DeployScript is Script { let deploy_contract = deploy_script.display().to_string() + ":DeployScript"; let node_config = - NodeConfig::test().with_eth_rpc_url(Some(rpc::next_http_archive_rpc_endpoint())).silent(); + NodeConfig::test().with_eth_rpc_url(Some(rpc::next_http_archive_rpc_endpoint())); let (_api, handle) = spawn(node_config).await; let private_key = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80".to_string(); @@ -1012,13 +1010,20 @@ struct Transaction { // test we output arguments forgetest_async!(can_execute_script_with_arguments, |prj, cmd| { - cmd.args(["init", "--force"]).arg(prj.root()).assert_success().stdout_eq(str![[r#" -Target directory is not empty, but `--force` was specified + cmd.args(["init", "--force"]) + .arg(prj.root()) + .assert_success() + .stdout_eq(str![[r#" Initializing [..]... Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) Installed forge-std[..] Initialized forge project +"#]]) + .stderr_eq(str![[r#" +Warning: Target directory is not empty, but `--force` was specified +... + "#]]); let (_api, handle) = spawn(NodeConfig::test()).await; @@ -1134,13 +1139,20 @@ SIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet // test we output arguments forgetest_async!(can_execute_script_with_arguments_nested_deploy, |prj, cmd| { - cmd.args(["init", "--force"]).arg(prj.root()).assert_success().stdout_eq(str![[r#" -Target directory is not empty, but `--force` was specified + cmd.args(["init", "--force"]) + .arg(prj.root()) + .assert_success() + .stdout_eq(str![[r#" Initializing [..]... Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) Installed forge-std[..] Initialized forge project +"#]]) + .stderr_eq(str![[r#" +Warning: Target directory is not empty, but `--force` was specified +... + "#]]); let (_api, handle) = spawn(NodeConfig::test()).await; @@ -1301,13 +1313,20 @@ forgetest_async!(does_script_override_correctly, |prj, cmd| { }); forgetest_async!(assert_tx_origin_is_not_overritten, |prj, cmd| { - cmd.args(["init", "--force"]).arg(prj.root()).assert_success().stdout_eq(str![[r#" -Target directory is not empty, but `--force` was specified + cmd.args(["init", "--force"]) + .arg(prj.root()) + .assert_success() + .stdout_eq(str![[r#" Initializing [..]... Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) Installed forge-std[..] Initialized forge project +"#]]) + .stderr_eq(str![[r#" +Warning: Target directory is not empty, but `--force` was specified +... + "#]]); let script = prj @@ -1382,13 +1401,20 @@ If you wish to simulate on-chain transactions pass a RPC URL. }); forgetest_async!(assert_can_create_multiple_contracts_with_correct_nonce, |prj, cmd| { - cmd.args(["init", "--force"]).arg(prj.root()).assert_success().stdout_eq(str![[r#" -Target directory is not empty, but `--force` was specified + cmd.args(["init", "--force"]) + .arg(prj.root()) + .assert_success() + .stdout_eq(str![[r#" Initializing [..]... Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) Installed forge-std[..] Initialized forge project +"#]]) + .stderr_eq(str![[r#" +Warning: Target directory is not empty, but `--force` was specified +... + "#]]); let script = prj @@ -1609,20 +1635,26 @@ contract Script { cmd.arg("script").args([&script.to_string_lossy(), "--sig", "run"]); cmd.assert_failure().stderr_eq(str![[r#" -Error: -Multiple functions with the same name `run` found in the ABI +Error: Multiple functions with the same name `run` found in the ABI "#]]); }); forgetest_async!(can_decode_custom_errors, |prj, cmd| { - cmd.args(["init", "--force"]).arg(prj.root()).assert_success().stdout_eq(str![[r#" -Target directory is not empty, but `--force` was specified + cmd.args(["init", "--force"]) + .arg(prj.root()) + .assert_success() + .stdout_eq(str![[r#" Initializing [..]... Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) Installed forge-std[..] Initialized forge project +"#]]) + .stderr_eq(str![[r#" +Warning: Target directory is not empty, but `--force` was specified +... + "#]]); let script = prj @@ -1652,8 +1684,7 @@ contract CustomErrorScript is Script { cmd.forge_fuse().arg("script").arg(script).args(["--tc", "CustomErrorScript"]); cmd.assert_failure().stderr_eq(str![[r#" -Error: -script failed: CustomError() +Error: script failed: CustomError() "#]]); }); @@ -1709,7 +1740,6 @@ Script ran successfully. success: bool true ## Setting up 1 EVM. -Script contains a transaction to 0x0000000000000000000000000000000000000000 which does not contain any code. ========================== @@ -1733,6 +1763,9 @@ ONCHAIN EXECUTION COMPLETE & SUCCESSFUL. [SAVED_SENSITIVE_VALUES] +"#]]).stderr_eq(str![[r#" +Warning: Script contains a transaction to 0x0000000000000000000000000000000000000000 which does not contain any code. + "#]]); // Ensure that we can correctly estimate gas when base fee is zero but priority fee is not. @@ -1758,7 +1791,6 @@ Script ran successfully. success: bool true ## Setting up 1 EVM. -Script contains a transaction to 0x0000000000000000000000000000000000000000 which does not contain any code. ========================== @@ -1782,6 +1814,9 @@ ONCHAIN EXECUTION COMPLETE & SUCCESSFUL. [SAVED_SENSITIVE_VALUES] +"#]]).stderr_eq(str![[r#" +Warning: Script contains a transaction to 0x0000000000000000000000000000000000000000 which does not contain any code. + "#]]); }); @@ -1826,7 +1861,6 @@ Script ran successfully. success: bool true ## Setting up 1 EVM. -Script contains a transaction to 0x0000000000000000000000000000000000000000 which does not contain any code. ========================== @@ -1850,6 +1884,9 @@ ONCHAIN EXECUTION COMPLETE & SUCCESSFUL. [SAVED_SENSITIVE_VALUES] +"#]]).stderr_eq(str![[r#" +Warning: Script contains a transaction to 0x0000000000000000000000000000000000000000 which does not contain any code. + "#]]); }); @@ -1886,8 +1923,7 @@ contract SimpleScript is Script { ]); cmd.assert_failure().stderr_eq(str![[r#" -Error: -script failed: missing CREATE2 deployer +Error: script failed: missing CREATE2 deployer "#]]); }); @@ -2228,8 +2264,7 @@ contract ContractScript is Script { ) .unwrap(); cmd.arg("script").arg(script).args(["--fork-url", "https://public-node.testnet.rsk.co"]).assert_failure().stderr_eq(str![[r#" -Error: -Failed to deploy script: +Error: Failed to deploy script: backend: failed while inspecting; header validation error: `prevrandao` not set; `prevrandao` not set; "#]]); diff --git a/crates/forge/tests/cli/test_cmd.rs b/crates/forge/tests/cli/test_cmd.rs index 1c8ae36689db..d76a6124f7bd 100644 --- a/crates/forge/tests/cli/test_cmd.rs +++ b/crates/forge/tests/cli/test_cmd.rs @@ -2191,22 +2191,19 @@ Warning: the following cheatcode(s) are deprecated and will be removed in future forgetest_init!(requires_single_test, |prj, cmd| { cmd.args(["test", "--debug"]).assert_failure().stderr_eq(str![[r#" -Error: -2 tests matched your criteria, but exactly 1 test must match in order to run the debugger. +Error: 2 tests matched your criteria, but exactly 1 test must match in order to run the debugger. Use --match-contract and --match-path to further limit the search. "#]]); cmd.forge_fuse().args(["test", "--flamegraph"]).assert_failure().stderr_eq(str![[r#" -Error: -2 tests matched your criteria, but exactly 1 test must match in order to generate a flamegraph. +Error: 2 tests matched your criteria, but exactly 1 test must match in order to generate a flamegraph. Use --match-contract and --match-path to further limit the search. "#]]); cmd.forge_fuse().args(["test", "--flamechart"]).assert_failure().stderr_eq(str![[r#" -Error: -2 tests matched your criteria, but exactly 1 test must match in order to generate a flamechart. +Error: 2 tests matched your criteria, but exactly 1 test must match in order to generate a flamechart. Use --match-contract and --match-path to further limit the search. @@ -2215,7 +2212,7 @@ Use --match-contract and --match-path to further limit the search. forgetest_init!(deprecated_regex_arg, |prj, cmd| { cmd.args(["test", "--decode-internal", "test_Increment"]).assert_success().stderr_eq(str![[r#" -warning: specifying argument for --decode-internal is deprecated and will be removed in the future, use --match-test instead +Warning: specifying argument for --decode-internal is deprecated and will be removed in the future, use --match-test instead "#]]); }); diff --git a/crates/script-sequence/src/lib.rs b/crates/script-sequence/src/lib.rs index 3aa5fc65a766..11970e9478be 100644 --- a/crates/script-sequence/src/lib.rs +++ b/crates/script-sequence/src/lib.rs @@ -1,5 +1,8 @@ //! Script Sequence and related types. +#[macro_use] +extern crate foundry_common; + pub mod sequence; pub mod transaction; diff --git a/crates/script-sequence/src/sequence.rs b/crates/script-sequence/src/sequence.rs index 080c725be69b..e34b6d06a865 100644 --- a/crates/script-sequence/src/sequence.rs +++ b/crates/script-sequence/src/sequence.rs @@ -2,7 +2,7 @@ use crate::transaction::TransactionWithMetadata; use alloy_primitives::{hex, map::HashMap, TxHash}; use alloy_rpc_types::AnyTransactionReceipt; use eyre::{ContextCompat, Result, WrapErr}; -use foundry_common::{fs, shell, TransactionMaybeSigned, SELECTOR_LEN}; +use foundry_common::{fs, TransactionMaybeSigned, SELECTOR_LEN}; use foundry_compilers::ArtifactId; use foundry_config::Config; use serde::{Deserialize, Serialize}; @@ -127,8 +127,8 @@ impl ScriptSequence { } if !silent { - shell::println(format!("\nTransactions saved to: {}\n", path.display()))?; - shell::println(format!("Sensitive values saved to: {}\n", sensitive_path.display()))?; + sh_println!("\nTransactions saved to: {}\n", path.display())?; + sh_println!("Sensitive values saved to: {}\n", sensitive_path.display())?; } Ok(()) diff --git a/crates/script/src/broadcast.rs b/crates/script/src/broadcast.rs index 82f28562d62d..4058aa6c59b3 100644 --- a/crates/script/src/broadcast.rs +++ b/crates/script/src/broadcast.rs @@ -21,7 +21,7 @@ use foundry_cheatcodes::Wallets; use foundry_cli::utils::{has_batch_support, has_different_gas_calc}; use foundry_common::{ provider::{get_http_provider, try_get_http_provider, RetryProvider}, - shell, TransactionMaybeSigned, + TransactionMaybeSigned, }; use foundry_config::Config; use futures::{future::join_all, StreamExt}; @@ -424,8 +424,8 @@ impl BundledState { seq_progress.inner.write().finish(); } - shell::println("\n\n==========================")?; - shell::println("\nONCHAIN EXECUTION COMPLETE & SUCCESSFUL.")?; + sh_println!("\n\n==========================")?; + sh_println!("\nONCHAIN EXECUTION COMPLETE & SUCCESSFUL.")?; Ok(BroadcastedState { args: self.args, diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index 4b357ae23bd7..ef42740841b2 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -188,10 +188,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/execute.rs b/crates/script/src/execute.rs index d1191505a328..97555694a691 100644 --- a/crates/script/src/execute.rs +++ b/crates/script/src/execute.rs @@ -19,7 +19,7 @@ use foundry_cli::utils::{ensure_clean_constructor, needs_setup}; use foundry_common::{ fmt::{format_token, format_token_raw}, provider::get_http_provider, - shell, ContractsByArtifact, + ContractsByArtifact, }; use foundry_config::{Config, NamedChain}; use foundry_debugger::Debugger; @@ -196,7 +196,7 @@ impl PreExecutionState { let sender = tx.transaction.from().expect("no sender"); if let Some(ns) = new_sender { if sender != ns { - shell::println("You have more than one deployer who could predeploy libraries. Using `--sender` instead.")?; + sh_warn!("You have more than one deployer who could predeploy libraries. Using `--sender` instead.")?; return Ok(None); } } else if sender != self.script_config.evm_opts.sender { @@ -255,7 +255,7 @@ For more information, please see https://eips.ethereum.org/EIPS/eip-3855", .map(|(_, chain)| *chain as u64) .format(", ") ); - shell::println(msg.yellow())?; + sh_warn!("{}", msg)?; } Ok(()) } @@ -301,10 +301,7 @@ impl ExecutedState { let rpc_data = RpcData::from_transactions(&txs); if rpc_data.is_multi_chain() { - shell::eprintln(format!( - "{}", - "Multi chain deployment is still under development. Use with caution.".yellow() - ))?; + sh_warn!("Multi chain deployment is still under development. Use with caution.")?; if !self.build_data.libraries.is_empty() { eyre::bail!( "Multi chain deployment does not support library linking at the moment." @@ -382,7 +379,7 @@ impl ExecutedState { } } Err(_) => { - shell::println(format!("{returned:?}"))?; + sh_err!("Failed to decode return value: {:x?}", returned)?; } } @@ -400,7 +397,7 @@ impl PreSimulationState { result, }; let json = serde_json::to_string(&json_result)?; - shell::println(json)?; + sh_println!("{json}")?; if !self.execution_result.success { return Err(eyre::eyre!( @@ -423,7 +420,7 @@ impl PreSimulationState { warn!(verbosity, "no traces"); } - shell::println("Traces:")?; + sh_println!("Traces:")?; for (kind, trace) in &result.traces { let should_include = match kind { TraceKind::Setup => verbosity >= 5, @@ -434,22 +431,22 @@ impl PreSimulationState { if should_include { let mut trace = trace.clone(); decode_trace_arena(&mut trace, decoder).await?; - shell::println(render_trace_arena(&trace))?; + sh_println!("{}", render_trace_arena(&trace))?; } } - shell::println(String::new())?; + sh_println!()?; } if result.success { - shell::println(format!("{}", "Script ran successfully.".green()))?; + sh_println!("{}", "Script ran successfully.".green())?; } if self.script_config.evm_opts.fork_url.is_none() { - shell::println(format!("Gas used: {}", result.gas_used))?; + sh_println!("Gas used: {}", result.gas_used)?; } if result.success && !result.returned.is_empty() { - shell::println("\n== Return ==")?; + sh_println!("\n== Return ==")?; match func.abi_decode_output(&result.returned, false) { Ok(decoded) => { for (index, (token, output)) in decoded.iter().zip(&func.outputs).enumerate() { @@ -464,24 +461,24 @@ impl PreSimulationState { } else { index.to_string() }; - shell::println(format!( - "{}: {internal_type} {}", - label.trim_end(), - format_token(token) - ))?; + sh_println!( + "{label}: {internal_type} {value}", + label = label.trim_end(), + value = format_token(token) + )?; } } Err(_) => { - shell::println(format!("{:x?}", (&result.returned)))?; + sh_err!("{:x?}", (&result.returned))?; } } } let console_logs = decode_console_logs(&result.logs); if !console_logs.is_empty() { - shell::println("\n== Logs ==")?; + sh_println!("\n== Logs ==")?; for log in console_logs { - shell::println(format!(" {log}"))?; + sh_println!(" {log}")?; } } diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index 49650584fd71..ec19c0bf9716 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -5,6 +5,9 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#[macro_use] +extern crate foundry_common; + #[macro_use] extern crate tracing; @@ -27,7 +30,7 @@ use foundry_cli::{opts::CoreBuildArgs, utils::LoadConfig}; use foundry_common::{ abi::{encode_function_args, get_func}, evm::{Breakpoints, EvmArgs}, - shell, ContractsByArtifact, CONTRACT_MAX_SIZE, SELECTOR_LEN, + ContractsByArtifact, CONTRACT_MAX_SIZE, SELECTOR_LEN, }; use foundry_compilers::ArtifactId; use foundry_config::{ @@ -52,7 +55,6 @@ use foundry_evm::{ use foundry_wallets::MultiWalletOpts; use serde::Serialize; use std::path::PathBuf; -use yansi::Paint; mod broadcast; mod build; @@ -257,7 +259,7 @@ impl ScriptArgs { return match pre_simulation.args.dump.clone() { Some(ref path) => pre_simulation.run_debug_file_dumper(path), None => pre_simulation.run_debugger(), - } + }; } if pre_simulation.args.json { @@ -279,7 +281,7 @@ impl ScriptArgs { // Check if there are any missing RPCs and exit early to avoid hard error. if pre_simulation.execution_artifacts.rpc_data.missing_rpc { - shell::println("\nIf you wish to simulate on-chain transactions pass a RPC URL.")?; + sh_println!("\nIf you wish to simulate on-chain transactions pass a RPC URL.")?; return Ok(()); } @@ -293,7 +295,7 @@ impl ScriptArgs { // Exit early in case user didn't provide any broadcast/verify related flags. if !bundled.args.should_broadcast() { - shell::println("\nSIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more.")?; + sh_println!("\nSIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more.")?; return Ok(()); } @@ -436,12 +438,9 @@ impl ScriptArgs { if deployment_size > max_size { prompt_user = self.should_broadcast(); - shell::println(format!( - "{}", - format!( - "`{name}` is above the contract size limit ({deployment_size} > {max_size})." - ).red() - ))?; + sh_err!( + "`{name}` is above the contract size limit ({deployment_size} > {max_size})." + )?; } } } diff --git a/crates/script/src/multi_sequence.rs b/crates/script/src/multi_sequence.rs index 0aabcf79ac3f..ec2f03ae9855 100644 --- a/crates/script/src/multi_sequence.rs +++ b/crates/script/src/multi_sequence.rs @@ -146,8 +146,8 @@ impl MultiChainSequence { } if !silent { - println!("\nTransactions saved to: {}\n", self.path.display()); - println!("Sensitive details saved to: {}\n", self.sensitive_path.display()); + sh_println!("\nTransactions saved to: {}\n", self.path.display())?; + sh_println!("Sensitive details saved to: {}\n", self.sensitive_path.display())?; } Ok(()) diff --git a/crates/script/src/simulate.rs b/crates/script/src/simulate.rs index 44788672a2da..6eb11d7f9117 100644 --- a/crates/script/src/simulate.rs +++ b/crates/script/src/simulate.rs @@ -16,7 +16,7 @@ use eyre::{Context, Result}; use forge_script_sequence::{ScriptSequence, TransactionWithMetadata}; use foundry_cheatcodes::Wallets; use foundry_cli::utils::{has_different_gas_calc, now}; -use foundry_common::{get_contract_name, shell, ContractData}; +use foundry_common::{get_contract_name, ContractData}; use foundry_evm::traces::{decode_trace_arena, render_trace_arena}; use futures::future::{join_all, try_join_all}; use parking_lot::RwLock; @@ -24,7 +24,6 @@ use std::{ collections::{BTreeMap, VecDeque}, sync::Arc, }; -use yansi::Paint; /// Same as [ExecutedState](crate::execute::ExecutedState), but also contains [ExecutionArtifacts] /// which are obtained from [ScriptResult]. @@ -75,7 +74,7 @@ impl PreSimulationState { .collect::>>()?; if self.args.skip_simulation { - shell::println("\nSKIPPING ON CHAIN SIMULATION.")?; + sh_println!("\nSKIPPING ON CHAIN SIMULATION.")?; } else { transactions = self.simulate_and_fill(transactions).await?; } @@ -171,7 +170,9 @@ impl PreSimulationState { if let Some(tx) = tx { if is_noop_tx { let to = tx.contract_address.unwrap(); - shell::println(format!("Script contains a transaction to {to} which does not contain any code.").yellow())?; + sh_warn!( + "Script contains a transaction to {to} which does not contain any code." + )?; // Only prompt if we're broadcasting and we've not disabled interactivity. if self.args.should_broadcast() && @@ -218,11 +219,10 @@ 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() { - let n = rpcs.len(); - let s = if n != 1 { "s" } else { "" }; - println!("\n## Setting up {n} EVM{s}."); - } + + let n = rpcs.len(); + let s = if n != 1 { "s" } else { "" }; + sh_println!("\n## Setting up {n} EVM{s}.")?; let futs = rpcs.into_iter().map(|rpc| async move { let mut script_config = self.script_config.clone(); @@ -348,24 +348,24 @@ impl FilledTransactionsState { provider_info.gas_price()? }; - shell::println("\n==========================")?; - shell::println(format!("\nChain {}", provider_info.chain))?; + sh_println!("\n==========================")?; + sh_println!("\nChain {}", provider_info.chain)?; - shell::println(format!( + sh_println!( "\nEstimated gas price: {} gwei", format_units(per_gas, 9) .unwrap_or_else(|_| "[Could not calculate]".to_string()) .trim_end_matches('0') .trim_end_matches('.') - ))?; - shell::println(format!("\nEstimated total gas used for script: {total_gas}"))?; - shell::println(format!( + )?; + sh_println!("\nEstimated total gas used for script: {total_gas}")?; + sh_println!( "\nEstimated amount required: {} ETH", format_units(total_gas.saturating_mul(per_gas), 18) .unwrap_or_else(|_| "[Could not calculate]".to_string()) .trim_end_matches('0') - ))?; - shell::println("\n==========================")?; + )?; + sh_println!("\n==========================")?; } } diff --git a/crates/test-utils/src/script.rs b/crates/test-utils/src/script.rs index 9b018573261f..f15e91d5af78 100644 --- a/crates/test-utils/src/script.rs +++ b/crates/test-utils/src/script.rs @@ -226,11 +226,9 @@ impl ScriptTester { trace!(target: "tests", "STDOUT\n{stdout}\n\nSTDERR\n{stderr}"); - let output = if expected.is_err() { &stderr } else { &stdout }; - if !output.contains(expected.as_str()) { - let which = if expected.is_err() { "stderr" } else { "stdout" }; + if !stdout.contains(expected.as_str()) && !stderr.contains(expected.as_str()) { panic!( - "--STDOUT--\n{stdout}\n\n--STDERR--\n{stderr}\n\n--EXPECTED--\n{:?} in {which}", + "--STDOUT--\n{stdout}\n\n--STDERR--\n{stderr}\n\n--EXPECTED--\n{:?} not found in stdout or stderr", expected.as_str() ); } @@ -286,7 +284,7 @@ impl ScriptOutcome { Self::OkNoEndpoint => "If you wish to simulate on-chain transactions pass a RPC URL.", Self::OkSimulation => "SIMULATION COMPLETE. To broadcast these", Self::OkBroadcast => "ONCHAIN EXECUTION COMPLETE & SUCCESSFUL", - Self::WarnSpecifyDeployer => "You have more than one deployer who could predeploy libraries. Using `--sender` instead.", + Self::WarnSpecifyDeployer => "Warning: You have more than one deployer who could predeploy libraries. Using `--sender` instead.", Self::MissingSender => "You seem to be using Foundry's default sender. Be sure to set your own --sender", Self::MissingWallet => "No associated wallet", Self::StaticCallNotAllowed => "staticcall`s are not allowed after `broadcast`; use `startBroadcast` instead", diff --git a/crates/verify/src/bytecode.rs b/crates/verify/src/bytecode.rs index 661eb5c8dcb3..02ca28c20066 100644 --- a/crates/verify/src/bytecode.rs +++ b/crates/verify/src/bytecode.rs @@ -299,7 +299,7 @@ impl VerifyBytecodeArgs { ); if self.json { - println!("{}", serde_json::to_string(&json_results)?); + sh_println!("{}", serde_json::to_string(&json_results)?)?; } return Ok(()); @@ -395,7 +395,7 @@ impl VerifyBytecodeArgs { &config, ); if self.json { - println!("{}", serde_json::to_string(&json_results)?); + sh_println!("{}", serde_json::to_string(&json_results)?)?; } return Ok(()); } @@ -498,7 +498,7 @@ impl VerifyBytecodeArgs { } if self.json { - println!("{}", serde_json::to_string(&json_results)?); + sh_println!("{}", serde_json::to_string(&json_results)?)?; } Ok(()) } diff --git a/crates/verify/src/etherscan/mod.rs b/crates/verify/src/etherscan/mod.rs index 8b8c2bc3549f..9b5b1fa345d3 100644 --- a/crates/verify/src/etherscan/mod.rs +++ b/crates/verify/src/etherscan/mod.rs @@ -17,7 +17,6 @@ use foundry_cli::utils::{get_provider, read_constructor_args_file, LoadConfig}; use foundry_common::{ abi::encode_function_args, retry::{Retry, RetryError}, - shell, }; use foundry_compilers::{artifacts::BytecodeObject, Artifact}; use foundry_config::{Chain, Config}; @@ -424,7 +423,7 @@ impl EtherscanVerificationProvider { if maybe_creation_code.starts_with(bytecode) { let constructor_args = &maybe_creation_code[bytecode.len()..]; let constructor_args = hex::encode(constructor_args); - shell::println(format!("Identified constructor arguments: {constructor_args}"))?; + sh_println!("Identified constructor arguments: {constructor_args}")?; Ok(constructor_args) } else { eyre::bail!("Local bytecode doesn't match on-chain bytecode") @@ -569,6 +568,7 @@ mod tests { cmd.args(["build", "--force"]).assert_success().stdout_eq(str![[r#" [COMPILING_FILES] with [SOLC_VERSION] +... [SOLC_VERSION] [ELAPSED] Compiler run successful! diff --git a/crates/verify/src/lib.rs b/crates/verify/src/lib.rs index 9d2372964850..a46fdba90155 100644 --- a/crates/verify/src/lib.rs +++ b/crates/verify/src/lib.rs @@ -3,6 +3,12 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#[macro_use] +extern crate foundry_common; + +#[macro_use] +extern crate tracing; + mod etherscan; pub mod provider; @@ -21,6 +27,3 @@ pub use verify::{VerifierArgs, VerifyArgs, VerifyCheckArgs}; mod types; mod utils; - -#[macro_use] -extern crate tracing;